mandrel 1.91.0 → 1.93.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.
- package/.agents/README.md +5 -5
- package/.agents/docs/SDLC.md +75 -107
- package/.agents/docs/configuration.md +1 -1
- package/.agents/docs/quality-gates.md +8 -7
- package/.agents/docs/workflows.md +4 -10
- package/.agents/instructions.md +2 -2
- package/.agents/schemas/agentrc.schema.json +1 -1
- package/.agents/schemas/lifecycle/README.md +2 -1
- package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
- package/.agents/schemas/risk-verdict.schema.json +14 -1
- package/.agents/scripts/README.md +0 -46
- package/.agents/scripts/analyze-execution.js +76 -1
- package/.agents/scripts/check-doc-links.js +3 -0
- package/.agents/scripts/check-workflow-cli-lint.js +208 -0
- package/.agents/scripts/epic-audit-recheck.js +1 -1
- package/.agents/scripts/epic-plan-clarity.js +16 -6
- package/.agents/scripts/epic-plan-decompose.js +37 -59
- package/.agents/scripts/epic-plan-healthcheck.js +39 -6
- package/.agents/scripts/epic-plan-spec.js +41 -168
- package/.agents/scripts/epic-reconcile.js +7 -1
- package/.agents/scripts/lib/Logger.js +4 -4
- package/.agents/scripts/lib/audit-suite/index.js +4 -5
- package/.agents/scripts/lib/audit-suite/runner.js +6 -4
- package/.agents/scripts/lib/audit-suite/selector.js +2 -3
- package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
- package/.agents/scripts/lib/command-header.js +20 -0
- package/.agents/scripts/lib/config/github.js +1 -3
- package/.agents/scripts/lib/config-settings-schema.js +4 -3
- package/.agents/scripts/lib/degraded-mode.js +1 -1
- package/.agents/scripts/lib/label-constants.js +18 -3
- package/.agents/scripts/lib/label-taxonomy.js +14 -3
- package/.agents/scripts/lib/mutation/baseline-snapshot.js +7 -6
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +18 -8
- package/.agents/scripts/lib/orchestration/context-hydration-engine.js +3 -4
- package/.agents/scripts/lib/orchestration/docs-digest.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +4 -0
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist.js +22 -12
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +6 -1
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +18 -2
- package/.agents/scripts/lib/orchestration/file-assumptions.js +68 -7
- package/.agents/scripts/{lifecycle-diff.js → lib/orchestration/lifecycle/ledger-diff.js} +10 -76
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +1 -1
- package/.agents/scripts/lib/orchestration/plan-context.js +696 -0
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +177 -0
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +99 -0
- package/.agents/scripts/lib/orchestration/plan-metrics.js +445 -0
- package/.agents/scripts/lib/orchestration/plan-persist/amend.js +359 -0
- package/.agents/scripts/lib/orchestration/plan-persist/delivery-mode.js +127 -0
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +1015 -0
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +206 -0
- package/.agents/scripts/lib/orchestration/plan-reachability.js +160 -0
- package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +3 -3
- package/.agents/scripts/lib/orchestration/planning-risk.js +1 -1
- package/.agents/scripts/lib/orchestration/spec-freshness.js +1 -1
- package/.agents/scripts/lib/orchestration/ticket-validator.js +18 -1
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +1 -1
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +11 -2
- package/.agents/scripts/lib/plan-phase-cleanup.js +16 -0
- package/.agents/scripts/lib/presentation/manifest-persistence.js +2 -3
- package/.agents/scripts/lib/templates/spec-author-prompts.js +74 -0
- package/.agents/scripts/plan-context.js +204 -0
- package/.agents/scripts/plan-critics.js +199 -0
- package/.agents/scripts/plan-persist.js +383 -0
- package/.agents/scripts/pr-watch-with-update.js +8 -7
- package/.agents/scripts/run-lint.js +10 -11
- package/.agents/scripts/story-plan.js +19 -7
- package/.agents/scripts/sync-claude-commands.js +10 -5
- package/.agents/skills/core/knowledge-transfer/SKILL.md +11 -7
- package/.agents/workflows/audit-documentation.md +5 -7
- package/.agents/workflows/audit-lighthouse.md +1 -0
- package/.agents/workflows/audit-security.md +1 -0
- package/.agents/workflows/audit-to-stories.md +16 -5
- package/.agents/workflows/git-deliver.md +76 -243
- package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
- package/.agents/workflows/helpers/code-review.md +7 -6
- package/.agents/workflows/helpers/deliver-epic.md +5 -4
- package/.agents/workflows/helpers/plan-epic-reference.md +120 -96
- package/.agents/workflows/helpers/plan-epic.md +307 -994
- package/.agents/workflows/helpers/plan-story.md +14 -14
- package/.agents/workflows/helpers/scope-triage-gate.md +15 -4
- package/.agents/workflows/helpers/single-story-deliver.md +5 -4
- package/.agents/workflows/helpers/worktree-lifecycle.md +9 -7
- package/.agents/workflows/mandrel-update.md +144 -466
- package/.agents/workflows/plan.md +87 -145
- package/docs/CHANGELOG.md +20 -0
- package/lib/cli/registry.js +21 -3
- package/package.json +1 -1
- package/.agents/schemas/loop-unit.schema.json +0 -70
- package/.agents/scripts/assert-branch.js +0 -81
- package/.agents/scripts/check-loop-units.js +0 -204
- package/.agents/scripts/detect-merges.js +0 -111
- package/.agents/scripts/git-pr-quality-gate.js +0 -205
- package/.agents/scripts/git-rebase-and-resolve.js +0 -234
- package/.agents/scripts/hierarchy-gate.js +0 -192
- package/.agents/scripts/hydrate-context.js +0 -179
- package/.agents/scripts/lib/loop-units/validate-loop-unit.js +0 -197
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/cli.js +0 -167
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/cli-args.js +0 -69
- package/.agents/scripts/lib/orchestration/plan-runner/plan-router.js +0 -86
- package/.agents/scripts/loc-delta.js +0 -205
- package/.agents/scripts/render-manifest.js +0 -143
- package/.agents/scripts/run-audit-suite.js +0 -97
- package/.agents/scripts/select-audits.js +0 -155
- package/.agents/scripts/update-mutation-baseline.js +0 -189
- package/.agents/workflows/explain.md +0 -118
- package/.agents/workflows/git-merge-pr.md +0 -377
- package/.agents/workflows/helpers/epic-plan-decompose.md +0 -22
- package/.agents/workflows/helpers/epic-plan-spec.md +0 -22
- package/.agents/workflows/loops/README.md +0 -65
- package/.agents/workflows/loops/fix-failing-tests.md +0 -74
- package/.agents/workflows/loops/nightly-audit.md +0 -81
- package/.agents/workflows/loops/watch-ci.md +0 -68
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* node:coverage ignore file */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* plan-context.js — step 1 of the collapsed `/plan` pipeline (Epic #4474,
|
|
6
|
+
* M3 PR2): the single emit-context CLI.
|
|
7
|
+
*
|
|
8
|
+
* Folds the retired 12-phase pipeline's two emit-context halves plus
|
|
9
|
+
* the three previously-no-CLI library calls
|
|
10
|
+
* (`findSimilarOpenEpics`, clarity scoring, re-plan detection) into ONE
|
|
11
|
+
* stdout-pure JSON envelope. The PR7 cutover retired the delegate CLIs —
|
|
12
|
+
* this is the only emit-context surface.
|
|
13
|
+
*
|
|
14
|
+
* Three entry forms (exactly one is required):
|
|
15
|
+
*
|
|
16
|
+
* --epic <id> Existing-Epic mode. Envelope carries `epic`,
|
|
17
|
+
* `clarity` (Epic Clarity Gate rubric), `replan`
|
|
18
|
+
* (already-planned signals) and `planState`.
|
|
19
|
+
*
|
|
20
|
+
* --one-pager <path> Ideation mode — the Epic does not exist yet
|
|
21
|
+
* (creation moves to the persist half). Envelope
|
|
22
|
+
* carries `onePager` and `duplicates[]` (cross-Epic
|
|
23
|
+
* dup search). No clarity score: the ideation path
|
|
24
|
+
* is definitionally clear.
|
|
25
|
+
*
|
|
26
|
+
* --seed "<text>" Headless ideation entry (#4496 fix 1) — neither
|
|
27
|
+
* the Epic nor the one-pager exists yet. The dup
|
|
28
|
+
* search runs off the raw seed text, and the
|
|
29
|
+
* envelope additively carries `seed`, `scopeTriage`
|
|
30
|
+
* (the scope-triage rubric applied CLI-side — fix 6)
|
|
31
|
+
* and `onePagerSpec`, so the one-pager sections are
|
|
32
|
+
* authored in the same batched write as the spec
|
|
33
|
+
* artifacts.
|
|
34
|
+
*
|
|
35
|
+
* Flags:
|
|
36
|
+
* --pretty Pretty-print the JSON envelope.
|
|
37
|
+
* --full-context Bypass the planning-context budget (unbounded body).
|
|
38
|
+
*
|
|
39
|
+
* stdout is reserved for the JSON envelope (Story #2278 discipline):
|
|
40
|
+
* `routeAllOutputToStderr()` runs before any pipeline code so a captured
|
|
41
|
+
* file is unconditionally parseable by `JSON.parse`.
|
|
42
|
+
*
|
|
43
|
+
* Exit codes:
|
|
44
|
+
* 0 — envelope emitted.
|
|
45
|
+
* 1 — fatal error (see stderr).
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
// Fail-fast if the framework's runtime deps are not installed — must be the
|
|
49
|
+
// first import so the check runs before any third-party-importing sibling
|
|
50
|
+
// module is evaluated (Story #3432).
|
|
51
|
+
import './lib/runtime-deps/ensure-installed.js';
|
|
52
|
+
import { parseArgs } from 'node:util';
|
|
53
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
54
|
+
import {
|
|
55
|
+
resolveConfig,
|
|
56
|
+
validateOrchestrationConfig,
|
|
57
|
+
} from './lib/config-resolver.js';
|
|
58
|
+
import { routeAllOutputToStderr } from './lib/Logger.js';
|
|
59
|
+
import { buildPlanContext } from './lib/orchestration/plan-context.js';
|
|
60
|
+
import { recordPlanInvocation } from './lib/orchestration/plan-metrics.js';
|
|
61
|
+
import { createProvider } from './lib/provider-factory.js';
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build the envelope and write it to `stdout` as a single JSON line
|
|
65
|
+
* (or pretty-printed with --pretty). Exported for tests: the stdout-purity
|
|
66
|
+
* test injects a fake provider and a capture stream and asserts the
|
|
67
|
+
* captured output is exactly one `JSON.parse`-able payload.
|
|
68
|
+
*
|
|
69
|
+
* @param {{
|
|
70
|
+
* mode: 'epic'|'one-pager'|'seed',
|
|
71
|
+
* epicId?: number,
|
|
72
|
+
* onePagerPath?: string,
|
|
73
|
+
* onePagerContent?: string,
|
|
74
|
+
* seedText?: string,
|
|
75
|
+
* provider: object,
|
|
76
|
+
* config: object,
|
|
77
|
+
* settings: object,
|
|
78
|
+
* fullContext?: boolean,
|
|
79
|
+
* pretty?: boolean,
|
|
80
|
+
* cwd?: string,
|
|
81
|
+
* stdout?: { write: (chunk: string) => void },
|
|
82
|
+
* }} args
|
|
83
|
+
* @returns {Promise<object>} the emitted envelope.
|
|
84
|
+
*/
|
|
85
|
+
export async function emitPlanContext({
|
|
86
|
+
mode,
|
|
87
|
+
epicId,
|
|
88
|
+
onePagerPath,
|
|
89
|
+
onePagerContent,
|
|
90
|
+
seedText,
|
|
91
|
+
provider,
|
|
92
|
+
config,
|
|
93
|
+
settings,
|
|
94
|
+
fullContext = false,
|
|
95
|
+
pretty = false,
|
|
96
|
+
cwd,
|
|
97
|
+
stdout = process.stdout,
|
|
98
|
+
}) {
|
|
99
|
+
const envelope = await buildPlanContext({
|
|
100
|
+
mode,
|
|
101
|
+
epicId,
|
|
102
|
+
onePagerPath,
|
|
103
|
+
onePagerContent,
|
|
104
|
+
seedText,
|
|
105
|
+
provider,
|
|
106
|
+
config,
|
|
107
|
+
settings,
|
|
108
|
+
fullContext,
|
|
109
|
+
cwd,
|
|
110
|
+
});
|
|
111
|
+
const json = pretty
|
|
112
|
+
? JSON.stringify(envelope, null, 2)
|
|
113
|
+
: JSON.stringify(envelope);
|
|
114
|
+
stdout.write(`${json}\n`);
|
|
115
|
+
return envelope;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function main() {
|
|
119
|
+
const { values } = parseArgs({
|
|
120
|
+
options: {
|
|
121
|
+
epic: { type: 'string' },
|
|
122
|
+
'one-pager': { type: 'string' },
|
|
123
|
+
seed: { type: 'string' },
|
|
124
|
+
pretty: { type: 'boolean', default: false },
|
|
125
|
+
'full-context': { type: 'boolean', default: false },
|
|
126
|
+
},
|
|
127
|
+
strict: true,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const hasEpic = typeof values.epic === 'string' && values.epic.length > 0;
|
|
131
|
+
const hasOnePager =
|
|
132
|
+
typeof values['one-pager'] === 'string' && values['one-pager'].length > 0;
|
|
133
|
+
const hasSeed = typeof values.seed === 'string' && values.seed.length > 0;
|
|
134
|
+
const entryForms = [hasEpic, hasOnePager, hasSeed].filter(Boolean).length;
|
|
135
|
+
if (entryForms !== 1) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
'Pass exactly one of --epic <id>, --one-pager <path> or --seed "<text>". ' +
|
|
138
|
+
'(--epic: existing-Epic mode; --one-pager: ideation mode; ' +
|
|
139
|
+
'--seed: headless ideation entry.)',
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const mode = hasEpic ? 'epic' : hasOnePager ? 'one-pager' : 'seed';
|
|
143
|
+
|
|
144
|
+
let epicId;
|
|
145
|
+
if (hasEpic) {
|
|
146
|
+
epicId = Number.parseInt(values.epic, 10);
|
|
147
|
+
if (!Number.isInteger(epicId)) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`--epic must be a numeric issue id (got "${values.epic}").`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// stdout is reserved for the JSON envelope: flip every Logger sink that
|
|
155
|
+
// could land on stdout to stderr BEFORE any pipeline code runs
|
|
156
|
+
// (Story #2278 — the same stdout-purity guarantee the retired pipeline
|
|
157
|
+
// gives; this CLI is emit-only so the flip is unconditional).
|
|
158
|
+
routeAllOutputToStderr();
|
|
159
|
+
|
|
160
|
+
let config;
|
|
161
|
+
let settings;
|
|
162
|
+
try {
|
|
163
|
+
config = resolveConfig();
|
|
164
|
+
// `settings` retains the legacy bag shape `buildAuthoringContext` and
|
|
165
|
+
// friends consume: `{ baseBranch, paths, planning, docsContextFiles }`.
|
|
166
|
+
settings = {
|
|
167
|
+
baseBranch: config.project?.baseBranch,
|
|
168
|
+
paths: config.project?.paths,
|
|
169
|
+
planning: config.planning,
|
|
170
|
+
docsContextFiles: config.project?.docsContextFiles,
|
|
171
|
+
};
|
|
172
|
+
validateOrchestrationConfig(config);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
throw new Error(`Config schema validation failed:\n${err.message}`);
|
|
175
|
+
}
|
|
176
|
+
const provider = createProvider(config);
|
|
177
|
+
|
|
178
|
+
// Plan-metrics ledger (#4474 PR1): stamp entry/exit + mode so the folded
|
|
179
|
+
// emit surface is measured against the 12-phase baseline. One-pager and
|
|
180
|
+
// seed modes have no Epic yet, so the record routes to the standalone
|
|
181
|
+
// stream (epicId null) exactly like `story-plan.js`.
|
|
182
|
+
await recordPlanInvocation(
|
|
183
|
+
{
|
|
184
|
+
cli: 'plan-context',
|
|
185
|
+
mode,
|
|
186
|
+
epicId: hasEpic ? epicId : null,
|
|
187
|
+
config,
|
|
188
|
+
},
|
|
189
|
+
() =>
|
|
190
|
+
emitPlanContext({
|
|
191
|
+
mode,
|
|
192
|
+
epicId,
|
|
193
|
+
onePagerPath: hasOnePager ? values['one-pager'] : undefined,
|
|
194
|
+
seedText: hasSeed ? values.seed : undefined,
|
|
195
|
+
provider,
|
|
196
|
+
config,
|
|
197
|
+
settings,
|
|
198
|
+
fullContext: values['full-context'],
|
|
199
|
+
pretty: values.pretty,
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
runAsCli(import.meta.url, main, { source: 'plan-context' });
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* plan-critics.js — deterministic dispatch gate for the conditional
|
|
5
|
+
* author-step critics of the collapsed /plan flow (Epic #4474 PR6,
|
|
6
|
+
* design §4).
|
|
7
|
+
*
|
|
8
|
+
* **Thin shim (#4496 fix 6).** The evaluation itself now lives in
|
|
9
|
+
* `lib/orchestration/plan-critics-evaluate.js` and is folded into
|
|
10
|
+
* `plan-persist.js` as a pre-write phase, so the headless (`--yes`) path
|
|
11
|
+
* never pays a standalone CLI turn for the dispatch decision. This CLI
|
|
12
|
+
* survives one release for the attended pre-gate evaluation (the verdict
|
|
13
|
+
* folds into gate #2's view before the persist runs) and for any external
|
|
14
|
+
* scripting; it delegates to the shared module and keeps its exact output
|
|
15
|
+
* contract.
|
|
16
|
+
*
|
|
17
|
+
* Runs between authoring and gate #2, entirely git-local (zero GitHub
|
|
18
|
+
* calls): reads the authored artifacts, evaluates the risk/size dispatch
|
|
19
|
+
* conditions for the consolidation (8.3) and pre-mortem (8.5) critics via
|
|
20
|
+
* the shared evaluator, and emits one JSON verdict on stdout. The workflow
|
|
21
|
+
* dispatches a fresh-context sub-agent ONLY for critics with
|
|
22
|
+
* `dispatch: true`; every skip decision is appended to the plan-metrics
|
|
23
|
+
* ledger (`kind: "critic-skip"`, with reasons) so under-firing is
|
|
24
|
+
* auditable — the persist validators remain unchanged hard gates
|
|
25
|
+
* regardless of what this gate decides.
|
|
26
|
+
*
|
|
27
|
+
* Conditions (design §4 / §6 PR6):
|
|
28
|
+
* - Consolidation: the existing deterministic precondition
|
|
29
|
+
* (`evaluateConsolidationPrecondition`) says dispatch AND (the draft
|
|
30
|
+
* has > 5 stories OR a confirmed divergence from the Tech Spec's
|
|
31
|
+
* Delivery Slicing table). Skipped outright in the single-delivery
|
|
32
|
+
* shape (no tickets exist to consolidate).
|
|
33
|
+
* - Pre-mortem: risk verdict overall level is high, OR ticket count is
|
|
34
|
+
* at least half `maxTickets`, OR any `planning.riskHeuristics` phrase
|
|
35
|
+
* matches the plan text (case-insensitive substring over tech spec +
|
|
36
|
+
* tickets + risk summary).
|
|
37
|
+
*
|
|
38
|
+
* Modes:
|
|
39
|
+
* --epic <id> Artifact paths default to the per-Epic temp tree
|
|
40
|
+
* (`temp/epic-<id>/techspec.md`, `risk-verdict.json`,
|
|
41
|
+
* `tickets.json`); skip records land on the
|
|
42
|
+
* per-Epic plan-metrics ledger.
|
|
43
|
+
* explicit paths Ideation mode: pass --tech-spec/--risk-verdict
|
|
44
|
+
* (and --tickets when fan-out) explicitly; skip
|
|
45
|
+
* records land on the standalone ledger stream.
|
|
46
|
+
*
|
|
47
|
+
* Output (stdout-pure JSON):
|
|
48
|
+
* { epicId, consolidation: { critic, dispatch, reasons },
|
|
49
|
+
* premortem: { critic, dispatch, reasons } }
|
|
50
|
+
*
|
|
51
|
+
* Exit codes: 0 — verdict emitted (dispatch decisions are data, not
|
|
52
|
+
* failures); 1 — fatal error (unreadable/invalid artifacts, bad args).
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
// Fail-fast if the framework's runtime deps are not installed — must be the
|
|
56
|
+
// first import so the check runs before any third-party-importing sibling
|
|
57
|
+
// module is evaluated (Story #3432).
|
|
58
|
+
import './lib/runtime-deps/ensure-installed.js';
|
|
59
|
+
import { readFile } from 'node:fs/promises';
|
|
60
|
+
import { parseArgs } from 'node:util';
|
|
61
|
+
|
|
62
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
63
|
+
import { epicArtifactPath } from './lib/config/temp-paths.js';
|
|
64
|
+
import {
|
|
65
|
+
resolveConfig,
|
|
66
|
+
validateOrchestrationConfig,
|
|
67
|
+
} from './lib/config-resolver.js';
|
|
68
|
+
import { routeAllOutputToStderr } from './lib/Logger.js';
|
|
69
|
+
import { loadRiskVerdict } from './lib/orchestration/epic-plan-spec/phases/risk-verdict.js';
|
|
70
|
+
import { evaluatePlanCritics } from './lib/orchestration/plan-critics-evaluate.js';
|
|
71
|
+
import {
|
|
72
|
+
appendCriticSkip,
|
|
73
|
+
recordPlanInvocation,
|
|
74
|
+
} from './lib/orchestration/plan-metrics.js';
|
|
75
|
+
|
|
76
|
+
const USAGE =
|
|
77
|
+
'Usage: plan-critics.js (--epic <EpicId> | --tech-spec <file> ' +
|
|
78
|
+
'--risk-verdict <file> [--tickets <file>]) [--pretty]';
|
|
79
|
+
|
|
80
|
+
async function readOptional(filePath, { required }) {
|
|
81
|
+
try {
|
|
82
|
+
return await readFile(filePath, 'utf8');
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (!required && err?.code === 'ENOENT') return null;
|
|
85
|
+
throw new Error(`Cannot read ${filePath}: ${err.message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function main() {
|
|
90
|
+
const { values } = parseArgs({
|
|
91
|
+
options: {
|
|
92
|
+
epic: { type: 'string' },
|
|
93
|
+
'tech-spec': { type: 'string' },
|
|
94
|
+
'risk-verdict': { type: 'string' },
|
|
95
|
+
tickets: { type: 'string' },
|
|
96
|
+
pretty: { type: 'boolean', default: false },
|
|
97
|
+
},
|
|
98
|
+
strict: true,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
let epicId = null;
|
|
102
|
+
if (values.epic !== undefined) {
|
|
103
|
+
epicId = Number.parseInt(values.epic, 10);
|
|
104
|
+
if (!Number.isInteger(epicId)) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`--epic must be a numeric issue id (got "${values.epic}").\n${USAGE}`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// stdout is reserved for the JSON verdict — flip every Logger sink to
|
|
112
|
+
// stderr before any pipeline code runs (same guarantee plan-context.js
|
|
113
|
+
// gives its envelope).
|
|
114
|
+
routeAllOutputToStderr();
|
|
115
|
+
|
|
116
|
+
let config;
|
|
117
|
+
try {
|
|
118
|
+
config = resolveConfig();
|
|
119
|
+
validateOrchestrationConfig(config);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
throw new Error(`Config schema validation failed:\n${err.message}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const fallback = (basename) =>
|
|
125
|
+
epicId === null ? undefined : epicArtifactPath(epicId, basename, config);
|
|
126
|
+
const techSpecPath = values['tech-spec'] ?? fallback('techspec.md');
|
|
127
|
+
const riskVerdictPath =
|
|
128
|
+
values['risk-verdict'] ?? fallback('risk-verdict.json');
|
|
129
|
+
const ticketsPath = values.tickets ?? fallback('tickets.json');
|
|
130
|
+
if (!techSpecPath || !riskVerdictPath) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Missing artifact path(s): without --epic, explicit --tech-spec and --risk-verdict are required.\n${USAGE}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const verdict = await recordPlanInvocation(
|
|
137
|
+
{ cli: 'plan-critics', mode: 'evaluate', epicId, config },
|
|
138
|
+
async () => {
|
|
139
|
+
const techSpecContent = await readOptional(techSpecPath, {
|
|
140
|
+
required: true,
|
|
141
|
+
});
|
|
142
|
+
const riskVerdict = loadRiskVerdict(riskVerdictPath);
|
|
143
|
+
// Tickets are shape-dependent: a single-delivery plan authors none.
|
|
144
|
+
// Required only when passed explicitly.
|
|
145
|
+
const ticketsRaw = ticketsPath
|
|
146
|
+
? await readOptional(ticketsPath, {
|
|
147
|
+
required: values.tickets !== undefined,
|
|
148
|
+
})
|
|
149
|
+
: null;
|
|
150
|
+
let tickets = null;
|
|
151
|
+
if (ticketsRaw !== null) {
|
|
152
|
+
try {
|
|
153
|
+
tickets = JSON.parse(ticketsRaw);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`Failed to parse tickets file "${ticketsPath}" as JSON: ${err.message}`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
if (!Array.isArray(tickets)) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`Tickets file "${ticketsPath}" must contain a JSON array.`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const { consolidation, premortem } = evaluatePlanCritics({
|
|
167
|
+
techSpecContent,
|
|
168
|
+
riskVerdict,
|
|
169
|
+
tickets,
|
|
170
|
+
config,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Skip-audit trail (#4474 PR6): every non-dispatch is a ledger
|
|
174
|
+
// record. Best-effort — a failed append never fails the gate.
|
|
175
|
+
for (const decision of [consolidation, premortem]) {
|
|
176
|
+
if (!decision.dispatch) {
|
|
177
|
+
await appendCriticSkip(
|
|
178
|
+
{
|
|
179
|
+
critic: decision.critic,
|
|
180
|
+
reasons: decision.reasons,
|
|
181
|
+
cli: 'plan-critics',
|
|
182
|
+
epicId,
|
|
183
|
+
},
|
|
184
|
+
config,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { epicId, consolidation, premortem };
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const json = values.pretty
|
|
194
|
+
? JSON.stringify(verdict, null, 2)
|
|
195
|
+
: JSON.stringify(verdict);
|
|
196
|
+
process.stdout.write(`${json}\n`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
runAsCli(import.meta.url, main, { source: 'plan-critics' });
|