mandrel 1.91.0 → 1.92.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/.agents/README.md +5 -5
  2. package/.agents/docs/SDLC.md +75 -107
  3. package/.agents/docs/configuration.md +1 -1
  4. package/.agents/docs/quality-gates.md +8 -7
  5. package/.agents/docs/workflows.md +4 -10
  6. package/.agents/instructions.md +2 -2
  7. package/.agents/schemas/agentrc.schema.json +1 -1
  8. package/.agents/schemas/lifecycle/README.md +2 -1
  9. package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
  10. package/.agents/schemas/risk-verdict.schema.json +14 -1
  11. package/.agents/scripts/README.md +0 -46
  12. package/.agents/scripts/analyze-execution.js +76 -1
  13. package/.agents/scripts/check-doc-links.js +3 -0
  14. package/.agents/scripts/check-workflow-cli-lint.js +208 -0
  15. package/.agents/scripts/epic-audit-recheck.js +1 -1
  16. package/.agents/scripts/epic-plan-clarity.js +16 -6
  17. package/.agents/scripts/epic-plan-decompose.js +37 -59
  18. package/.agents/scripts/epic-plan-healthcheck.js +39 -6
  19. package/.agents/scripts/epic-plan-spec.js +41 -168
  20. package/.agents/scripts/epic-reconcile.js +7 -1
  21. package/.agents/scripts/lib/Logger.js +4 -4
  22. package/.agents/scripts/lib/audit-suite/index.js +4 -5
  23. package/.agents/scripts/lib/audit-suite/runner.js +6 -4
  24. package/.agents/scripts/lib/audit-suite/selector.js +2 -3
  25. package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
  26. package/.agents/scripts/lib/command-header.js +20 -0
  27. package/.agents/scripts/lib/config/github.js +1 -3
  28. package/.agents/scripts/lib/config-settings-schema.js +4 -3
  29. package/.agents/scripts/lib/degraded-mode.js +1 -1
  30. package/.agents/scripts/lib/label-constants.js +18 -3
  31. package/.agents/scripts/lib/label-taxonomy.js +14 -3
  32. package/.agents/scripts/lib/mutation/baseline-snapshot.js +7 -6
  33. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +18 -8
  34. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +3 -4
  35. package/.agents/scripts/lib/orchestration/docs-digest.js +2 -2
  36. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +4 -0
  37. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist.js +22 -12
  38. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +6 -1
  39. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +18 -2
  40. package/.agents/scripts/{lifecycle-diff.js → lib/orchestration/lifecycle/ledger-diff.js} +10 -76
  41. package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +1 -1
  42. package/.agents/scripts/lib/orchestration/plan-context.js +510 -0
  43. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +177 -0
  44. package/.agents/scripts/lib/orchestration/plan-metrics.js +445 -0
  45. package/.agents/scripts/lib/orchestration/plan-persist/amend.js +359 -0
  46. package/.agents/scripts/lib/orchestration/plan-persist/delivery-mode.js +127 -0
  47. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +978 -0
  48. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +191 -0
  49. package/.agents/scripts/lib/orchestration/plan-reachability.js +160 -0
  50. package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +3 -3
  51. package/.agents/scripts/lib/orchestration/planning-risk.js +1 -1
  52. package/.agents/scripts/lib/orchestration/spec-freshness.js +1 -1
  53. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +1 -1
  54. package/.agents/scripts/lib/orchestration/ticketing/reads.js +11 -2
  55. package/.agents/scripts/lib/plan-phase-cleanup.js +16 -0
  56. package/.agents/scripts/lib/presentation/manifest-persistence.js +2 -3
  57. package/.agents/scripts/lib/templates/spec-author-prompts.js +74 -0
  58. package/.agents/scripts/plan-context.js +186 -0
  59. package/.agents/scripts/plan-critics.js +227 -0
  60. package/.agents/scripts/plan-persist.js +383 -0
  61. package/.agents/scripts/pr-watch-with-update.js +8 -7
  62. package/.agents/scripts/run-lint.js +10 -11
  63. package/.agents/scripts/story-plan.js +19 -7
  64. package/.agents/scripts/sync-claude-commands.js +10 -5
  65. package/.agents/skills/core/knowledge-transfer/SKILL.md +11 -7
  66. package/.agents/workflows/audit-documentation.md +5 -7
  67. package/.agents/workflows/audit-lighthouse.md +1 -0
  68. package/.agents/workflows/audit-security.md +1 -0
  69. package/.agents/workflows/audit-to-stories.md +16 -5
  70. package/.agents/workflows/git-deliver.md +76 -243
  71. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  72. package/.agents/workflows/helpers/code-review.md +7 -6
  73. package/.agents/workflows/helpers/deliver-epic.md +5 -4
  74. package/.agents/workflows/helpers/plan-epic-reference.md +109 -96
  75. package/.agents/workflows/helpers/plan-epic.md +246 -999
  76. package/.agents/workflows/helpers/plan-story.md +14 -14
  77. package/.agents/workflows/helpers/scope-triage-gate.md +6 -4
  78. package/.agents/workflows/helpers/single-story-deliver.md +5 -4
  79. package/.agents/workflows/helpers/worktree-lifecycle.md +9 -7
  80. package/.agents/workflows/mandrel-update.md +144 -466
  81. package/.agents/workflows/plan.md +73 -143
  82. package/docs/CHANGELOG.md +13 -0
  83. package/lib/cli/registry.js +21 -3
  84. package/package.json +1 -1
  85. package/.agents/schemas/loop-unit.schema.json +0 -70
  86. package/.agents/scripts/assert-branch.js +0 -81
  87. package/.agents/scripts/check-loop-units.js +0 -204
  88. package/.agents/scripts/detect-merges.js +0 -111
  89. package/.agents/scripts/git-pr-quality-gate.js +0 -205
  90. package/.agents/scripts/git-rebase-and-resolve.js +0 -234
  91. package/.agents/scripts/hierarchy-gate.js +0 -192
  92. package/.agents/scripts/hydrate-context.js +0 -179
  93. package/.agents/scripts/lib/loop-units/validate-loop-unit.js +0 -197
  94. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/cli.js +0 -167
  95. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/cli-args.js +0 -69
  96. package/.agents/scripts/lib/orchestration/plan-runner/plan-router.js +0 -86
  97. package/.agents/scripts/loc-delta.js +0 -205
  98. package/.agents/scripts/render-manifest.js +0 -143
  99. package/.agents/scripts/run-audit-suite.js +0 -97
  100. package/.agents/scripts/select-audits.js +0 -155
  101. package/.agents/scripts/update-mutation-baseline.js +0 -189
  102. package/.agents/workflows/explain.md +0 -118
  103. package/.agents/workflows/git-merge-pr.md +0 -377
  104. package/.agents/workflows/helpers/epic-plan-decompose.md +0 -22
  105. package/.agents/workflows/helpers/epic-plan-spec.md +0 -22
  106. package/.agents/workflows/loops/README.md +0 -65
  107. package/.agents/workflows/loops/fix-failing-tests.md +0 -74
  108. package/.agents/workflows/loops/nightly-audit.md +0 -81
  109. package/.agents/workflows/loops/watch-ci.md +0 -68
