diff --git a/docs/README.md b/docs/README.md index fad1080..67ff4f3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -26,6 +26,7 @@ This is the documentation router for Devflow Native. | OpenCairn split example | [examples/opencairn-parallel-split.md](./examples/opencairn-parallel-split.md) | | Simple status output example | [examples/simple-status-output.md](./examples/simple-status-output.md) | | Session list filter examples | [examples/session-list-filters.md](./examples/session-list-filters.md) | +| Repo-local Codex/Claude plugin init skill | [../plugins/devflow/skills/init/SKILL.md](../plugins/devflow/skills/init/SKILL.md) | | Repo-local Codex plugin start skill | [../plugins/devflow/skills/start/SKILL.md](../plugins/devflow/skills/start/SKILL.md) | | Repo-local Codex/Claude plugin status skill | [../plugins/devflow/skills/status/SKILL.md](../plugins/devflow/skills/status/SKILL.md) | | Repo-local Codex/Claude plugin doctor skill | [../plugins/devflow/skills/doctor/SKILL.md](../plugins/devflow/skills/doctor/SKILL.md) | diff --git a/docs/architecture.md b/docs/architecture.md index b6fbb2d..4eb44ae 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -48,7 +48,7 @@ packages/cli devflow harness/init/status/split/finish/doctor/gates/session/review packages/mcp - devflow.status/devflow.split/devflow.finish/devflow.doctor/devflow.next_prompt/devflow.rewrite_prompt/devflow.sessions_codex/devflow.sessions_attach_plan/devflow.sessions_attach/devflow.sessions_list/devflow.sessions_note tools + devflow.init/status/split/finish/doctor/next_prompt/rewrite_prompt/session/work/review/gate/harness tools packages/integrations Claude Code plugin, Codex plugin and MCP config, editor hooks @@ -152,6 +152,7 @@ Initial tools: ```text devflow.status +devflow.init devflow.split devflow.finish devflow.doctor @@ -293,6 +294,8 @@ docs/contributing/workflow.md scripts/project-health.* .devflow/config.json .devflow/state/ +.github/workflows/devflow.yml +plugins/devflow/ ``` ## Boundaries diff --git a/docs/contributing/commands.md b/docs/contributing/commands.md index 8409e07..dc0086c 100644 --- a/docs/contributing/commands.md +++ b/docs/contributing/commands.md @@ -70,10 +70,11 @@ This loop should answer three daily questions before larger surfaces exist: `devflow split` now has a CLI renderer over the core split contract, can read project-specific split tasks, and can register generated sessions into the -local work item registry. `devflow init` now has a first guarded scaffold implementation: +local work item registry. `devflow init` now has a preset-aware bootstrap path: without `--confirm`, it renders the plan only; with `--confirm`, it writes the -minimum project contract, sets `review.required` to `true`, and skips existing -files instead of overwriting them. `devflow health` checks those scaffold files +project contract, optional native harness files, optional GitHub CI, inferred +gates, and review policy while skipping existing non-AGENTS files and +augmenting existing `AGENTS.md`. `devflow health` checks those scaffold files and configured gates. `devflow gates run` executes one configured gate and records pass/fail evidence. @@ -179,32 +180,45 @@ output for a chosen platform. Creates or updates a project contract. -The MVP implementation is confirmation-gated: +The implementation is confirmation-gated and preset-aware: ```powershell devflow init --repo C:\path\to\repo --profile standard --platform windows-powershell --json devflow init --repo C:\path\to\repo --profile standard --platform windows-powershell --confirm --json +devflow init --preset solo-product --targets codex,claude --ci github --review required --json +devflow init --preset solo-product --targets codex,claude --ci github --review required --confirm --json ``` Inputs: - profile +- preset: `solo-product`, `research`, or `content-site` - platform +- native targets: `codex`, `claude` +- CI provider: `github` +- review mode: `required` or `optional` - docs template -- gate profile +- inferred gate profile from package scripts Outputs: - `AGENTS.md` +- `AGENTS.md` Devflow section append when the file already exists - docs router - architecture maps - testing strategy - `.devflow/config.json` - -The generated `.devflow/config.json` enables `review.required` by default so a -newly initialized repo gets the strict review loop immediately. The generated -workflow notes point agents through `devflow review request`, `devflow review -record`, and then `devflow finish`. +- optional `plugins/devflow/*` native harness files +- optional `.github/workflows/devflow.yml` + +The generated `.devflow/config.json` records the chosen preset, platform, +review policy, and inferred gates. `solo-product` enables +`review.required` by default. `research` and `content-site` default to +risk-based or direct-docs-main policy unless `--review required` is passed. +The generated workflow notes point agents through `devflow review request`, +`devflow review record`, and then `devflow finish` when review is required. +Native harness files use the same canonical contents as `devflow harness +install`, and the generated init skill is a thin wrapper over the CLI. Safety rules: diff --git a/docs/harness.md b/docs/harness.md index 2c22383..9f977c2 100644 --- a/docs/harness.md +++ b/docs/harness.md @@ -151,8 +151,19 @@ to publish the latest handoff. ## Harness Commands -The harness command group is the install and repair surface for existing repos. -It should adopt mature repositories instead of blindly scaffolding over them. +`devflow init` is the bootstrap surface for new repositories or repositories +that want a full Devflow project contract in one step. It can write preset +policy, `.devflow/config.json`, `AGENTS.md`, docs, inferred gates, GitHub CI, +and native Codex/Claude plugin files: + +```text +devflow init --preset solo-product --targets codex,claude --ci github --review required +devflow init --preset solo-product --targets codex,claude --ci github --review required --confirm +``` + +The harness command group remains the install and repair surface for existing +repos. It should adopt mature repositories instead of blindly scaffolding over +them. ```text devflow harness inspect --targets codex,claude,superpowers,codegraph @@ -224,6 +235,13 @@ missing `review.required` config that can be merged without dropping existing gates. Project instructions and gate definitions are reported but not rewritten automatically. +The Devflow init skill is intentionally a thin wrapper over this CLI behavior. +It chooses `solo-product`, `research`, or `content-site`, runs a dry-run +`devflow init` command, explains the plan, runs `--confirm` only when requested, +then verifies with `devflow health` and `devflow harness health` when native +targets were installed. Policy and file generation stay in Devflow core so +Codex, Claude, shell sessions, and future hosts reproduce the same bootstrap. + ## Install UX The public README should not lead with "run npm install" as the main user diff --git a/docs/roadmap.md b/docs/roadmap.md index c022678..10b7cf9 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -76,14 +76,20 @@ Exit criteria: - a new repo can get a complete development workflow scaffold - health status can detect missing docs, workflow files, and malformed gates -Current implementation note: `devflow init` has a first confirmation-gated -scaffold path. The CLI renders a scaffold plan by default and writes -`.devflow/config.json`, `AGENTS.md`, docs router, workflow, testing strategy, -and architecture map index only with `--confirm`, skipping existing files -instead of overwriting them. `devflow health` and MCP `devflow.health` can now -report missing scaffold files, configured gates, and invalid gate definitions -with missing ids, missing commands, or duplicate ids. Template customization -remains later Phase 2 work. +Current implementation note: `devflow init` has a confirmation-gated bootstrap +path. The CLI renders a scaffold plan by default and writes only with +`--confirm`. It supports `--preset solo-product|research|content-site`, +`--targets codex,claude`, `--ci github`, and `--review required|optional`. +It writes `.devflow/config.json`, `AGENTS.md`, docs router, workflow, testing +strategy, architecture map index, optional GitHub Actions workflow, and optional +canonical `plugins/devflow/*` native harness files. Existing non-AGENTS files +are skipped; existing `AGENTS.md` files are augmented with a Devflow section. +Gate definitions are inferred from conservative package scripts such as +`docs:check`, `lint`, `test`, `build`, `bench`, and `links:check`. `devflow +health` and MCP `devflow.health` can now report missing scaffold files, +configured gates, and invalid gate definitions with missing ids, missing +commands, or duplicate ids. Richer custom template packs remain later Phase 2 +work. Current implementation note: `devflow harness inspect --targets codex,claude,superpowers,codegraph` reports native plugin readiness, MCP launch @@ -193,6 +199,7 @@ Build: - local Devflow MCP server - `devflow.doctor` MCP tool - `devflow.status` MCP tool +- `devflow.init` MCP tool - `devflow.harness_inspect` MCP tool - `devflow.harness_plan` MCP tool - `devflow.harness_health` MCP tool @@ -234,7 +241,7 @@ Exit criteria: - plugin/slash-command UX calls the same core contracts as the CLI Current implementation note: `packages/mcp` has testable handler functions for -`devflow.status`, `devflow.harness_inspect`, `devflow.harness_plan`, +`devflow.status`, `devflow.init`, `devflow.harness_inspect`, `devflow.harness_plan`, `devflow.harness_health`, `devflow.harness_smoke`, confirmed-write `devflow.harness_repair`, `devflow.split`, `devflow.explain_term`, `devflow.doctor`, `devflow.finish`, `devflow.record_gate`, `devflow.gates_run`, diff --git a/packages/cli/README.md b/packages/cli/README.md index f69ad9d..44cf038 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -87,14 +87,21 @@ packages/cli/ - `devflow mistakes list` - `devflow mistakes detect` -`devflow init` currently renders a scaffold plan by default and writes the -minimum project contract only when `--confirm` is provided. The first scaffold -includes `.devflow/config.json`, `AGENTS.md`, a docs router, workflow notes, -testing strategy, and an architecture map index. It also sets -`review.required` to `true` and points workflow notes through `devflow review -request`, `devflow review record`, and `devflow finish`. Existing files are -skipped instead of overwritten. `devflow health` checks that the same scaffold -files and at least one configured gate are present. `devflow harness inspect` reports +`devflow init` renders a scaffold plan by default and writes only when +`--confirm` is provided. It now supports `--preset solo-product|research|content-site`, +`--targets codex,claude`, `--ci github`, and `--review required|optional`. +The scaffold includes `.devflow/config.json`, `AGENTS.md`, a docs router, +workflow notes, testing strategy, and an architecture map index. With native +targets, it also installs canonical `plugins/devflow/*` files for Codex and +Claude and ignores them as local harness files unless `--repo-visible` is +passed. With `--ci github`, it creates `.github/workflows/devflow.yml` from +inferred package scripts. `AGENTS.md` is appended with a Devflow section when +it already exists instead of being replaced. `solo-product` defaults to +`review.required: true`; `research` and `content-site` default to risk-based +or direct-docs-main review policy unless `--review required` is passed. Existing +non-AGENTS files are skipped instead of overwritten. `devflow health` checks +that the same scaffold files and at least one configured gate are present. +`devflow harness inspect` reports native Codex and Claude Code readiness, MCP config presence, instruction files, Superpowers signals, CodeGraph-style provider signals, configured gates, and the smallest install or repair recommendations without writing files. @@ -148,10 +155,9 @@ filtered by `--agent `, `--work `, `--since `, sorted by unchanged. Use `--json` for the stable agent contract or omit it for a short terminal summary with active filters, attached session ids, changed-file counts, observed times, limit totals, sort choice, or manual note summaries. The text -summary also surfaces a compact warning count when local state has warnings. `devflow -sessions note` records manual or external session context as local state. The -future `devflow init` command can reuse the same rendering and config -infrastructure once the MVP loop is stable. +summary also surfaces a compact warning count when local state has warnings. +`devflow sessions note` records manual or external session context as local +state. `devflow finish --json` returns a false-completion guard contract in addition to the original evidence summary. It reads configured gates from diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js index 8fd0125..51f1a6b 100644 --- a/packages/cli/src/index.js +++ b/packages/cli/src/index.js @@ -173,14 +173,23 @@ try { async function renderInit(argsForCommand) { const options = parseOptions(argsForCommand); const repoPath = options.repo ?? cwd(); + const packageJson = await readPackageJson(repoPath); const plan = createInitPlan({ repo: repoPath, profile: options.profile, + preset: options.preset, platform: options.platform ?? defaultPlatformName(), + targets: parseTargetList(options.targets), + ci: options.ci, + review: options.review, + packageJson, }); if (options.confirm) { - const result = await writeInitPlan(repoPath, plan, { confirmed: true }); + const result = await writeInitPlan(repoPath, plan, { + confirmed: true, + repoVisible: options["repo-visible"], + }); render({ ...plan, result }, options.json); return; } @@ -188,6 +197,17 @@ async function renderInit(argsForCommand) { render(plan, options.json); } +async function readPackageJson(repoPath) { + try { + return JSON.parse(await readFile(join(repoPath, "package.json"), "utf8")); + } catch (error) { + if (error.code === "ENOENT" || error instanceof SyntaxError) { + return null; + } + throw error; + } +} + async function renderHealth(argsForCommand) { const options = parseOptions(argsForCommand); const repoPath = options.repo ?? cwd(); @@ -1224,6 +1244,13 @@ function readPackageVersion() { function renderHelp(group) { const groups = { + init: [ + "devflow init [--json]", + "devflow init --preset solo-product --targets codex,claude --ci github --review required [--json]", + "devflow init --preset solo-product --targets codex,claude --ci github --review required --confirm [--json]", + "devflow init --preset research --review optional [--json]", + "devflow init --preset content-site --ci github [--json]", + ], harness: [ "devflow harness inspect [--json]", "devflow harness plan [--json]", @@ -1301,7 +1328,7 @@ function renderHelp(group) { " node packages/cli/src/index.js harness health", "", "Core commands:", - " init Plan or write a .devflow project scaffold", + " init Plan or write a preset-based Devflow project bootstrap", " health Check the project scaffold", " doctor Inspect local shell/tooling rules", " mistakes Record and detect repeated agent mistake memory", diff --git a/packages/cli/test/cli-mvp.test.mjs b/packages/cli/test/cli-mvp.test.mjs index 193055a..d58e1e7 100644 --- a/packages/cli/test/cli-mvp.test.mjs +++ b/packages/cli/test/cli-mvp.test.mjs @@ -728,6 +728,70 @@ test("CLI init --confirm writes the minimum project scaffold", async () => { assert.equal(config.review.required, true); }); +test("CLI init --confirm writes preset bootstrap files for native agents and github ci", async () => { + const repoPath = await createTempGitRepo(); + await writeFile( + join(repoPath, "package.json"), + `${JSON.stringify( + { + scripts: { + "docs:check": "node scripts/check-doc-links.mjs", + lint: "eslint .", + test: "node --test", + build: "vite build", + }, + }, + null, + 2, + )}\n`, + "utf8", + ); + + const { stdout } = await execFileAsync("node", [ + "packages/cli/src/index.js", + "init", + "--repo", + repoPath, + "--preset", + "solo-product", + "--targets", + "codex,claude", + "--ci", + "github", + "--review", + "required", + "--confirm", + "--json", + ]); + const parsed = JSON.parse(stdout); + const config = JSON.parse(await readFile(join(repoPath, ".devflow", "config.json"), "utf8")); + const agents = await readFile(join(repoPath, "AGENTS.md"), "utf8"); + const workflow = await readFile(join(repoPath, ".github", "workflows", "devflow.yml"), "utf8"); + const initSkill = await readFile(join(repoPath, "plugins", "devflow", "skills", "init", "SKILL.md"), "utf8"); + const initCommand = await readFile(join(repoPath, "plugins", "devflow", "commands", "init.md"), "utf8"); + const codexManifest = JSON.parse(await readFile(join(repoPath, "plugins", "devflow", ".codex-plugin", "plugin.json"), "utf8")); + const claudeManifest = JSON.parse(await readFile(join(repoPath, "plugins", "devflow", ".claude-plugin", "plugin.json"), "utf8")); + const gitignore = await readFile(join(repoPath, ".gitignore"), "utf8"); + + assert.equal(parsed.preset, "solo-product"); + assert.equal(parsed.ci, "github"); + assert.deepEqual(parsed.targets, ["codex", "claude"]); + assert.equal(config.defaultProfile, "solo-product"); + assert.equal(config.review.required, true); + assert.deepEqual( + config.gates.map((gate) => gate.command), + ["npm run docs:check", "npm run lint", "npm test", "npm run build"], + ); + assert.match(agents, /Direct Main Exceptions/); + assert.match(workflow, /npm run docs:check/); + assert.match(initSkill, /devflow init --preset/); + assert.match(initSkill, /thin wrapper/); + assert.match(initCommand, /devflow init/); + assert.equal(codexManifest.name, "devflow"); + assert.equal(claudeManifest.name, "devflow"); + assert.match(gitignore, /^plugins\/devflow\/$/m); +}); + test("CLI health reports missing scaffold files", async () => { const repoPath = await createTempGitRepo(); @@ -822,7 +886,7 @@ test("CLI harness inspect renders target readiness JSON", async () => { await writeFile(join(repoPath, "plugins", "devflow", "hooks", "stop.mjs"), "\n", "utf8"); await writeFile(join(repoPath, "plugins", "devflow", "skills", "start", "SKILL.md"), "# Start\n", "utf8"); await writeFile(join(repoPath, "plugins", "devflow", "skills", "finish", "SKILL.md"), "# Finish\n", "utf8"); - for (const skill of ["status", "doctor", "harness", "work", "gates", "review", "split", "next", "rewrite", "sessions", "explain"]) { + for (const skill of ["init", "status", "doctor", "harness", "work", "gates", "review", "split", "next", "rewrite", "sessions", "explain"]) { await mkdir(join(repoPath, "plugins", "devflow", "skills", skill), { recursive: true }); await writeFile(join(repoPath, "plugins", "devflow", "skills", skill, "SKILL.md"), `# ${skill}\n`, "utf8"); } diff --git a/packages/cli/test/plugin-contract.test.mjs b/packages/cli/test/plugin-contract.test.mjs index 774daaf..1bfab55 100644 --- a/packages/cli/test/plugin-contract.test.mjs +++ b/packages/cli/test/plugin-contract.test.mjs @@ -19,6 +19,7 @@ test("repo-local Codex plugin exposes devflow start skill and marketplace entry" const claudeHooks = JSON.parse(await readFile("plugins/devflow/hooks/claude-hooks.json", "utf8")); const mcpConfig = JSON.parse(await readFile("plugins/devflow/.mcp.json", "utf8")); const startSkill = await readFile("plugins/devflow/skills/start/SKILL.md", "utf8"); + const initSkill = await readFile("plugins/devflow/skills/init/SKILL.md", "utf8"); const statusSkill = await readFile("plugins/devflow/skills/status/SKILL.md", "utf8"); const doctorSkill = await readFile("plugins/devflow/skills/doctor/SKILL.md", "utf8"); const harnessSkill = await readFile("plugins/devflow/skills/harness/SKILL.md", "utf8"); @@ -32,6 +33,7 @@ test("repo-local Codex plugin exposes devflow start skill and marketplace entry" const sessionsSkill = await readFile("plugins/devflow/skills/sessions/SKILL.md", "utf8"); const finishSkill = await readFile("plugins/devflow/skills/finish/SKILL.md", "utf8"); const startCommand = await readFile("plugins/devflow/commands/start.md", "utf8"); + const initCommand = await readFile("plugins/devflow/commands/init.md", "utf8"); const statusCommand = await readFile("plugins/devflow/commands/status.md", "utf8"); const explainCommand = await readFile("plugins/devflow/commands/explain.md", "utf8"); const reviewCommand = await readFile("plugins/devflow/commands/review.md", "utf8"); @@ -75,6 +77,12 @@ test("repo-local Codex plugin exposes devflow start skill and marketplace entry" assert.match(startSkill, /not dependent on any one profile/); assert.match(startSkill, /Get-Content -LiteralPath/); + assert.match(initSkill, /devflow init --preset/); + assert.match(initSkill, /thin wrapper/); + assert.match(initSkill, /solo-product/); + assert.match(initSkill, /research/); + assert.match(initSkill, /content-site/); + assert.match(statusSkill, /devflow status --json/); assert.match(doctorSkill, /devflow doctor --json/); assert.match(harnessSkill, /devflow harness inspect/); @@ -122,6 +130,7 @@ test("repo-local Codex plugin exposes devflow start skill and marketplace entry" assert.match(finishSkill, /commit, PR, continue, or next-session prompt/); assert.match(startCommand, /devflow doctor --json/); assert.match(startCommand, /devflow status --json/); + assert.match(initCommand, /devflow init/); assert.match(statusCommand, /devflow status --json/); assert.match(explainCommand, /devflow explain/); assert.match(explainCommand, /plain-language/); diff --git a/packages/core/src/index.js b/packages/core/src/index.js index f2d65a1..4ba0314 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -7,6 +7,13 @@ import { promisify } from "node:util"; const execFileAsync = promisify(execFile); const DEVFLOW_RUNTIME_GITIGNORE_ENTRIES = [".devflow/state/", ".devflow/next-prompt.md"]; const DEVFLOW_LOCAL_HARNESS_GITIGNORE_ENTRIES = ["plugins/devflow/"]; +const INIT_PRESETS = new Set(["standard", "solo-product", "research", "content-site"]); +const INIT_PRESET_GATE_ORDER = { + standard: ["docs:check", "test"], + "solo-product": ["docs:check", "lint", "test", "build"], + research: ["bench", "benchmark", "eval", "test"], + "content-site": ["lint", "build", "links:check", "link:check", "test"], +}; export function createStatusSummary(input = {}) { const changedFiles = input.changedFiles ?? []; @@ -496,8 +503,17 @@ export function createPromptRewrite(input = {}) { export function createInitPlan(input = {}) { const repoPath = input.repo ?? process.cwd(); - const profile = input.profile ?? "standard"; + const preset = normalizeInitPreset(input); + const profile = input.profile ?? preset; const platform = input.platform ?? "windows-powershell"; + const targets = normalizeInitTargets(input.targets); + const ci = normalizeInitCi(input.ci); + const reviewRequired = normalizeInitReviewRequired(input.review, preset); + const gates = inferInitGates({ + preset, + packageJson: input.packageJson, + gates: input.gates, + }); const files = [ { path: ".devflow/config.json", @@ -507,10 +523,12 @@ export function createInitPlan(input = {}) { schemaVersion: 1, defaultProfile: profile, defaultPlatform: platform, + preset, review: { - required: true, + required: reviewRequired, + policy: initReviewPolicy(preset, reviewRequired), }, - gates: [{ id: "docs-check", command: "npm run docs:check" }], + gates, }, null, 2, @@ -536,30 +554,12 @@ export function createInitPlan(input = {}) { { path: "docs/contributing/workflow.md", kind: "workflow", - content: [ - "# Development Workflow", - "", - "1. Run `devflow status` before starting.", - "2. Before finishing, run `devflow review request --work ` when review is required.", - "3. Record review evidence with `devflow review record --work `.", - "4. Record completed work with `devflow finish`.", - "5. Include changed files, gates, risks, review evidence, and the next-session prompt.", - "", - ].join("\n"), + content: createInitWorkflowDoc({ preset, reviewRequired, gates }), }, { path: "docs/testing/strategy.md", kind: "testing", - content: [ - "# Testing Strategy", - "", - "Record every verification gate that proves a work item is ready.", - "", - "## Initial Gate", - "", - "- `npm run docs:check`", - "", - ].join("\n"), + content: createInitTestingDoc({ preset, gates }), }, { path: "docs/architecture/maps/README.md", @@ -574,15 +574,19 @@ export function createInitPlan(input = {}) { { path: "AGENTS.md", kind: "agent-guide", - content: [ - "# Agent Guide", - "", - "Start with `devflow doctor` and `devflow status` before command-heavy work.", - "Before claiming completion, request and record required review evidence, then run `devflow finish`.", - "", - ].join("\n"), + content: createInitAgentGuide({ preset, reviewRequired, targets, ci }), }, ]; + if (ci === "github") { + files.push({ + path: ".github/workflows/devflow.yml", + kind: "ci-workflow", + content: createInitGithubWorkflow({ gates }), + }); + } + for (const file of createInitHarnessFiles(targets)) { + files.push(file); + } return { schemaVersion: "0.1", @@ -590,6 +594,13 @@ export function createInitPlan(input = {}) { repo: { absolutePath: repoPath, }, + preset, + targets, + ci, + review: { + required: reviewRequired, + policy: initReviewPolicy(preset, reviewRequired), + }, profile: { name: profile, requiredRuntime: false, @@ -605,6 +616,245 @@ export function createInitPlan(input = {}) { }; } +function normalizeInitPreset(input = {}) { + const rawPreset = input.preset ?? input.profile ?? "standard"; + const preset = String(rawPreset || "standard").trim() || "standard"; + if (input.preset !== undefined && !INIT_PRESETS.has(preset)) { + throw new Error(`Unsupported devflow init preset: ${preset}`); + } + return preset; +} + +function normalizeInitTargets(targets) { + if (targets === undefined) { + return []; + } + if (Array.isArray(targets) && targets.length === 0) { + return []; + } + return normalizeHarnessTargets(Array.isArray(targets) ? targets : [targets]) + .filter((target) => target === "codex" || target === "claude"); +} + +function normalizeInitCi(ci) { + if (ci === undefined || ci === false) { + return "none"; + } + const normalized = String(ci).trim().toLowerCase(); + if (normalized === "none") { + return "none"; + } + if (normalized !== "github") { + throw new Error(`Unsupported devflow init ci provider: ${ci}`); + } + return normalized; +} + +function normalizeInitReviewRequired(review, preset) { + if (review === undefined) { + return preset === "standard" || preset === "solo-product"; + } + if (review === true) { + return true; + } + if (review === false) { + return false; + } + const normalized = String(review).trim().toLowerCase(); + if (normalized === "required") { + return true; + } + if (normalized === "optional" || normalized === "none") { + return false; + } + throw new Error(`Unsupported devflow init review mode: ${review}`); +} + +function initReviewPolicy(preset, required) { + if (required) { + return "required"; + } + if (preset === "research") { + return "risk-based"; + } + if (preset === "content-site") { + return "direct-docs-main"; + } + return "optional"; +} + +function inferInitGates(input = {}) { + if (Array.isArray(input.gates) && input.gates.length > 0) { + return normalizeGates(input.gates); + } + + const scripts = extractPackageScripts(input.packageJson); + const order = INIT_PRESET_GATE_ORDER[input.preset] ?? INIT_PRESET_GATE_ORDER.standard; + const gates = []; + const seen = new Set(); + + for (const script of order) { + if (!scripts.has(script) || seen.has(script)) { + continue; + } + seen.add(script); + gates.push({ + id: scriptToGateId(script), + command: script === "test" ? "npm test" : `npm run ${script}`, + }); + } + + if (gates.length > 0) { + return gates; + } + + return [{ id: "docs-check", command: "npm run docs:check" }]; +} + +function extractPackageScripts(packageJson) { + if (!packageJson) { + return new Set(); + } + let parsed = packageJson; + if (typeof packageJson === "string") { + try { + parsed = JSON.parse(packageJson); + } catch { + return new Set(); + } + } + if (!isPlainObject(parsed?.scripts)) { + return new Set(); + } + return new Set(Object.keys(parsed.scripts)); +} + +function scriptToGateId(script) { + return String(script).replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, ""); +} + +function createInitWorkflowDoc(input = {}) { + const directMainLine = input.preset === "solo-product" + ? "Small docs-only or private-note edits can go direct main; feature, CI, security, database, or public behavior changes use PR review." + : input.preset === "research" + ? "Small experiment-note updates can go direct main; benchmark, fixture, scoring, or published-result changes use PR review." + : input.preset === "content-site" + ? "Small typo or article edits can go direct main; design, feature, SEO, routing, or build changes use PR review." + : "Small private docs edits can go direct main; cross-boundary or public behavior changes use PR review."; + return [ + "# Development Workflow", + "", + `Preset: ${input.preset ?? "standard"}.`, + "", + "1. Run `devflow status` before starting.", + "2. Before finishing, run `devflow review request --work ` when review is required.", + "3. Record review evidence with `devflow review record --work `.", + "4. Record completed work with `devflow finish`.", + "5. Include changed files, gates, risks, review evidence, and the next-session prompt.", + "", + "## Direct Main Exceptions", + "", + directMainLine, + "", + "## Initial Gates", + "", + ...(input.gates ?? []).map((gate) => `- ${gate.id}: \`${gate.command}\``), + "", + ].join("\n"); +} + +function createInitTestingDoc(input = {}) { + return [ + "# Testing Strategy", + "", + "Record every verification gate that proves a work item is ready.", + "", + `Preset: ${input.preset ?? "standard"}.`, + "", + "## Initial Gates", + "", + ...(input.gates ?? []).map((gate) => `- \`${gate.command}\``), + "", + ].join("\n"); +} + +function createInitAgentGuide(input = {}) { + const reviewLine = input.reviewRequired + ? "Before claiming completion, request and record required review evidence, then run `devflow finish`." + : "Before claiming completion, record relevant gates, risk notes, and the next-session prompt with `devflow finish`."; + const directMainLine = input.preset === "content-site" + ? "Small writing-only edits may go direct main; design, feature, SEO, routing, or build changes should use PR review." + : input.preset === "research" + ? "Small notes may go direct main; benchmark, fixture, scoring, or public-result changes should use PR review." + : "Small docs-only edits may go direct main; feature, CI, security, database, or public behavior changes should use PR review."; + return [ + "# Agent Guide", + "", + "## Devflow Native", + "", + `Preset: ${input.preset ?? "standard"}.`, + "", + "Start with `devflow doctor` and `devflow status` before command-heavy work.", + reviewLine, + "Use `devflow init` as the repo bootstrap surface; skills should call Devflow commands and interpret the result.", + "", + "## Direct Main Exceptions", + "", + directMainLine, + "", + "## Installed Agent Targets", + "", + input.targets?.length ? `- ${input.targets.join(", ")}` : "- none", + "", + "## CI", + "", + `- ${input.ci ?? "none"}`, + "", + ].join("\n"); +} + +function createInitGithubWorkflow(input = {}) { + const gates = input.gates?.length ? input.gates : [{ id: "docs-check", command: "npm run docs:check" }]; + return [ + "name: Devflow", + "", + "on:", + " pull_request:", + " push:", + " branches: [main]", + "", + "jobs:", + " gates:", + " runs-on: ubuntu-latest", + " steps:", + " - uses: actions/checkout@v4", + " - uses: actions/setup-node@v4", + " with:", + " node-version: 20", + " - run: npm install", + ...gates.flatMap((gate) => [ + ` - name: ${gate.id}`, + ` run: ${gate.command}`, + ]), + "", + ].join("\n"); +} + +function createInitHarnessFiles(targets) { + const paths = new Set(); + for (const target of targets ?? []) { + const summary = createHarnessTargetSummary(target, []); + for (const check of summary.checks ?? []) { + if (check.path !== "AGENTS.md") { + paths.add(check.path); + } + } + } + return [...paths] + .map((path) => ({ path, kind: "harness", content: harnessFileContent(path) })) + .filter((file) => file.content !== null); +} + export function createHealthSummary(input = {}) { const requiredFiles = (input.requiredFiles ?? defaultRequiredFiles()).map((file) => { const path = typeof file === "string" ? file : file.path; @@ -1348,12 +1598,23 @@ export async function writeInitPlan(repoPath, plan, options = {}) { } const written = []; + const updated = []; const skipped = []; for (const file of plan.files ?? []) { const target = join(repoPath, file.path); try { - await readFile(target, "utf8"); + const existing = await readFile(target, "utf8"); + if (file.path === "AGENTS.md") { + const appended = appendDevflowAgentGuide(existing, file.content); + if (appended !== existing) { + await writeFile(target, appended, "utf8"); + updated.push({ path: file.path, action: "append-devflow-section" }); + } else { + skipped.push({ path: file.path, reason: "already-exists" }); + } + continue; + } skipped.push({ path: file.path, reason: "already-exists" }); continue; } catch (error) { @@ -1367,7 +1628,10 @@ export async function writeInitPlan(repoPath, plan, options = {}) { written.push({ path: file.path }); } - const gitignore = await ensureDevflowRuntimeGitignore(repoPath); + const hasLocalHarness = (plan.files ?? []).some((file) => file.path?.startsWith("plugins/devflow/")); + const gitignore = await ensureDevflowRuntimeGitignore(repoPath, { + includeLocalHarness: hasLocalHarness && !isRepoVisibleHarnessInstall(options), + }); if (gitignore.status === "written") { written.push({ path: ".gitignore" }); } else { @@ -1378,10 +1642,22 @@ export async function writeInitPlan(repoPath, plan, options = {}) { schemaVersion: "0.1", command: "init_result", written, + updated, skipped, }; } +function appendDevflowAgentGuide(existing, generated) { + if (existing.includes("## Devflow Native")) { + return existing; + } + const markerIndex = generated.indexOf("## Devflow Native"); + const section = markerIndex >= 0 ? generated.slice(markerIndex).trim() : generated.trim(); + const normalized = existing.length > 0 && !existing.endsWith("\n") ? `${existing}\n` : existing; + const spacer = normalized.endsWith("\n\n") ? "" : "\n"; + return `${normalized}${spacer}${section}\n`; +} + export async function ensureDevflowRuntimeGitignore(repoPath, options = {}) { const target = join(repoPath, ".gitignore"); let existing = ""; @@ -2742,6 +3018,7 @@ function harnessProbePaths() { "plugins/devflow/hooks/tool-result.mjs", "plugins/devflow/hooks/stop.mjs", "plugins/devflow/.mcp.json", + "plugins/devflow/skills/init/SKILL.md", "plugins/devflow/skills/start/SKILL.md", "plugins/devflow/skills/status/SKILL.md", "plugins/devflow/skills/doctor/SKILL.md", @@ -2755,6 +3032,7 @@ function harnessProbePaths() { "plugins/devflow/skills/sessions/SKILL.md", "plugins/devflow/skills/explain/SKILL.md", "plugins/devflow/skills/finish/SKILL.md", + "plugins/devflow/commands/init.md", "plugins/devflow/commands/start.md", "plugins/devflow/commands/status.md", "plugins/devflow/commands/doctor.md", @@ -2804,6 +3082,7 @@ function createHarnessTargetSummary(target, existingPaths) { { path: "plugins/devflow/hooks/tool-result.mjs", kind: "hook-script" }, { path: "plugins/devflow/hooks/stop.mjs", kind: "hook-script" }, { path: "plugins/devflow/.mcp.json", kind: "mcp-config" }, + { path: "plugins/devflow/skills/init/SKILL.md", kind: "skill" }, { path: "plugins/devflow/skills/start/SKILL.md", kind: "skill" }, { path: "plugins/devflow/skills/status/SKILL.md", kind: "skill" }, { path: "plugins/devflow/skills/doctor/SKILL.md", kind: "skill" }, @@ -2831,6 +3110,7 @@ function createHarnessTargetSummary(target, existingPaths) { { path: "plugins/devflow/hooks/tool-result.mjs", kind: "hook-script" }, { path: "plugins/devflow/hooks/stop.mjs", kind: "hook-script" }, { path: "plugins/devflow/.mcp.json", kind: "mcp-config" }, + { path: "plugins/devflow/skills/init/SKILL.md", kind: "skill" }, { path: "plugins/devflow/skills/start/SKILL.md", kind: "skill" }, { path: "plugins/devflow/skills/status/SKILL.md", kind: "skill" }, { path: "plugins/devflow/skills/doctor/SKILL.md", kind: "skill" }, @@ -2845,6 +3125,7 @@ function createHarnessTargetSummary(target, existingPaths) { { path: "plugins/devflow/skills/explain/SKILL.md", kind: "skill" }, { path: "plugins/devflow/skills/finish/SKILL.md", kind: "skill" }, { path: "plugins/devflow/commands/start.md", kind: "command" }, + { path: "plugins/devflow/commands/init.md", kind: "command" }, { path: "plugins/devflow/commands/status.md", kind: "command" }, { path: "plugins/devflow/commands/doctor.md", kind: "command" }, { path: "plugins/devflow/commands/harness.md", kind: "command" }, @@ -3203,6 +3484,11 @@ function harnessFileContent(path) { "plugins/devflow/hooks/pre-tool-use.mjs": createHarnessPreToolUseScript(), "plugins/devflow/hooks/tool-result.mjs": createHarnessToolResultScript(), "plugins/devflow/hooks/stop.mjs": createHarnessStopScript(), + "plugins/devflow/skills/init/SKILL.md": createHarnessSkillFile( + "init", + "Initialize or bootstrap a repository with Devflow Native presets.", + "This skill is a thin wrapper over `devflow init --preset --targets --ci --review `. Choose the preset, run a dry-run JSON plan first, explain writes and inferred gates, then run `--confirm` only when requested.", + ), "plugins/devflow/skills/start/SKILL.md": [ "---", "name: devflow-start", @@ -3282,6 +3568,11 @@ function harnessFileContent(path) { "If `devflow finish` returns `review.nextAction.command` or `review.nextAction.recordCommand`, follow both commands before claiming completion.", "", ].join("\n"), + "plugins/devflow/commands/init.md": createHarnessCommandFile( + "Initialize or bootstrap a repository with Devflow Native presets.", + "[--preset solo-product|research|content-site] [--targets codex,claude] [--ci github] [--review required] [--confirm]", + "Run `devflow init $ARGUMENTS --json`; when no arguments are provided, choose a preset first, dry-run before confirmed writes, then verify with `devflow health --json` and native `devflow harness health --targets codex,claude --json` when targets were installed.", + ), "plugins/devflow/commands/start.md": createHarnessCommandFile( "Load Devflow start context before command-heavy work.", "[--work ] [--agent ]", diff --git a/packages/core/test/mvp-contract.test.mjs b/packages/core/test/mvp-contract.test.mjs index 3895273..2f26f06 100644 --- a/packages/core/test/mvp-contract.test.mjs +++ b/packages/core/test/mvp-contract.test.mjs @@ -508,6 +508,89 @@ test("init plan writes scaffold files only after confirmation", async () => { assert.match(gitignore, /^\.devflow\/next-prompt\.md$/m); }); +test("init plan bootstraps solo product presets with native harness and github ci", () => { + const repoPath = "C:\\repo"; + const plan = createInitPlan({ + repo: repoPath, + preset: "solo-product", + targets: ["codex", "claude"], + ci: "github", + review: "required", + platform: "windows-powershell", + packageJson: { + scripts: { + "docs:check": "node scripts/check-doc-links.mjs", + lint: "eslint .", + test: "node --test", + build: "vite build", + }, + }, + }); + const config = JSON.parse(plan.files.find((file) => file.path === ".devflow/config.json").content); + const agents = plan.files.find((file) => file.path === "AGENTS.md").content; + const workflow = plan.files.find((file) => file.path === ".github/workflows/devflow.yml").content; + + assert.equal(plan.preset, "solo-product"); + assert.deepEqual(plan.targets, ["codex", "claude"]); + assert.equal(plan.ci, "github"); + assert.equal(config.defaultProfile, "solo-product"); + assert.equal(config.review.required, true); + assert.deepEqual( + config.gates.map((gate) => gate.id), + ["docs-check", "lint", "test", "build"], + ); + assert.ok(plan.files.some((file) => file.path === "plugins/devflow/skills/init/SKILL.md")); + assert.ok(plan.files.some((file) => file.path === "plugins/devflow/commands/init.md")); + assert.ok(plan.files.some((file) => file.path === "plugins/devflow/.codex-plugin/plugin.json")); + assert.ok(plan.files.some((file) => file.path === "plugins/devflow/.claude-plugin/plugin.json")); + assert.match(agents, /Direct Main Exceptions/); + assert.match(workflow, /npm run docs:check/); + assert.match(workflow, /npm run build/); +}); + +test("init plan exposes research and content-site preset policy", () => { + const research = createInitPlan({ + repo: "C:\\repo", + preset: "research", + packageJson: { scripts: { bench: "node bench/run.mjs", test: "node --test" } }, + }); + const contentSite = createInitPlan({ + repo: "C:\\repo", + preset: "content-site", + packageJson: { scripts: { build: "astro build", lint: "eslint .", "links:check": "lychee docs" } }, + }); + const researchConfig = JSON.parse(research.files.find((file) => file.path === ".devflow/config.json").content); + const contentConfig = JSON.parse(contentSite.files.find((file) => file.path === ".devflow/config.json").content); + + assert.equal(research.preset, "research"); + assert.equal(contentSite.preset, "content-site"); + assert.equal(researchConfig.review.required, false); + assert.ok(researchConfig.gates.some((gate) => gate.id === "bench")); + assert.equal(contentConfig.review.required, false); + assert.deepEqual( + contentConfig.gates.map((gate) => gate.id), + ["lint", "build", "links-check"], + ); +}); + +test("init write augments existing AGENTS instructions instead of replacing them", async () => { + const repoPath = await mkdtemp(join(tmpdir(), "devflow-init-agents-")); + await writeFile(join(repoPath, "AGENTS.md"), "# Existing Agent Rules\n\nKeep custom project rules.\n", "utf8"); + const plan = createInitPlan({ + repo: repoPath, + preset: "solo-product", + review: "required", + }); + + const result = await writeInitPlan(repoPath, plan, { confirmed: true }); + const agents = await readFile(join(repoPath, "AGENTS.md"), "utf8"); + + assert.ok(result.updated.some((file) => file.path === "AGENTS.md")); + assert.match(agents, /Keep custom project rules/); + assert.match(agents, /## Devflow Native/); + assert.match(agents, /Direct Main Exceptions/); +}); + test("init plan preserves and deduplicates Devflow runtime gitignore entries", async () => { const repoPath = await mkdtemp(join(tmpdir(), "devflow-init-gitignore-")); await writeFile(join(repoPath, ".gitignore"), "node_modules\n.devflow/state/\n", "utf8"); @@ -601,6 +684,7 @@ test("harness inspect summary reports native target readiness and recommendation "plugins/devflow/hooks/tool-result.mjs", "plugins/devflow/hooks/stop.mjs", "plugins/devflow/.mcp.json", + "plugins/devflow/skills/init/SKILL.md", "plugins/devflow/skills/start/SKILL.md", "plugins/devflow/skills/status/SKILL.md", "plugins/devflow/skills/doctor/SKILL.md", @@ -661,7 +745,7 @@ test("harness inspector reads repo files without writing", async () => { await writeFile(join(repoPath, "plugins", "devflow", "hooks", "stop.mjs"), "\n", "utf8"); await writeFile(join(repoPath, "plugins", "devflow", "skills", "start", "SKILL.md"), "# Start\n", "utf8"); await writeFile(join(repoPath, "plugins", "devflow", "skills", "finish", "SKILL.md"), "# Finish\n", "utf8"); - for (const skill of ["status", "doctor", "harness", "work", "gates", "review", "split", "next", "rewrite", "sessions", "explain"]) { + for (const skill of ["init", "status", "doctor", "harness", "work", "gates", "review", "split", "next", "rewrite", "sessions", "explain"]) { await mkdir(join(repoPath, "plugins", "devflow", "skills", skill), { recursive: true }); await writeFile(join(repoPath, "plugins", "devflow", "skills", skill, "SKILL.md"), `# ${skill}\n`, "utf8"); } diff --git a/packages/mcp/README.md b/packages/mcp/README.md index a4e9cfd..29cf52f 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -9,6 +9,7 @@ JSON-RPC transport for MCP-capable hosts. - `devflow.doctor` - `devflow.status` - `devflow.health` +- `devflow.init` - `devflow.harness_inspect` - `devflow.harness_plan` - `devflow.harness_health` @@ -62,6 +63,15 @@ missing files, configured gates, invalid gate definitions, and recommendations without reading private agent history. A gate is invalid when its id is missing, its command is missing, or its id duplicates another configured gate. +`devflow.init` exposes the same preset-aware project bootstrap as the CLI. +Without `confirm: true`, it returns the scaffold plan only. With +`confirm: true`, it writes `.devflow/config.json`, project docs, +`AGENTS.md` creation or augmentation, optional GitHub Actions workflow, and +optional canonical `plugins/devflow/*` native harness files. It accepts +`preset`, `targets`, `ci`, `review`, `repoVisible`, and optional `gates` or +`packageJson`; otherwise it reads `package.json` from the target repo to infer +conservative gates. + `devflow.harness_inspect`, `devflow.harness_plan`, `devflow.harness_health`, and `devflow.harness_smoke` expose the native harness read path to MCP-capable hosts. They accept `repo` plus optional `targets` as an array or comma-separated diff --git a/packages/mcp/src/index.js b/packages/mcp/src/index.js index 7bc5d28..74f9f40 100644 --- a/packages/mcp/src/index.js +++ b/packages/mcp/src/index.js @@ -1,4 +1,5 @@ import { readFile } from "node:fs/promises"; +import { join } from "node:path"; import { discoverClaudeSessions, @@ -15,6 +16,7 @@ import { import { createDoctorSummary, createFinishSummary, + createInitPlan, createMistakeDetection, createMistakeListSummary, createNextPrompt, @@ -54,6 +56,7 @@ import { recordWorkUnblockedEvent, runConfiguredGate, writeHarnessRepair, + writeInitPlan, } from "../../core/src/index.js"; const tools = [ @@ -65,6 +68,10 @@ const tools = [ name: "devflow.health", description: "Inspect required scaffold files and configured verification gates.", }, + { + name: "devflow.init", + description: "Plan or write a preset-based Devflow project bootstrap.", + }, { name: "devflow.harness_inspect", description: "Inspect Codex, Claude Code, Superpowers, and optional CodeGraph-style harness readiness without writing files.", @@ -229,6 +236,10 @@ export async function callTool(name, args = {}) { return callHealth(args); } + if (name === "devflow.init") { + return callInit(args); + } + if (name === "devflow.harness_inspect") { return callHarnessInspect(args); } @@ -412,6 +423,44 @@ async function callHealth(args) { return toolResult(summary, `devflow health: ${summary.status}`); } +async function callInit(args) { + const repoPath = args.repo ?? process.cwd(); + const packageJson = args.packageJson ?? await readInitPackageJson(repoPath); + const plan = createInitPlan({ + repo: repoPath, + profile: args.profile, + preset: args.preset, + platform: args.platform, + targets: parseHarnessTargets(args.targets), + ci: args.ci, + review: args.review, + packageJson, + gates: args.gates, + }); + + if (!args.confirm) { + return toolResult(plan, "devflow init: plan"); + } + + const result = await writeInitPlan(repoPath, plan, { + confirmed: true, + repoVisible: Boolean(args.repoVisible ?? args["repo-visible"]), + }); + + return toolResult({ ...plan, result }, "devflow init: written"); +} + +async function readInitPackageJson(repoPath) { + try { + return JSON.parse(await readFile(join(repoPath, "package.json"), "utf8")); + } catch (error) { + if (error.code === "ENOENT" || error instanceof SyntaxError) { + return null; + } + throw error; + } +} + async function callHarnessInspect(args) { const repoPath = args.repo ?? process.cwd(); const summary = await readHarnessInspect(repoPath, { diff --git a/packages/mcp/test/mcp-contract.test.mjs b/packages/mcp/test/mcp-contract.test.mjs index 44631bd..68fbf7c 100644 --- a/packages/mcp/test/mcp-contract.test.mjs +++ b/packages/mcp/test/mcp-contract.test.mjs @@ -14,6 +14,7 @@ test("MCP lists initial devflow tools", () => { assert.ok(names.includes("devflow.doctor")); assert.ok(names.includes("devflow.status")); assert.ok(names.includes("devflow.health")); + assert.ok(names.includes("devflow.init")); assert.ok(names.includes("devflow.harness_inspect")); assert.ok(names.includes("devflow.harness_plan")); assert.ok(names.includes("devflow.harness_health")); @@ -51,6 +52,82 @@ test("MCP lists initial devflow tools", () => { assert.ok(tools.every((tool) => tool.inputSchema?.type === "object")); }); +test("MCP init renders a preset bootstrap dry run without writing files", async () => { + const repoPath = await mkdtemp(join(tmpdir(), "devflow-mcp-init-plan-")); + await writeFile( + join(repoPath, "package.json"), + `${JSON.stringify({ + scripts: { + "docs:check": "node scripts/check-doc-links.mjs", + lint: "eslint .", + test: "node --test", + build: "vite build", + }, + })}\n`, + "utf8", + ); + + const result = await callTool("devflow.init", { + repo: repoPath, + preset: "solo-product", + targets: "codex,claude", + ci: "github", + review: "required", + }); + + assert.equal(result.structuredContent.command, "init"); + assert.equal(result.structuredContent.preset, "solo-product"); + assert.deepEqual(result.structuredContent.targets, ["codex", "claude"]); + assert.equal(result.structuredContent.ci, "github"); + assert.deepEqual( + JSON.parse(result.structuredContent.files.find((file) => file.path === ".devflow/config.json").content).gates.map( + (gate) => gate.command, + ), + ["npm run docs:check", "npm run lint", "npm test", "npm run build"], + ); + assert.ok(result.structuredContent.files.some((file) => file.path === "plugins/devflow/skills/init/SKILL.md")); + assert.match(result.content[0].text, /devflow init: plan/); + await assert.rejects(readFile(join(repoPath, ".devflow", "config.json"), "utf8"), /ENOENT/); +}); + +test("MCP init writes confirmed preset bootstrap files", async () => { + const repoPath = await mkdtemp(join(tmpdir(), "devflow-mcp-init-confirm-")); + await writeFile(join(repoPath, "AGENTS.md"), "# Existing Rules\n\nKeep this.\n", "utf8"); + await writeFile( + join(repoPath, "package.json"), + `${JSON.stringify({ + scripts: { + "docs:check": "node scripts/check-doc-links.mjs", + test: "node --test", + }, + })}\n`, + "utf8", + ); + + const result = await callTool("devflow.init", { + repo: repoPath, + preset: "solo-product", + targets: ["codex", "claude"], + ci: "github", + review: "required", + confirm: true, + }); + const config = JSON.parse(await readFile(join(repoPath, ".devflow", "config.json"), "utf8")); + const agents = await readFile(join(repoPath, "AGENTS.md"), "utf8"); + const workflow = await readFile(join(repoPath, ".github", "workflows", "devflow.yml"), "utf8"); + const initSkill = await readFile(join(repoPath, "plugins", "devflow", "skills", "init", "SKILL.md"), "utf8"); + + assert.equal(result.structuredContent.result.command, "init_result"); + assert.ok(result.structuredContent.result.updated.some((file) => file.path === "AGENTS.md")); + assert.equal(config.preset, "solo-product"); + assert.equal(config.review.required, true); + assert.match(agents, /Keep this/); + assert.match(agents, /## Devflow Native/); + assert.match(workflow, /npm run docs:check/); + assert.match(initSkill, /devflow init --preset/); + assert.match(result.content[0].text, /devflow init: written/); +}); + test("MCP harness tools inspect, plan, and health-check native setup", async () => { const repoPath = await mkdtemp(join(tmpdir(), "devflow-mcp-harness-")); await writeFile(join(repoPath, "AGENTS.md"), "# Agent Guide\n"); diff --git a/packages/mcp/test/stdio-contract.test.mjs b/packages/mcp/test/stdio-contract.test.mjs index 5b6b771..8cbb538 100644 --- a/packages/mcp/test/stdio-contract.test.mjs +++ b/packages/mcp/test/stdio-contract.test.mjs @@ -118,6 +118,7 @@ test("stdio transport responds while stdin remains open", async () => { assert.equal(response.id, 4); assert.ok(response.result.tools.some((tool) => tool.name === "devflow.status")); + assert.ok(response.result.tools.some((tool) => tool.name === "devflow.init")); assert.equal(stderr, ""); }); diff --git a/plugins/devflow/commands/init.md b/plugins/devflow/commands/init.md new file mode 100644 index 0000000..88908c3 --- /dev/null +++ b/plugins/devflow/commands/init.md @@ -0,0 +1,23 @@ +--- +description: Initialize or bootstrap a repository with Devflow Native presets. +argument-hint: "[--preset solo-product|research|content-site] [--targets codex,claude] [--ci github] [--review required] [--confirm]" +--- + +Use the Devflow Init workflow. + +Default to a dry run when `$ARGUMENTS` does not include `--confirm`: + +```powershell +devflow init $ARGUMENTS --json +``` + +If `$ARGUMENTS` is empty, inspect the repo and choose the preset before running: + +```powershell +devflow init --preset solo-product --targets codex,claude --ci github --review required --json +``` + +Explain planned files, inferred gates, review policy, CI workflow, and native +harness targets. Run a confirmed write only when the maintainer has asked to +proceed, then verify with `devflow health --json` and, for native agent targets, +`devflow harness health --targets codex,claude --json`. diff --git a/plugins/devflow/skills/init/SKILL.md b/plugins/devflow/skills/init/SKILL.md new file mode 100644 index 0000000..e70bb2f --- /dev/null +++ b/plugins/devflow/skills/init/SKILL.md @@ -0,0 +1,60 @@ +--- +name: init +description: Initialize or bootstrap a repository with Devflow Native presets by calling devflow init and interpreting the result. +--- + +# Devflow Init + +Use this skill when a maintainer wants to set up Devflow Native in a new or +existing repository, especially when they mention init, bootstrap, preset, +review policy, CI, or native Codex/Claude harness files. + +This skill is a thin wrapper over the Devflow CLI. The CLI owns policy, +file generation, review config, gate inference, CI workflow output, and native +harness file installation. The skill chooses a preset, calls `devflow init`, +and explains the result. + +## Preset Choice + +- Use `solo-product` for product repositories where feature changes should + normally use PR review, `review.required` should be enabled, CI should run, + and `AGENTS.md` should record direct-main exceptions. +- Use `research` for paper, experiment, benchmark, or evaluation repos where + evidence logs, fixtures, reproducibility, and bench gates matter most. +- Use `content-site` for docs, blog, or marketing-content repos where small + writing edits can go direct main but build, lint, link, design, SEO, route, + or feature changes still need gates and review. + +## Steps + +1. Run `devflow doctor --json` and apply the local execution contract. +2. Inspect package scripts and existing instructions with `devflow status --json` + plus targeted file reads when needed. +3. Pick the narrowest preset that matches the repo. +4. Render a dry run first, for example: + +```powershell +devflow init --preset solo-product --targets codex,claude --ci github --review required --json +``` + +5. Explain planned writes, skipped files, inferred gates, CI choice, and whether + `AGENTS.md` will be created or augmented. +6. Only run the confirmed write when the maintainer asked to proceed or the + workflow is already confirmation-gated: + +```powershell +devflow init --preset solo-product --targets codex,claude --ci github --review required --confirm --json +``` + +7. Verify with `devflow health --json`. If native agent files were installed, + also run `devflow harness health --targets codex,claude --json`. + +## Output + +Report: + +- chosen preset and why +- files written, updated, skipped, and ignored +- inferred gates and CI workflow +- review policy +- next command, such as health, harness health, or finish evidence recording