mandrel 1.90.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.
- 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-deliver-preflight.js +37 -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/{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/lifecycle/listeners/finalizer.js +32 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +510 -0
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +177 -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 +978 -0
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +191 -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/remote-verifier.js +165 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +10 -0
- package/.agents/scripts/lib/orchestration/spec-freshness.js +1 -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 +186 -0
- package/.agents/scripts/plan-critics.js +227 -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/single-story-init.js +22 -0
- 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/deliver.md +8 -0
- 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 +16 -4
- package/.agents/workflows/helpers/plan-epic-reference.md +109 -96
- package/.agents/workflows/helpers/plan-epic.md +246 -999
- package/.agents/workflows/helpers/plan-story.md +14 -14
- package/.agents/workflows/helpers/scope-triage-gate.md +6 -4
- package/.agents/workflows/helpers/single-story-deliver.md +13 -4
- package/.agents/workflows/helpers/worktree-lifecycle.md +9 -7
- package/.agents/workflows/mandrel-update.md +144 -466
- package/.agents/workflows/plan.md +73 -143
- 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,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spec-author-prompts.js — single source of the Tech Spec and Acceptance
|
|
3
|
+
* Spec authoring system prompts (Epic #4474, M3 PR2).
|
|
4
|
+
*
|
|
5
|
+
* **Single source of the prompt bodies.** These two prompts were lifted
|
|
6
|
+
* VERBATIM from the "(authoritative)" fenced blocks in
|
|
7
|
+
* `.agents/skills/core/epic-plan-spec-author/SKILL.md` (§ "Tech Spec system
|
|
8
|
+
* prompt" and § "Acceptance Spec system prompt") as the M3 side of the
|
|
9
|
+
* M3/M8 boundary handshake: the `plan-context.js` envelope renders them
|
|
10
|
+
* into `systemPrompts.spec` / `systemPrompts.acceptance` so the envelope
|
|
11
|
+
* is authoritative from day one, and M8 (#4479) then deletes the
|
|
12
|
+
* skill-side copies. This mirrors the Story #4162 pattern established by
|
|
13
|
+
* `decomposer-prompts.js` for the decompose prompt: one rendered carrier,
|
|
14
|
+
* no second verbatim copy to drift.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The Tech Spec authoring system prompt (verbatim from the
|
|
19
|
+
* `epic-plan-spec-author` SKILL's authoritative block).
|
|
20
|
+
*
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function renderTechSpecSystemPrompt() {
|
|
24
|
+
return TECH_SPEC_SYSTEM_PROMPT;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The Acceptance Spec authoring system prompt (verbatim from the
|
|
29
|
+
* `epic-plan-spec-author` SKILL's authoritative block).
|
|
30
|
+
*
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
export function renderAcceptanceSpecSystemPrompt() {
|
|
34
|
+
return ACCEPTANCE_SPEC_SYSTEM_PROMPT;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const TECH_SPEC_SYSTEM_PROMPT = `You are an expert Engineering Architect.
|
|
38
|
+
Your job is to convert an Epic into a Technical Specification for implementation.
|
|
39
|
+
|
|
40
|
+
The Tech Spec should outline:
|
|
41
|
+
1. Delivery Slicing — propose how the Epic's enumerated capabilities cluster into shippable Stories. This count is a CEILING, not a target: the Phase 8 consolidation pass may merge below your proposed count when slices form dependent single-consumer chains, but never splits above it. Do NOT coarsen the Epic enumeration to produce this; the grouping recommendation is the granularity lever.
|
|
42
|
+
2. Architecture & Design
|
|
43
|
+
3. Data Models (if any)
|
|
44
|
+
4. API Changes (if any)
|
|
45
|
+
5. Core Components
|
|
46
|
+
6. Security & Privacy Considerations
|
|
47
|
+
|
|
48
|
+
CRITICAL REQUIREMENTS:
|
|
49
|
+
- Respond ONLY with valid Markdown.
|
|
50
|
+
- Do not use top-level <h1> (# ) tags. Open the document with the \`## Delivery Slicing\` section — it is the primary input to Phase 8 consolidation, so author it first and hang the rest of the spec off it.
|
|
51
|
+
- Do NOT restate the Epic's Context, Goal, or Scope — your output lands as sections of the same Epic body, which travels into every downstream story agent's prompt, so any restatement is pure duplication and a drift risk. If a brief technical orientation is genuinely useful, add an optional \`## Technical Overview\` of no more than 2–3 sentences that names the *technical approach* only (which subsystems are touched and reused); never re-narrate the problem statement, goals, or scope.
|
|
52
|
+
- Format architectural decisions clearly with bullet points.
|
|
53
|
+
- Author the \`## Delivery Slicing\` section as a markdown table with columns \`Slice | What ships | Independent?\`, using noun-phrase slice names (e.g. "Foundation", "Transport seam", "Send helper") that map onto Feature titles. "Independent?" answers: can this slice ship to production and provide value without the next slice landing? A slice you mark "Independent? No" MUST carry a one-line justification (parallelism, risk isolation, or delivery-envelope pressure); an unjustified dependent single-consumer slice folds into its consumer by default rather than shipping as its own Story.`;
|
|
54
|
+
|
|
55
|
+
const ACCEPTANCE_SPEC_SYSTEM_PROMPT = `You are an expert Acceptance Engineer.
|
|
56
|
+
Your job is to convert an Epic and a Tech Spec into a structured Acceptance Specification that drives features-first BDD authoring.
|
|
57
|
+
|
|
58
|
+
The Acceptance Spec should outline:
|
|
59
|
+
1. Acceptance Table — one row per user-visible outcome, expressed as a Markdown table with columns: AC ID | Outcome | Feature File | Scenario | Disposition
|
|
60
|
+
2. Stable AC IDs — assign AC-1, AC-2, ... in document order; reuse the same ID across re-plans when an Outcome is materially unchanged so scenario tags (@ac-N) stay aligned
|
|
61
|
+
3. Disposition — tag each row with one of: new | updated | unchanged
|
|
62
|
+
|
|
63
|
+
The Epic body's \`## Acceptance Criteria\` bullets are the single source of truth for what the spec verifies. Your table does not re-invent criteria — it anchors each one to a specific Epic AC bullet.
|
|
64
|
+
|
|
65
|
+
CRITICAL REQUIREMENTS:
|
|
66
|
+
- Respond ONLY with valid Markdown.
|
|
67
|
+
- Do not use top-level <h1> (# ) tags. Start with ## Acceptance Table — the table lands as a section of the Epic body, so it must NOT reuse the Epic's own ## Acceptance Criteria heading.
|
|
68
|
+
- Every AC row MUST have a stable AC ID of the form AC-<n> (AC-1, AC-2, ...) — do not reorder IDs across re-plans; new ACs get fresh sequential IDs.
|
|
69
|
+
- Every AC row MUST carry a Disposition value from the enum: new | updated | unchanged. (At Epic close, the acceptance reconciler overwrites Disposition with the verification outcome — satisfied | pending | missing — inside this section only; on re-plan, reset each row to the authoring enum.)
|
|
70
|
+
- Each Outcome MUST be a **terse restatement keyed to a specific Epic \`## Acceptance Criteria\` bullet** — lead the Outcome with the bullet's anchor (its quoted lead phrase or an explicit "Epic AC N" index) and keep the rest to a single user-visible behaviour. Do NOT re-elaborate the Epic bullet in independent words: a free-standing Outcome that paraphrases the criterion without naming the bullet it verifies is forbidden, because it drifts from the Epic silently. No DB assertions, no HTTP status codes, no internal implementation details.
|
|
71
|
+
- Where one Epic AC bullet genuinely expands into several user-visible outcomes, emit one row per outcome and declare the split on each — e.g. lead with "splits Epic AC 3" — so the fan-out is explicit rather than hidden.
|
|
72
|
+
- Anchor coverage MUST be complete and auditable: every Epic AC bullet MUST be covered by at least one row, and every row MUST anchor to an Epic AC bullet. Flag divergence in the authored spec instead of dropping it — if an Epic AC bullet has no corresponding row, or a row has no Epic anchor, call it out explicitly (a note beneath the table) rather than silently omitting the bullet or emitting an unanchored row.
|
|
73
|
+
- Cite proposed feature file paths under tests/features/** so Phase 8 can scaffold matching scenarios.
|
|
74
|
+
- Acceptance Outcomes MUST NOT prescribe a commit subject that begins with a non-Conventional-Commits prefix (allowed leading types: feat|fix|chore|refactor|perf|docs|style|test|build|ci|revert). The legacy \`baseline-refresh\` token used as a leading subject prescription is forbidden — commitlint will reject it at commit time, and the decompose-time validator (\`ticket-validator.js\` → \`validateAcceptanceSubjectPrefix\`) will reject the decompose with \`code: 'forbidden-subject-prefix'\`. Use a Conventional-Commits subject (e.g. \`chore(baselines): refresh ...\`) and a body trailer (e.g. \`baseline-refresh: true\` — trailer with a value, not a subject prefix) when a machine-readable marker is needed. See Epic #2501 for rationale.`;
|
|
@@ -0,0 +1,186 @@
|
|
|
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
|
+
* Two 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
|
+
* Flags:
|
|
27
|
+
* --pretty Pretty-print the JSON envelope.
|
|
28
|
+
* --full-context Bypass the planning-context budget (unbounded body).
|
|
29
|
+
*
|
|
30
|
+
* stdout is reserved for the JSON envelope (Story #2278 discipline):
|
|
31
|
+
* `routeAllOutputToStderr()` runs before any pipeline code so a captured
|
|
32
|
+
* file is unconditionally parseable by `JSON.parse`.
|
|
33
|
+
*
|
|
34
|
+
* Exit codes:
|
|
35
|
+
* 0 — envelope emitted.
|
|
36
|
+
* 1 — fatal error (see stderr).
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
// Fail-fast if the framework's runtime deps are not installed — must be the
|
|
40
|
+
// first import so the check runs before any third-party-importing sibling
|
|
41
|
+
// module is evaluated (Story #3432).
|
|
42
|
+
import './lib/runtime-deps/ensure-installed.js';
|
|
43
|
+
import { parseArgs } from 'node:util';
|
|
44
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
45
|
+
import {
|
|
46
|
+
resolveConfig,
|
|
47
|
+
validateOrchestrationConfig,
|
|
48
|
+
} from './lib/config-resolver.js';
|
|
49
|
+
import { routeAllOutputToStderr } from './lib/Logger.js';
|
|
50
|
+
import { buildPlanContext } from './lib/orchestration/plan-context.js';
|
|
51
|
+
import { recordPlanInvocation } from './lib/orchestration/plan-metrics.js';
|
|
52
|
+
import { createProvider } from './lib/provider-factory.js';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build the envelope and write it to `stdout` as a single JSON line
|
|
56
|
+
* (or pretty-printed with --pretty). Exported for tests: the stdout-purity
|
|
57
|
+
* test injects a fake provider and a capture stream and asserts the
|
|
58
|
+
* captured output is exactly one `JSON.parse`-able payload.
|
|
59
|
+
*
|
|
60
|
+
* @param {{
|
|
61
|
+
* mode: 'epic'|'one-pager',
|
|
62
|
+
* epicId?: number,
|
|
63
|
+
* onePagerPath?: string,
|
|
64
|
+
* onePagerContent?: string,
|
|
65
|
+
* provider: object,
|
|
66
|
+
* config: object,
|
|
67
|
+
* settings: object,
|
|
68
|
+
* fullContext?: boolean,
|
|
69
|
+
* pretty?: boolean,
|
|
70
|
+
* cwd?: string,
|
|
71
|
+
* stdout?: { write: (chunk: string) => void },
|
|
72
|
+
* }} args
|
|
73
|
+
* @returns {Promise<object>} the emitted envelope.
|
|
74
|
+
*/
|
|
75
|
+
export async function emitPlanContext({
|
|
76
|
+
mode,
|
|
77
|
+
epicId,
|
|
78
|
+
onePagerPath,
|
|
79
|
+
onePagerContent,
|
|
80
|
+
provider,
|
|
81
|
+
config,
|
|
82
|
+
settings,
|
|
83
|
+
fullContext = false,
|
|
84
|
+
pretty = false,
|
|
85
|
+
cwd,
|
|
86
|
+
stdout = process.stdout,
|
|
87
|
+
}) {
|
|
88
|
+
const envelope = await buildPlanContext({
|
|
89
|
+
mode,
|
|
90
|
+
epicId,
|
|
91
|
+
onePagerPath,
|
|
92
|
+
onePagerContent,
|
|
93
|
+
provider,
|
|
94
|
+
config,
|
|
95
|
+
settings,
|
|
96
|
+
fullContext,
|
|
97
|
+
cwd,
|
|
98
|
+
});
|
|
99
|
+
const json = pretty
|
|
100
|
+
? JSON.stringify(envelope, null, 2)
|
|
101
|
+
: JSON.stringify(envelope);
|
|
102
|
+
stdout.write(`${json}\n`);
|
|
103
|
+
return envelope;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function main() {
|
|
107
|
+
const { values } = parseArgs({
|
|
108
|
+
options: {
|
|
109
|
+
epic: { type: 'string' },
|
|
110
|
+
'one-pager': { type: 'string' },
|
|
111
|
+
pretty: { type: 'boolean', default: false },
|
|
112
|
+
'full-context': { type: 'boolean', default: false },
|
|
113
|
+
},
|
|
114
|
+
strict: true,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const hasEpic = typeof values.epic === 'string' && values.epic.length > 0;
|
|
118
|
+
const hasOnePager =
|
|
119
|
+
typeof values['one-pager'] === 'string' && values['one-pager'].length > 0;
|
|
120
|
+
if (hasEpic === hasOnePager) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
'Pass exactly one of --epic <id> or --one-pager <path>. ' +
|
|
123
|
+
'(--epic: existing-Epic mode; --one-pager: ideation mode.)',
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let epicId;
|
|
128
|
+
if (hasEpic) {
|
|
129
|
+
epicId = Number.parseInt(values.epic, 10);
|
|
130
|
+
if (!Number.isInteger(epicId)) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`--epic must be a numeric issue id (got "${values.epic}").`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// stdout is reserved for the JSON envelope: flip every Logger sink that
|
|
138
|
+
// could land on stdout to stderr BEFORE any pipeline code runs
|
|
139
|
+
// (Story #2278 — the same stdout-purity guarantee the retired pipeline
|
|
140
|
+
// gives; this CLI is emit-only so the flip is unconditional).
|
|
141
|
+
routeAllOutputToStderr();
|
|
142
|
+
|
|
143
|
+
let config;
|
|
144
|
+
let settings;
|
|
145
|
+
try {
|
|
146
|
+
config = resolveConfig();
|
|
147
|
+
// `settings` retains the legacy bag shape `buildAuthoringContext` and
|
|
148
|
+
// friends consume: `{ baseBranch, paths, planning, docsContextFiles }`.
|
|
149
|
+
settings = {
|
|
150
|
+
baseBranch: config.project?.baseBranch,
|
|
151
|
+
paths: config.project?.paths,
|
|
152
|
+
planning: config.planning,
|
|
153
|
+
docsContextFiles: config.project?.docsContextFiles,
|
|
154
|
+
};
|
|
155
|
+
validateOrchestrationConfig(config);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
throw new Error(`Config schema validation failed:\n${err.message}`);
|
|
158
|
+
}
|
|
159
|
+
const provider = createProvider(config);
|
|
160
|
+
|
|
161
|
+
// Plan-metrics ledger (#4474 PR1): stamp entry/exit + mode so the folded
|
|
162
|
+
// emit surface is measured against the 12-phase baseline. One-pager mode
|
|
163
|
+
// has no Epic yet, so the record routes to the standalone stream
|
|
164
|
+
// (epicId null) exactly like `story-plan.js`.
|
|
165
|
+
await recordPlanInvocation(
|
|
166
|
+
{
|
|
167
|
+
cli: 'plan-context',
|
|
168
|
+
mode: hasEpic ? 'epic' : 'one-pager',
|
|
169
|
+
epicId: hasEpic ? epicId : null,
|
|
170
|
+
config,
|
|
171
|
+
},
|
|
172
|
+
() =>
|
|
173
|
+
emitPlanContext({
|
|
174
|
+
mode: hasEpic ? 'epic' : 'one-pager',
|
|
175
|
+
epicId,
|
|
176
|
+
onePagerPath: hasOnePager ? values['one-pager'] : undefined,
|
|
177
|
+
provider,
|
|
178
|
+
config,
|
|
179
|
+
settings,
|
|
180
|
+
fullContext: values['full-context'],
|
|
181
|
+
pretty: values.pretty,
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
runAsCli(import.meta.url, main, { source: 'plan-context' });
|
|
@@ -0,0 +1,227 @@
|
|
|
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
|
+
* Runs between authoring and gate #2, entirely git-local (zero GitHub
|
|
9
|
+
* calls): reads the authored artifacts, evaluates the risk/size dispatch
|
|
10
|
+
* conditions for the consolidation (8.3) and pre-mortem (8.5) critics via
|
|
11
|
+
* `lib/orchestration/plan-critic-conditions.js`, and emits one JSON
|
|
12
|
+
* verdict on stdout. The workflow dispatches a fresh-context sub-agent
|
|
13
|
+
* ONLY for critics with `dispatch: true`; every skip decision is appended
|
|
14
|
+
* to the plan-metrics ledger (`kind: "critic-skip"`, with reasons) so
|
|
15
|
+
* under-firing is auditable — the persist validators remain unchanged
|
|
16
|
+
* hard gates regardless of what this gate decides.
|
|
17
|
+
*
|
|
18
|
+
* Conditions (design §4 / §6 PR6):
|
|
19
|
+
* - Consolidation: the existing deterministic precondition
|
|
20
|
+
* (`evaluateConsolidationPrecondition`) says dispatch AND (the draft
|
|
21
|
+
* has > 5 stories OR a confirmed divergence from the Tech Spec's
|
|
22
|
+
* Delivery Slicing table). Skipped outright in the single-delivery
|
|
23
|
+
* shape (no tickets exist to consolidate).
|
|
24
|
+
* - Pre-mortem: risk verdict overall level is high, OR ticket count is
|
|
25
|
+
* at least half `maxTickets`, OR any `planning.riskHeuristics` phrase
|
|
26
|
+
* matches the plan text (case-insensitive substring over tech spec +
|
|
27
|
+
* tickets + risk summary).
|
|
28
|
+
*
|
|
29
|
+
* Modes:
|
|
30
|
+
* --epic <id> Artifact paths default to the per-Epic temp tree
|
|
31
|
+
* (`temp/epic-<id>/techspec.md`, `risk-verdict.json`,
|
|
32
|
+
* `tickets.json`); skip records land on the
|
|
33
|
+
* per-Epic plan-metrics ledger.
|
|
34
|
+
* explicit paths Ideation mode: pass --tech-spec/--risk-verdict
|
|
35
|
+
* (and --tickets when fan-out) explicitly; skip
|
|
36
|
+
* records land on the standalone ledger stream.
|
|
37
|
+
*
|
|
38
|
+
* Output (stdout-pure JSON):
|
|
39
|
+
* { epicId, consolidation: { critic, dispatch, reasons },
|
|
40
|
+
* premortem: { critic, dispatch, reasons } }
|
|
41
|
+
*
|
|
42
|
+
* Exit codes: 0 — verdict emitted (dispatch decisions are data, not
|
|
43
|
+
* failures); 1 — fatal error (unreadable/invalid artifacts, bad args).
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
// Fail-fast if the framework's runtime deps are not installed — must be the
|
|
47
|
+
// first import so the check runs before any third-party-importing sibling
|
|
48
|
+
// module is evaluated (Story #3432).
|
|
49
|
+
import './lib/runtime-deps/ensure-installed.js';
|
|
50
|
+
import { readFile } from 'node:fs/promises';
|
|
51
|
+
import { parseArgs } from 'node:util';
|
|
52
|
+
|
|
53
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
54
|
+
import { epicArtifactPath } from './lib/config/temp-paths.js';
|
|
55
|
+
import {
|
|
56
|
+
getLimits,
|
|
57
|
+
resolveConfig,
|
|
58
|
+
validateOrchestrationConfig,
|
|
59
|
+
} from './lib/config-resolver.js';
|
|
60
|
+
import { routeAllOutputToStderr } from './lib/Logger.js';
|
|
61
|
+
import { loadRiskVerdict } from './lib/orchestration/epic-plan-spec/phases/risk-verdict.js';
|
|
62
|
+
import {
|
|
63
|
+
evaluateConsolidationDispatch,
|
|
64
|
+
evaluatePremortemDispatch,
|
|
65
|
+
} from './lib/orchestration/plan-critic-conditions.js';
|
|
66
|
+
import {
|
|
67
|
+
appendCriticSkip,
|
|
68
|
+
recordPlanInvocation,
|
|
69
|
+
} from './lib/orchestration/plan-metrics.js';
|
|
70
|
+
|
|
71
|
+
const USAGE =
|
|
72
|
+
'Usage: plan-critics.js (--epic <EpicId> | --tech-spec <file> ' +
|
|
73
|
+
'--risk-verdict <file> [--tickets <file>]) [--pretty]';
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolve the planning risk heuristics list from the canonical config
|
|
77
|
+
* block (same resolution `plan-context.js` and the decompose context use).
|
|
78
|
+
*
|
|
79
|
+
* @param {object} config
|
|
80
|
+
* @returns {string[]}
|
|
81
|
+
*/
|
|
82
|
+
function resolveRiskHeuristics(config = {}) {
|
|
83
|
+
if (Array.isArray(config.planning?.riskHeuristics)) {
|
|
84
|
+
return config.planning.riskHeuristics;
|
|
85
|
+
}
|
|
86
|
+
return config.agentSettings?.planning?.riskHeuristics || [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function readOptional(filePath, { required }) {
|
|
90
|
+
try {
|
|
91
|
+
return await readFile(filePath, 'utf8');
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (!required && err?.code === 'ENOENT') return null;
|
|
94
|
+
throw new Error(`Cannot read ${filePath}: ${err.message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function main() {
|
|
99
|
+
const { values } = parseArgs({
|
|
100
|
+
options: {
|
|
101
|
+
epic: { type: 'string' },
|
|
102
|
+
'tech-spec': { type: 'string' },
|
|
103
|
+
'risk-verdict': { type: 'string' },
|
|
104
|
+
tickets: { type: 'string' },
|
|
105
|
+
pretty: { type: 'boolean', default: false },
|
|
106
|
+
},
|
|
107
|
+
strict: true,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
let epicId = null;
|
|
111
|
+
if (values.epic !== undefined) {
|
|
112
|
+
epicId = Number.parseInt(values.epic, 10);
|
|
113
|
+
if (!Number.isInteger(epicId)) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`--epic must be a numeric issue id (got "${values.epic}").\n${USAGE}`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// stdout is reserved for the JSON verdict — flip every Logger sink to
|
|
121
|
+
// stderr before any pipeline code runs (same guarantee plan-context.js
|
|
122
|
+
// gives its envelope).
|
|
123
|
+
routeAllOutputToStderr();
|
|
124
|
+
|
|
125
|
+
let config;
|
|
126
|
+
try {
|
|
127
|
+
config = resolveConfig();
|
|
128
|
+
validateOrchestrationConfig(config);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
throw new Error(`Config schema validation failed:\n${err.message}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const fallback = (basename) =>
|
|
134
|
+
epicId === null ? undefined : epicArtifactPath(epicId, basename, config);
|
|
135
|
+
const techSpecPath = values['tech-spec'] ?? fallback('techspec.md');
|
|
136
|
+
const riskVerdictPath =
|
|
137
|
+
values['risk-verdict'] ?? fallback('risk-verdict.json');
|
|
138
|
+
const ticketsPath = values.tickets ?? fallback('tickets.json');
|
|
139
|
+
if (!techSpecPath || !riskVerdictPath) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Missing artifact path(s): without --epic, explicit --tech-spec and --risk-verdict are required.\n${USAGE}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const verdict = await recordPlanInvocation(
|
|
146
|
+
{ cli: 'plan-critics', mode: 'evaluate', epicId, config },
|
|
147
|
+
async () => {
|
|
148
|
+
const techSpecContent = await readOptional(techSpecPath, {
|
|
149
|
+
required: true,
|
|
150
|
+
});
|
|
151
|
+
const riskVerdict = loadRiskVerdict(riskVerdictPath);
|
|
152
|
+
// Tickets are shape-dependent: a single-delivery plan authors none.
|
|
153
|
+
// Required only when passed explicitly.
|
|
154
|
+
const ticketsRaw = ticketsPath
|
|
155
|
+
? await readOptional(ticketsPath, {
|
|
156
|
+
required: values.tickets !== undefined,
|
|
157
|
+
})
|
|
158
|
+
: null;
|
|
159
|
+
let tickets = null;
|
|
160
|
+
if (ticketsRaw !== null) {
|
|
161
|
+
try {
|
|
162
|
+
tickets = JSON.parse(ticketsRaw);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Failed to parse tickets file "${ticketsPath}" as JSON: ${err.message}`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if (!Array.isArray(tickets)) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Tickets file "${ticketsPath}" must contain a JSON array.`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const consolidation =
|
|
176
|
+
tickets === null
|
|
177
|
+
? {
|
|
178
|
+
critic: 'consolidation',
|
|
179
|
+
dispatch: false,
|
|
180
|
+
reasons: [
|
|
181
|
+
'single-delivery shape — no draft tickets exist to consolidate.',
|
|
182
|
+
],
|
|
183
|
+
}
|
|
184
|
+
: evaluateConsolidationDispatch({
|
|
185
|
+
draftStories: tickets,
|
|
186
|
+
specText: techSpecContent,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const premortem = evaluatePremortemDispatch({
|
|
190
|
+
riskVerdict,
|
|
191
|
+
ticketCount: tickets?.length ?? 0,
|
|
192
|
+
maxTickets: getLimits(config).maxTickets,
|
|
193
|
+
riskHeuristics: resolveRiskHeuristics(config),
|
|
194
|
+
planText: [
|
|
195
|
+
techSpecContent,
|
|
196
|
+
ticketsRaw ?? '',
|
|
197
|
+
riskVerdict.summary ?? '',
|
|
198
|
+
].join('\n'),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Skip-audit trail (#4474 PR6): every non-dispatch is a ledger
|
|
202
|
+
// record. Best-effort — a failed append never fails the gate.
|
|
203
|
+
for (const decision of [consolidation, premortem]) {
|
|
204
|
+
if (!decision.dispatch) {
|
|
205
|
+
await appendCriticSkip(
|
|
206
|
+
{
|
|
207
|
+
critic: decision.critic,
|
|
208
|
+
reasons: decision.reasons,
|
|
209
|
+
cli: 'plan-critics',
|
|
210
|
+
epicId,
|
|
211
|
+
},
|
|
212
|
+
config,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { epicId, consolidation, premortem };
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const json = values.pretty
|
|
222
|
+
? JSON.stringify(verdict, null, 2)
|
|
223
|
+
: JSON.stringify(verdict);
|
|
224
|
+
process.stdout.write(`${json}\n`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
runAsCli(import.meta.url, main, { source: 'plan-critics' });
|