@@ -1,197 +0,0 @@
1
- /**
2
- * lib/loop-units/validate-loop-unit.js — loop-unit frontmatter validator.
3
- *
4
- * Parses a loop-unit markdown file's YAML frontmatter and AJV-validates
5
- * it against `.agents/schemas/loop-unit.schema.json` (Ajv2020). Mirrors
6
- * the validation pattern established by `lib/spec/loader.js` (Ajv2020 +
7
- * ajv-formats + js-yaml, cached compiled validator, normalised
8
- * `{ path, message }` issues).
9
- *
10
- * A "loop unit" is a markdown file under `.agents/workflows/loops/` whose
11
- * leading `---`-fenced YAML frontmatter block defines a recurring unit of
12
- * work (cadence, goal, conditional verify, round cap, exhaustion policy).
13
- *
14
- * Public surface:
15
- * • `parseFrontmatter(source)` → extracts and YAML-parses the leading
16
- * `---`-fenced block. Returns the parsed object (or `{}` for an empty
17
- * block). Throws `LoopUnitParseError` when the block is absent or the
18
- * YAML does not parse.
19
- * • `validateLoopUnit(filePath, opts?)` → reads the file, parses its
20
- * frontmatter, validates against the schema, and returns
21
- * `{ valid, issues, data }`. `issues` is an array of
22
- * `{ path, message }` (empty when valid). Never throws on a *validation*
23
- * failure — it reports it via `valid: false` — but does throw
24
- * `LoopUnitParseError` for an unreadable file or unparseable
25
- * frontmatter so callers can distinguish "structurally broken file"
26
- * from "schema-invalid unit".
27
- *
28
- * The module makes no GitHub calls and no process mutations; it is pure
29
- * file I/O + schema validation. The `opts` bag accepts `{ schemaPath, fs }`
30
- * so tests can point at a sandbox schema without monkey-patching globals.
31
- */
32
-
33
- import {
34
- existsSync as defaultExistsSync,
35
- readFileSync as defaultReadFileSync,
36
- } from 'node:fs';
37
- import path from 'node:path';
38
- import { fileURLToPath } from 'node:url';
39
- import Ajv2020 from 'ajv/dist/2020.js';
40
- import addFormats from 'ajv-formats';
41
- import yaml from 'js-yaml';
42
-
43
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
44
-
45
- // scripts/lib/loop-units/ → scripts/lib/ → scripts/ → .agents/
46
- const PROJECT_AGENTS_DIR = path.resolve(__dirname, '..', '..', '..');
47
- export const DEFAULT_SCHEMA_PATH = path.join(
48
- PROJECT_AGENTS_DIR,
49
- 'schemas',
50
- 'loop-unit.schema.json',
51
- );
52
-
53
- const defaultFsAdapter = Object.freeze({
54
- existsSync: defaultExistsSync,
55
- readFileSync: defaultReadFileSync,
56
- });
57
-
58
- let cachedValidator = null;
59
- let cachedValidatorKey = null;
60
-
61
- /**
62
- * Compile (and cache) the Ajv2020 validator for the loop-unit schema.
63
- * Cached by absolute schema path so tests can swap to a sandbox schema.
64
- *
65
- * @param {string} schemaPath
66
- * @param {{ readFileSync: typeof defaultReadFileSync }} fs
67
- * @returns {(data: unknown) => boolean}
68
- */
69
- function getValidator(schemaPath, fs) {
70
- if (cachedValidator && cachedValidatorKey === schemaPath) {
71
- return cachedValidator;
72
- }
73
- const ajv = new Ajv2020({ allErrors: true, strict: false });
74
- addFormats(ajv);
75
- const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
76
- cachedValidator = ajv.compile(schema);
77
- cachedValidatorKey = schemaPath;
78
- return cachedValidator;
79
- }
80
-
81
- /**
82
- * Raised when a loop-unit file cannot be read, has no YAML frontmatter
83
- * block, or the frontmatter does not parse as YAML.
84
- */
85
- export class LoopUnitParseError extends Error {
86
- /**
87
- * @param {string} filePath
88
- * @param {string} reason
89
- */
90
- constructor(filePath, reason) {
91
- super(`Loop unit ${filePath} could not be parsed: ${reason}`);
92
- this.name = 'LoopUnitParseError';
93
- this.filePath = filePath;
94
- this.reason = reason;
95
- }
96
- }
97
-
98
- // Leading `---`-fenced YAML block. Tolerates CRLF and a leading BOM. The
99
- // closing fence is a `---` (or `...`) on its own line.
100
- const FRONTMATTER_RE = /^?---\r?\n([\s\S]*?)\r?\n(?:---|\.\.\.)\s*(?:\r?\n|$)/;
101
-
102
- /**
103
- * Extract and YAML-parse the leading `---`-fenced frontmatter block from a
104
- * markdown source string.
105
- *
106
- * @param {string} source raw file contents
107
- * @param {string} [filePath] used only for error messages
108
- * @returns {object} the parsed frontmatter object (`{}` if empty)
109
- * @throws {LoopUnitParseError} when no fence is present or the YAML fails
110
- */
111
- export function parseFrontmatter(source, filePath = '<string>') {
112
- const match = FRONTMATTER_RE.exec(source);
113
- if (!match) {
114
- throw new LoopUnitParseError(
115
- filePath,
116
- 'no YAML frontmatter block (expected a leading "---" fence)',
117
- );
118
- }
119
- let parsed;
120
- try {
121
- parsed = yaml.load(match[1], { filename: filePath });
122
- } catch (err) {
123
- throw new LoopUnitParseError(
124
- filePath,
125
- `frontmatter is not valid YAML: ${err.message}`,
126
- );
127
- }
128
- if (parsed == null) return {};
129
- if (typeof parsed !== 'object' || Array.isArray(parsed)) {
130
- throw new LoopUnitParseError(
131
- filePath,
132
- 'frontmatter must be a YAML mapping',
133
- );
134
- }
135
- return parsed;
136
- }
137
-
138
- /**
139
- * Convert Ajv's error array into a `{ path, message }` shape. For
140
- * `required` errors Ajv leaves the missing property in
141
- * `params.missingProperty` rather than the instance path, so we append it
142
- * so the caller sees `/loop/verify` instead of `/loop` and the message
143
- * names the missing field.
144
- *
145
- * @param {Array<{instancePath:string,message:string,keyword:string,params?:Record<string,unknown>}>} ajvErrors
146
- * @returns {Array<{path:string,message:string}>}
147
- */
148
- function normaliseAjvErrors(ajvErrors) {
149
- return (ajvErrors ?? []).map((err) => {
150
- let p = err.instancePath || '/';
151
- let message = err.message ?? 'validation failed';
152
- if (
153
- err.keyword === 'required' &&
154
- typeof err.params?.missingProperty === 'string'
155
- ) {
156
- const sep = p === '/' ? '' : '/';
157
- p = `${p}${sep}${err.params.missingProperty}`;
158
- message = `must have required property '${err.params.missingProperty}'`;
159
- }
160
- return { path: p, message };
161
- });
162
- }
163
-
164
- /**
165
- * Read, parse, and schema-validate a loop-unit markdown file.
166
- *
167
- * @param {string} filePath
168
- * @param {{ schemaPath?: string, fs?: typeof defaultFsAdapter }} [opts]
169
- * @returns {{ valid: boolean, issues: Array<{path:string,message:string}>, data: object }}
170
- * @throws {LoopUnitParseError} when the file is unreadable or its
171
- * frontmatter is missing/unparseable.
172
- */
173
- export function validateLoopUnit(filePath, opts = {}) {
174
- const fs = opts.fs ?? defaultFsAdapter;
175
- const schemaPath = opts.schemaPath ?? DEFAULT_SCHEMA_PATH;
176
-
177
- if (!fs.existsSync(filePath)) {
178
- throw new LoopUnitParseError(filePath, 'file does not exist');
179
- }
180
-
181
- let raw;
182
- try {
183
- raw = fs.readFileSync(filePath, 'utf8');
184
- } catch (err) {
185
- throw new LoopUnitParseError(filePath, `unreadable: ${err.message}`);
186
- }
187
-
188
- const data = parseFrontmatter(raw, filePath);
189
- const validate = getValidator(schemaPath, fs);
190
- const ok = validate(data);
191
-
192
- return {
193
- valid: ok,
194
- issues: ok ? [] : normaliseAjvErrors(validate.errors),
195
- data,
196
- };
197
- }
@@ -1,167 +0,0 @@
1
- /**
2
- * cli.js — Phase 6 (CLI entry orchestration) of the epic-plan-decompose
3
- * pipeline (Story #2466).
4
- *
5
- * Owns CLI flag parsing, the `--emit-context` JSON envelope path, and
6
- * the tickets-file load → `runDecomposePhase` persist path. The
7
- * partial-failure diagnostics emitter lives in the sibling
8
- * `diagnostics.js` module.
9
- *
10
- * @module lib/orchestration/epic-plan-decompose/phases/cli
11
- */
12
-
13
- import { readFile } from 'node:fs/promises';
14
- import { parseArgs } from 'node:util';
15
-
16
- import { drainPendingCleanupAtBoot } from '../../../../epic-plan-spec.js';
17
- import {
18
- PROJECT_ROOT,
19
- resolveConfig,
20
- validateOrchestrationConfig,
21
- } from '../../../config-resolver.js';
22
- import {
23
- Logger,
24
- routeAllOutputToStderr,
25
- STDERR_LOGGER,
26
- } from '../../../Logger.js';
27
- import { createProvider } from '../../../provider-factory.js';
28
- import { buildDecompositionContext } from './context.js';
29
- import { reportPartialFailure } from './diagnostics.js';
30
- import { runDecomposePhase } from './persist.js';
31
-
32
- const CLI_OPTIONS = {
33
- epic: { type: 'string' },
34
- tickets: { type: 'string' },
35
- force: { type: 'boolean', default: false },
36
- resume: { type: 'boolean', default: false },
37
- 'allow-over-budget': { type: 'boolean', default: false },
38
- 'allow-large-fan-out': { type: 'boolean', default: false },
39
- 'emit-context': { type: 'boolean', default: false },
40
- pretty: { type: 'boolean', default: false },
41
- 'full-context': { type: 'boolean', default: false },
42
- };
43
-
44
- const USAGE =
45
- 'Usage: epic-plan-decompose.js --epic <EpicId> (--emit-context [--pretty] [--full-context] | --tickets <file>) [--force | --resume] [--allow-over-budget] [--allow-large-fan-out]';
46
-
47
- function parseEpicId(rawEpic) {
48
- if (!rawEpic) throw new Error(USAGE);
49
- const epicId = Number.parseInt(rawEpic, 10);
50
- if (Number.isNaN(epicId)) {
51
- throw new Error(`Invalid epic ID: "${rawEpic}" — must be a number.`);
52
- }
53
- return epicId;
54
- }
55
-
56
- function loadResolvedConfig() {
57
- let config;
58
- try {
59
- config = resolveConfig();
60
- // Epic #2880 / F14B: pass the canonical resolved config directly.
61
- // The legacy `config.orchestration` pointer is gone; the validator
62
- // reads `config.github` and `config.delivery.worktreeIsolation`.
63
- validateOrchestrationConfig(config);
64
- } catch (err) {
65
- throw new Error(
66
- `Orchestration config schema validation failed:\n${err.message}`,
67
- );
68
- }
69
- return config;
70
- }
71
-
72
- async function driveDrainPendingCleanup({ config, provider, emitContext }) {
73
- try {
74
- await drainPendingCleanupAtBoot({
75
- repoRoot: PROJECT_ROOT,
76
- // Epic #2880 / F14B: drainPendingCleanupAtBoot now reads opts.config
77
- // (canonical) and resolves the worktree root from
78
- // config.delivery.worktreeIsolation.root.
79
- config,
80
- provider,
81
- logger: emitContext ? STDERR_LOGGER : undefined,
82
- });
83
- } catch (err) {
84
- Logger.warn(`[epic-plan-decompose] worktree sweep skipped: ${err.message}`);
85
- }
86
- }
87
-
88
- async function runEmitContextPath({ epicId, provider, config, values }) {
89
- const ctx = await buildDecompositionContext(epicId, provider, config, {
90
- fullContext: values['full-context'],
91
- });
92
- // Surface the resolved reviewability budget on stderr so the operator
93
- // sees the same value the decomposer prompt embeds. `maxTickets` is a
94
- // framework constant (LIMITS_DEFAULTS.maxTickets), no longer an
95
- // operator-configurable key. Story #2798 — language is "reviewability
96
- // budget", not "prompt cap".
97
- Logger.error(
98
- `[epic-plan-decompose] Resolved maxTickets reviewability budget = ${ctx.maxTickets} (framework constant).`,
99
- );
100
- const json = values.pretty
101
- ? JSON.stringify(ctx, null, 2)
102
- : JSON.stringify(ctx);
103
- process.stdout.write(`${json}\n`);
104
- }
105
-
106
- async function loadTicketsFile(ticketsPath) {
107
- if (!ticketsPath) {
108
- throw new Error(
109
- 'Missing --tickets <file>. (Use --emit-context first to gather authoring context.)',
110
- );
111
- }
112
- const raw = await readFile(ticketsPath, 'utf8');
113
- try {
114
- return JSON.parse(raw);
115
- } catch (err) {
116
- throw new Error(
117
- `Failed to parse tickets file "${ticketsPath}" as JSON: ${err.message}`,
118
- );
119
- }
120
- }
121
-
122
- async function runPersistPath({ epicId, provider, config, values }) {
123
- const tickets = await loadTicketsFile(values.tickets);
124
- let result;
125
- try {
126
- result = await runDecomposePhase(epicId, provider, { tickets }, config, {
127
- force: values.force,
128
- resume: values.resume,
129
- allowOverBudget: values['allow-over-budget'],
130
- allowLargeFanOut: values['allow-large-fan-out'],
131
- });
132
- } catch (err) {
133
- await reportPartialFailure({ epicId, provider, err });
134
- throw err;
135
- }
136
- process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
137
- }
138
-
139
- /* node:coverage ignore next */
140
- export async function main() {
141
- const { values } = parseArgs({ options: CLI_OPTIONS });
142
- const epicId = parseEpicId(values.epic);
143
- if (values.force && values.resume) {
144
- throw new Error('--force and --resume are mutually exclusive.');
145
- }
146
-
147
- const config = loadResolvedConfig();
148
- // Epic #2880 / F14B: createProvider takes the canonical resolved config
149
- // (it reads config.github). The legacy config.orchestration pointer is
150
- // gone; passing it yields undefined and crashes the provider factory's
151
- // hard guard.
152
- const provider = createProvider(config);
153
- const emitContext = values['emit-context'];
154
-
155
- // Story #2278 — in --emit-context mode stdout is reserved for the JSON
156
- // envelope. Flip every Logger sink that could land on stdout to stderr
157
- // before any orchestration code runs.
158
- if (emitContext) routeAllOutputToStderr();
159
-
160
- await driveDrainPendingCleanup({ config, provider, emitContext });
161
-
162
- if (emitContext) {
163
- await runEmitContextPath({ epicId, provider, config, values });
164
- return;
165
- }
166
- await runPersistPath({ epicId, provider, config, values });
167
- }
@@ -1,69 +0,0 @@
1
- /**
2
- * cli-args.js — argv parsing + validation for the `epic-plan-spec` CLI.
3
- *
4
- * Extracted from `epic-plan-spec.js` (refs #3685) so the top-level entry
5
- * stays a thin wire-up that scores at or above the maintainability floor;
6
- * the option schema and the epic-id guards live here.
7
- *
8
- * Story #4324: the separate-ticket persist flags `--techspec` /
9
- * `--acceptance-spec` are retired with the context-ticket classes. The
10
- * authored content now lands as sections of the Epic body; pass it via
11
- * `--tech-spec <file>` and `--acceptance-table <file>`. The old flags fail
12
- * with a usage error naming the removal.
13
- */
14
-
15
- import { parseArgs } from 'node:util';
16
-
17
- /** Retired flags → the replacement guidance surfaced in the usage error. */
18
- const RETIRED_FLAGS = Object.freeze({
19
- '--techspec':
20
- '--techspec was retired by the context-ticket fold (Story #4324): the Tech Spec is no longer a separate context::tech-spec ticket. Pass --tech-spec <file> — the content lands as a managed section of the Epic body.',
21
- '--acceptance-spec':
22
- '--acceptance-spec was retired by the context-ticket fold (Story #4324): the Acceptance Spec is no longer a separate context::acceptance-spec ticket. Pass --acceptance-table <file> — the AC-ID table lands as the ## Acceptance Table section of the Epic body.',
23
- });
24
-
25
- /**
26
- * Parse and validate the `epic-plan-spec` CLI arguments.
27
- *
28
- * @param {string[]} [argv] Defaults to `process.argv.slice(2)`.
29
- * @returns {{ values: Record<string, unknown>, epicId: number }}
30
- * @throws {Error} when `--epic` is missing or not a number, or when a
31
- * retired separate-ticket persist flag is supplied.
32
- */
33
- export function parseEpicPlanSpecArgs(argv = process.argv.slice(2)) {
34
- for (const arg of argv) {
35
- const flag = arg.split('=')[0];
36
- if (RETIRED_FLAGS[flag]) {
37
- throw new Error(`[epic-plan-spec] ${RETIRED_FLAGS[flag]}`);
38
- }
39
- }
40
-
41
- const { values } = parseArgs({
42
- args: argv,
43
- options: {
44
- epic: { type: 'string' },
45
- 'tech-spec': { type: 'string' },
46
- 'acceptance-table': { type: 'string' },
47
- 'risk-verdict': { type: 'string' },
48
- force: { type: 'boolean', default: false },
49
- 'force-review': { type: 'boolean', default: false },
50
- steal: { type: 'boolean', default: false },
51
- 'emit-context': { type: 'boolean', default: false },
52
- pretty: { type: 'boolean', default: false },
53
- 'full-context': { type: 'boolean', default: false },
54
- },
55
- });
56
-
57
- if (!values.epic) {
58
- throw new Error(
59
- 'Usage: epic-plan-spec.js --epic <EpicId> (--emit-context [--pretty] [--full-context] | --tech-spec <file> --risk-verdict <file> [--acceptance-table <file>]) [--force]',
60
- );
61
- }
62
-
63
- const epicId = Number.parseInt(values.epic, 10);
64
- if (Number.isNaN(epicId)) {
65
- throw new Error(`Invalid epic ID: "${values.epic}" — must be a number.`);
66
- }
67
-
68
- return { values, epicId };
69
- }
@@ -1,86 +0,0 @@
1
- /**
2
- * plan-router — given an Epic's current labels, decide which plan-phase CLI
3
- * should run next.
4
- *
5
- * Used by the local `/plan` wrapper (chains spec → decompose).
6
- *
7
- * The router is intentionally stateless. Callers feed the current label set
8
- * (a string array, usually from `provider.getEpic(id).labels`) and receive a
9
- * `{ phase, script, command }` descriptor; no I/O is performed.
10
- */
11
-
12
- import { AGENT_LABELS } from '../../label-constants.js';
13
-
14
- export const PLAN_PHASE_NAMES = Object.freeze({
15
- SPEC: 'spec',
16
- DECOMPOSE: 'decompose',
17
- });
18
-
19
- /**
20
- * Canonical descriptor for each planning phase. `script` is the repo-relative
21
- * path used by the local wrapper; `command` is the slash-command invocation
22
- * operators fire.
23
- *
24
- * Spec and Decompose are served by the unified `/plan` wrapper with a
25
- * `--phase` flag — the phase workflows themselves live at
26
- * `.agents/workflows/helpers/epic-plan-{spec,decompose}.md` and are not
27
- * directly invokable slash commands.
28
- *
29
- * Exported as `PLAN_PHASE_DESCRIPTORS` to make the CLI-routing role explicit
30
- * (it describes which wrapper script/slash-command serves each planning phase).
31
- */
32
- export const PLAN_PHASE_DESCRIPTORS = Object.freeze({
33
- [PLAN_PHASE_NAMES.SPEC]: {
34
- phase: PLAN_PHASE_NAMES.SPEC,
35
- script: '.agents/scripts/epic-plan-spec.js',
36
- command: '/plan --phase spec',
37
- parkingLabel: AGENT_LABELS.REVIEW_SPEC,
38
- },
39
- [PLAN_PHASE_NAMES.DECOMPOSE]: {
40
- phase: PLAN_PHASE_NAMES.DECOMPOSE,
41
- script: '.agents/scripts/epic-plan-decompose.js',
42
- command: '/plan --phase decompose',
43
- parkingLabel: AGENT_LABELS.READY,
44
- },
45
- });
46
-
47
- /**
48
- * Given the Epic's current labels, pick the next plan phase to run in the
49
- * local `/plan` wrapper.
50
- *
51
- * Precedence:
52
- * 1. If the Epic already carries `agent::ready`, there is nothing left to
53
- * do — return `null` (the wrapper surfaces a no-op message).
54
- * 2. If the Epic carries `agent::review-spec`, decomposition is the next
55
- * step (the operator has finished review).
56
- * 3. Otherwise (fresh Epic), start with the spec phase.
57
- *
58
- * @param {string[]} labels Current labels on the Epic.
59
- * @returns {object|null} Phase descriptor or null when no more work remains.
60
- */
61
- export function nextPhaseForEpic(labels = []) {
62
- const set = new Set(labels);
63
- if (set.has(AGENT_LABELS.READY)) return null;
64
- if (set.has(AGENT_LABELS.REVIEW_SPEC)) {
65
- return PLAN_PHASE_DESCRIPTORS[PLAN_PHASE_NAMES.DECOMPOSE];
66
- }
67
- return PLAN_PHASE_DESCRIPTORS[PLAN_PHASE_NAMES.SPEC];
68
- }
69
-
70
- /**
71
- * For a given current phase, return the next phase the local wrapper should
72
- * advance to. Used to chain spec → decompose after operator confirmation.
73
- *
74
- * @param {string} currentPhase One of `PLAN_PHASE_NAMES`.
75
- * @returns {object|null}
76
- */
77
- export function advancePhase(currentPhase) {
78
- switch (currentPhase) {
79
- case PLAN_PHASE_NAMES.SPEC:
80
- return PLAN_PHASE_DESCRIPTORS[PLAN_PHASE_NAMES.DECOMPOSE];
81
- case PLAN_PHASE_NAMES.DECOMPOSE:
82
- return null;
83
- default:
84
- return null;
85
- }
86
- }