feat(work): pnpm work decompose subcommand
Closes the gap surfaced by the user: `pnpm work` usage referenced
`decompose` (via docs + the to-prd skill) but the subcommand was
never built. Mirrors `pnpm work dispatch`'s shape.
scripts/work/decompose.mjs (new):
- validatePrdForDecompose(prdPath) — refuses draft (must go
through human review first), in-review (review incomplete),
shipped (epic already exists); accepts only approved
- printDecomposePlan(prdId, prdPath, frontmatter) — print-mode
output showing the PRD's eligibility + sandcastle invocation
plan + auth modes
- executeDecompose(prdId, prdPath, prdText) — invokes sandcastle
with .sandcastle/decomposer.prompt.md, passing PRD_FILE_CONTENT
promptArg. The decomposer agent writes the epic + per-story
files to disk on a sandcastle branch the human can review
- runCli(args, { workRoot }) — entry point used by cli.mjs
- Direct invocation also supported (mirrors dispatch.mjs's
invokedDirectly guard, NEW pattern after this commit)
scripts/work/decompose.test.mjs (new, 9 tests, all green):
- validatePrdForDecompose: accepts approved; rejects draft,
in-review, shipped, unknown status, missing file
- runCli: writes error + returns 1 on missing PRD; writes error
+ returns 1 on draft PRD; prints plan + returns 0 on approved
scripts/work/cli.mjs:
- Adds `decompose` subcommand to usage + dispatch
- Usage formatting realigned for the 3-line subcommand block
scripts/work/dispatch.mjs:
- **Fix** the bug surfaced by the user: dispatch.mjs's CLI ran
as a top-level side effect whenever any of its exports was
imported. decompose.mjs imports resolveClaudeAuth from it, so
importing decompose.mjs printed "No ready task to dispatch."
Added an `import.meta.url === \`file://${process.argv[1]}\``
guard so the CLI only runs when invoked directly. This unblocks
cross-import without side effects.
Smoke-tested end-to-end:
- `pnpm work decompose` (no id) prints usage + exits 2
- `pnpm work decompose 2026-05-13-binder-wrap-helper` prints the
decompose plan with status: approved (eligible)
- 9/9 unit tests green
- dispatch.mjs's existing direct-invocation path unchanged
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import path from "node:path";
|
|||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { buildState } from "./state-builder.mjs";
|
import { buildState } from "./state-builder.mjs";
|
||||||
import { runCli as runPrdShip } from "./prd-ship.mjs";
|
import { runCli as runPrdShip } from "./prd-ship.mjs";
|
||||||
|
import { runCli as runDecompose } from "./decompose.mjs";
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const REPO_ROOT = path.resolve(__dirname, "..", "..");
|
const REPO_ROOT = path.resolve(__dirname, "..", "..");
|
||||||
@@ -116,11 +117,14 @@ function mark(status) {
|
|||||||
|
|
||||||
function usage() {
|
function usage() {
|
||||||
console.log(
|
console.log(
|
||||||
"Usage: pnpm work <rebuild-state|status|next|ready|blocked|dispatch|prd-ship>",
|
"Usage: pnpm work <rebuild-state|status|next|ready|blocked|dispatch|decompose|prd-ship>",
|
||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
" dispatch Print the next dispatch plan (use --execute to invoke sandcastle)",
|
" dispatch Print the next dispatch plan (use --execute to invoke sandcastle)",
|
||||||
);
|
);
|
||||||
|
console.log(
|
||||||
|
" decompose <prd-id> Decompose an approved PRD into epic + stories (use --execute to invoke sandcastle)",
|
||||||
|
);
|
||||||
console.log(
|
console.log(
|
||||||
" prd-ship <id> Flip a PRD's status to `shipped` (run after its epic completes)",
|
" prd-ship <id> Flip a PRD's status to `shipped` (run after its epic completes)",
|
||||||
);
|
);
|
||||||
@@ -145,4 +149,9 @@ else if (cmd === "dispatch") {
|
|||||||
workRoot: WORK_ROOT,
|
workRoot: WORK_ROOT,
|
||||||
});
|
});
|
||||||
process.exit(exitCode);
|
process.exit(exitCode);
|
||||||
|
} else if (cmd === "decompose") {
|
||||||
|
runDecompose(process.argv.slice(3), {
|
||||||
|
repoRoot: REPO_ROOT,
|
||||||
|
workRoot: WORK_ROOT,
|
||||||
|
}).then((code) => process.exit(code));
|
||||||
} else usage();
|
} else usage();
|
||||||
|
|||||||
234
scripts/work/decompose.mjs
Normal file
234
scripts/work/decompose.mjs
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* scripts/work/decompose.mjs
|
||||||
|
*
|
||||||
|
* Decomposer dispatcher — takes an approved PRD and invokes the decomposer
|
||||||
|
* agent to write the epic folder + per-requirement story files under
|
||||||
|
* docs/work/<epic-slug>/.
|
||||||
|
*
|
||||||
|
* Default mode (no --execute): print the dispatch plan + validate the PRD
|
||||||
|
* (refuses to proceed on draft / in-review / shipped). Safe anywhere.
|
||||||
|
*
|
||||||
|
* --execute mode: requires @ai-hero/sandcastle + auth (Claude subscription
|
||||||
|
* via ~/.claude OR ANTHROPIC_API_KEY). Mirrors `pnpm work dispatch --execute`.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* pnpm work decompose <prd-id>
|
||||||
|
* pnpm work decompose <prd-id> --execute
|
||||||
|
*/
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { findPrdPath, parseFrontmatter } from "./prd-ship.mjs";
|
||||||
|
import { resolveClaudeAuth } from "./dispatch.mjs";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const REPO_ROOT = path.resolve(__dirname, "..", "..");
|
||||||
|
const WORK_ROOT = path.join(REPO_ROOT, "docs", "work");
|
||||||
|
const SANDCASTLE_DIR = path.join(REPO_ROOT, ".sandcastle");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that a PRD is in a decomposable state. Throws on draft (must go
|
||||||
|
* through human review), in-review (review not yet complete), shipped (epic
|
||||||
|
* already exists), or missing.
|
||||||
|
*
|
||||||
|
* Returns the parsed frontmatter + body for the caller to pass into the
|
||||||
|
* decomposer.
|
||||||
|
*/
|
||||||
|
export function validatePrdForDecompose(prdPath) {
|
||||||
|
if (!fs.existsSync(prdPath)) {
|
||||||
|
throw new Error(`PRD file not found: ${prdPath}`);
|
||||||
|
}
|
||||||
|
const text = fs.readFileSync(prdPath, "utf8");
|
||||||
|
const { frontmatter } = parseFrontmatter(text);
|
||||||
|
const status = frontmatter.status;
|
||||||
|
|
||||||
|
if (status === "draft") {
|
||||||
|
throw new Error(
|
||||||
|
`PRD status is "draft" — human review required before decomposing. ` +
|
||||||
|
`Flip status to "approved" in the PRD frontmatter, then re-run.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (status === "in-review") {
|
||||||
|
throw new Error(
|
||||||
|
`PRD status is "in-review" — review must complete (status -> approved) before decomposing.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (status === "shipped") {
|
||||||
|
throw new Error(
|
||||||
|
`PRD status is "shipped" — its epic should already exist under docs/work/. ` +
|
||||||
|
`Re-decomposing a shipped PRD is not supported.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (status !== "approved") {
|
||||||
|
throw new Error(
|
||||||
|
`Unexpected PRD status "${status}" — expected "approved" to decompose.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { frontmatter, text };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print what would happen on --execute. Validation runs in both modes; this
|
||||||
|
* is the "preview" companion of executeDecompose().
|
||||||
|
*/
|
||||||
|
export function printDecomposePlan(prdId, prdPath, frontmatter) {
|
||||||
|
console.log("=== Decompose plan ===");
|
||||||
|
console.log(` PRD: ${path.relative(REPO_ROOT, prdPath)}`);
|
||||||
|
console.log(` Id: ${prdId}`);
|
||||||
|
console.log(` Title: ${frontmatter.title ?? "(no title)"}`);
|
||||||
|
console.log(` Status: ${frontmatter.status} (eligible to decompose)`);
|
||||||
|
console.log();
|
||||||
|
console.log(` Decomposer prompt: .sandcastle/decomposer.prompt.md`);
|
||||||
|
console.log(
|
||||||
|
` Output: docs/work/<epic-slug>/_epic.md + per-requirement story files`,
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
console.log("To run for real:");
|
||||||
|
console.log(
|
||||||
|
" - With Claude subscription: `claude login` (one-time) then `pnpm work decompose <id> --execute`",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
" - With API key: `ANTHROPIC_API_KEY=... pnpm work decompose <id> --execute`",
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
console.log(
|
||||||
|
"(Execute mode requires @ai-hero/sandcastle, a sandbox provider, and auth — see docs/guides/runbook.md.)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoke sandcastle with the decomposer prompt + the PRD file content. The
|
||||||
|
* decomposer agent writes the epic + stories to disk inside the sandbox; the
|
||||||
|
* orchestrator then has those files in a branch the human can review.
|
||||||
|
*/
|
||||||
|
export async function executeDecompose(prdId, prdPath, prdText) {
|
||||||
|
const auth = resolveClaudeAuth();
|
||||||
|
if (auth.mode === "missing") {
|
||||||
|
console.error("✗ --execute requires either:");
|
||||||
|
console.error(
|
||||||
|
" 1. Claude Code logged in on host (run `claude login` first; ~/.claude/ becomes the auth source — this is the recommended path for Pro/Max subscribers)",
|
||||||
|
);
|
||||||
|
console.error(" 2. ANTHROPIC_API_KEY or OPENAI_API_KEY in env (fallback)");
|
||||||
|
console.error("");
|
||||||
|
console.error(
|
||||||
|
" Override Claude creds path via SANDCASTLE_CLAUDE_CREDS_DIR.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`Auth mode: ${auth.mode === "subscription" ? `subscription (mounting ${auth.hostPath})` : "api-key"}`,
|
||||||
|
);
|
||||||
|
console.log(`Decomposing PRD: ${prdId}`);
|
||||||
|
|
||||||
|
let sandcastleRoot;
|
||||||
|
let dockerProvider;
|
||||||
|
try {
|
||||||
|
sandcastleRoot = await import("@ai-hero/sandcastle");
|
||||||
|
const dockerModule = await import("@ai-hero/sandcastle/sandboxes/docker");
|
||||||
|
dockerProvider = dockerModule.docker;
|
||||||
|
} catch {
|
||||||
|
console.error(
|
||||||
|
"✗ @ai-hero/sandcastle is not installed. Run `pnpm install` first.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dockerOpts = {};
|
||||||
|
const agentOpts = {};
|
||||||
|
if (auth.mode === "subscription") {
|
||||||
|
dockerOpts.mounts = [
|
||||||
|
{
|
||||||
|
hostPath: auth.hostPath,
|
||||||
|
sandboxPath: auth.sandboxPath,
|
||||||
|
readonly: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else if (auth.mode === "api-key") {
|
||||||
|
agentOpts.env = auth.env;
|
||||||
|
}
|
||||||
|
const sandbox = dockerProvider(dockerOpts);
|
||||||
|
const agent = sandcastleRoot.claudeCode("claude-sonnet-4-6", agentOpts);
|
||||||
|
|
||||||
|
const decomposerPrompt = path.join(SANDCASTLE_DIR, "decomposer.prompt.md");
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = await sandcastleRoot.run({
|
||||||
|
agent,
|
||||||
|
sandbox,
|
||||||
|
promptFile: decomposerPrompt,
|
||||||
|
promptArgs: { PRD_FILE_CONTENT: prdText },
|
||||||
|
cwd: REPO_ROOT,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("✗ Decomposer dispatch failed:", e.message);
|
||||||
|
console.error(
|
||||||
|
" See docs/guides/runbook.md → 'Using Sandcastle' for setup.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`Decomposer returned. Branch: ${result.branch}, Commits: ${result.commits.length}`,
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
console.log("=== Suggested next steps ===");
|
||||||
|
console.log(
|
||||||
|
` 1. Inspect the new epic folder under docs/work/<epic-slug>/ on branch ${result.branch}`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` 2. Review the generated stories + tasks; edit anything that should change`,
|
||||||
|
);
|
||||||
|
console.log(` 3. Merge the branch to main`);
|
||||||
|
console.log(
|
||||||
|
` 4. pnpm work rebuild-state && pnpm work next # see the first ready task`,
|
||||||
|
);
|
||||||
|
console.log(` 5. pnpm work dispatch --execute # dispatch it`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- CLI ----
|
||||||
|
|
||||||
|
function usage() {
|
||||||
|
console.error("Usage: pnpm work decompose <prd-id> [--execute]");
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runCli(args, { workRoot }) {
|
||||||
|
const positional = args.filter((a) => !a.startsWith("--"));
|
||||||
|
const prdId = positional[0];
|
||||||
|
if (!prdId) {
|
||||||
|
usage();
|
||||||
|
}
|
||||||
|
|
||||||
|
const prdPath = findPrdPath(workRoot, prdId);
|
||||||
|
if (!prdPath) {
|
||||||
|
console.error(`PRD with id="${prdId}" not found under docs/work/prds/`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let frontmatter;
|
||||||
|
let prdText;
|
||||||
|
try {
|
||||||
|
const result = validatePrdForDecompose(prdPath);
|
||||||
|
frontmatter = result.frontmatter;
|
||||||
|
prdText = result.text;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Cannot decompose ${prdId}: ${err.message}`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.includes("--execute")) {
|
||||||
|
await executeDecompose(prdId, prdPath, prdText);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
printDecomposePlan(prdId, prdPath, frontmatter);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const invokedDirectly = import.meta.url === `file://${process.argv[1]}`;
|
||||||
|
if (invokedDirectly) {
|
||||||
|
runCli(process.argv.slice(2), {
|
||||||
|
repoRoot: REPO_ROOT,
|
||||||
|
workRoot: WORK_ROOT,
|
||||||
|
}).then((code) => process.exit(code));
|
||||||
|
}
|
||||||
162
scripts/work/decompose.test.mjs
Normal file
162
scripts/work/decompose.test.mjs
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import { test, describe } from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import os from "node:os";
|
||||||
|
import { validatePrdForDecompose, runCli } from "./decompose.mjs";
|
||||||
|
|
||||||
|
function writePrd(dir, id, status) {
|
||||||
|
const file = path.join(dir, `${id}.prd.md`);
|
||||||
|
fs.writeFileSync(
|
||||||
|
file,
|
||||||
|
`---
|
||||||
|
id: ${id}
|
||||||
|
title: Test PRD
|
||||||
|
type: prd
|
||||||
|
status: ${status}
|
||||||
|
author: tester
|
||||||
|
created: 2026-05-13
|
||||||
|
---
|
||||||
|
|
||||||
|
body content
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupRepo() {
|
||||||
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "decompose-"));
|
||||||
|
fs.mkdirSync(path.join(tmp, "prds"), { recursive: true });
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("validatePrdForDecompose", () => {
|
||||||
|
test("accepts an approved PRD", () => {
|
||||||
|
const tmp = setupRepo();
|
||||||
|
try {
|
||||||
|
const file = writePrd(path.join(tmp, "prds"), "test-1", "approved");
|
||||||
|
const result = validatePrdForDecompose(file);
|
||||||
|
assert.equal(result.frontmatter.status, "approved");
|
||||||
|
assert.ok(result.text.includes("body content"));
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects draft (must go through human review)", () => {
|
||||||
|
const tmp = setupRepo();
|
||||||
|
try {
|
||||||
|
const file = writePrd(path.join(tmp, "prds"), "test-2", "draft");
|
||||||
|
assert.throws(
|
||||||
|
() => validatePrdForDecompose(file),
|
||||||
|
/status is "draft".*Flip status to "approved"/,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects in-review", () => {
|
||||||
|
const tmp = setupRepo();
|
||||||
|
try {
|
||||||
|
const file = writePrd(path.join(tmp, "prds"), "test-3", "in-review");
|
||||||
|
assert.throws(
|
||||||
|
() => validatePrdForDecompose(file),
|
||||||
|
/status is "in-review"/,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects shipped (epic already exists)", () => {
|
||||||
|
const tmp = setupRepo();
|
||||||
|
try {
|
||||||
|
const file = writePrd(path.join(tmp, "prds"), "test-4", "shipped");
|
||||||
|
assert.throws(() => validatePrdForDecompose(file), /status is "shipped"/);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects unknown status", () => {
|
||||||
|
const tmp = setupRepo();
|
||||||
|
try {
|
||||||
|
const file = writePrd(path.join(tmp, "prds"), "test-5", "cancelled");
|
||||||
|
assert.throws(
|
||||||
|
() => validatePrdForDecompose(file),
|
||||||
|
/Unexpected PRD status "cancelled"/,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects missing file", () => {
|
||||||
|
assert.throws(
|
||||||
|
() => validatePrdForDecompose("/nonexistent/path.prd.md"),
|
||||||
|
/PRD file not found/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("runCli (print mode)", () => {
|
||||||
|
test("returns 1 + writes error when PRD id not found", async () => {
|
||||||
|
const tmp = setupRepo();
|
||||||
|
const errors = [];
|
||||||
|
const origError = console.error;
|
||||||
|
console.error = (m) => errors.push(m);
|
||||||
|
try {
|
||||||
|
const code = await runCli(["nonexistent-id"], {
|
||||||
|
repoRoot: tmp,
|
||||||
|
workRoot: tmp,
|
||||||
|
});
|
||||||
|
assert.equal(code, 1);
|
||||||
|
assert.ok(errors.some((e) => /not found under docs\/work\/prds/.test(e)));
|
||||||
|
} finally {
|
||||||
|
console.error = origError;
|
||||||
|
fs.rmSync(tmp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 1 + writes error when PRD is draft", async () => {
|
||||||
|
const tmp = setupRepo();
|
||||||
|
const errors = [];
|
||||||
|
const origError = console.error;
|
||||||
|
console.error = (m) => errors.push(m);
|
||||||
|
try {
|
||||||
|
writePrd(path.join(tmp, "prds"), "draft-prd", "draft");
|
||||||
|
const code = await runCli(["draft-prd"], {
|
||||||
|
repoRoot: tmp,
|
||||||
|
workRoot: tmp,
|
||||||
|
});
|
||||||
|
assert.equal(code, 1);
|
||||||
|
assert.ok(errors.some((e) => /Cannot decompose/.test(e)));
|
||||||
|
} finally {
|
||||||
|
console.error = origError;
|
||||||
|
fs.rmSync(tmp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("prints plan when PRD is approved + returns 0", async () => {
|
||||||
|
const tmp = setupRepo();
|
||||||
|
const logs = [];
|
||||||
|
const origLog = console.log;
|
||||||
|
console.log = (m) => logs.push(m);
|
||||||
|
try {
|
||||||
|
writePrd(path.join(tmp, "prds"), "approved-prd", "approved");
|
||||||
|
const code = await runCli(["approved-prd"], {
|
||||||
|
repoRoot: tmp,
|
||||||
|
workRoot: tmp,
|
||||||
|
});
|
||||||
|
assert.equal(code, 0);
|
||||||
|
const all = logs.join("\n");
|
||||||
|
assert.match(all, /Decompose plan/);
|
||||||
|
assert.match(all, /approved-prd/);
|
||||||
|
assert.match(all, /eligible/);
|
||||||
|
} finally {
|
||||||
|
console.log = origLog;
|
||||||
|
fs.rmSync(tmp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -275,9 +275,15 @@ async function executeDispatch() {
|
|||||||
console.log("(Automatic state mutation by the orchestrator is v2.)");
|
console.log("(Automatic state mutation by the orchestrator is v2.)");
|
||||||
}
|
}
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
// Only run the CLI when this module is invoked directly. Without this guard,
|
||||||
if (args.includes("--execute")) {
|
// importing any export (e.g. `resolveClaudeAuth` from sibling work scripts)
|
||||||
|
// triggers the CLI as a side effect.
|
||||||
|
const invokedDirectly = import.meta.url === `file://${process.argv[1]}`;
|
||||||
|
if (invokedDirectly) {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
if (args.includes("--execute")) {
|
||||||
executeDispatch();
|
executeDispatch();
|
||||||
} else {
|
} else {
|
||||||
printPlan();
|
printPlan();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user