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,234 +0,0 @@
1
- #!/usr/bin/env node
2
- /* node:coverage ignore file -- rebase CLI; conflict-resolution heuristics over real git state, testing would assert only the mock structure */
3
-
4
- /**
5
- * git-rebase-and-resolve.js — Rebase pipeline for `/git-merge-pr` Step 2.5.
6
- *
7
- * `/git-merge-pr` previously embedded the rebase retry loop as prose in
8
- * markdown: "rebase onto base → if conflicts, follow the template → stage and
9
- * continue → force-push". That works right up until the LLM forgets one of
10
- * the steps (the force-push with `--force-with-lease`, the abort on
11
- * cascading conflicts, the continue after each resolved batch).
12
- *
13
- * This script runs the rebase, reports the outcome in structured form, and
14
- * — when a conflict stops the rebase — leaves the working tree in the
15
- * conflicted state so the caller can apply manual resolutions and invoke
16
- * `--continue` (delegating back to git's native flow rather than trying to
17
- * automate conflict merging, which is a problem for judgment not scripting).
18
- *
19
- * Usage:
20
- * node .agents/scripts/git-rebase-and-resolve.js --onto <base> [--head <branch>] [--continue] [--abort] [--json]
21
- *
22
- * Modes:
23
- * (default) — `git fetch origin`, checkout head, `git rebase <onto>`.
24
- * --continue — `git rebase --continue` after resolutions are staged.
25
- * --abort — `git rebase --abort` to bail cleanly.
26
- *
27
- * Output (always JSON when --json, otherwise human-readable):
28
- * {
29
- * outcome: 'clean' | 'conflict' | 'aborted' | 'continued',
30
- * head, onto,
31
- * conflictedFiles: [...],
32
- * stderr: "...",
33
- * }
34
- *
35
- * Exit codes:
36
- * 0 — clean rebase or continue that completed.
37
- * 1 — conflict state (caller must resolve then invoke --continue).
38
- * 2 — usage / git error.
39
- */
40
-
41
- import { parseArgs } from 'node:util';
42
- import { runAsCli } from './lib/cli-utils.js';
43
- import { gitSpawn } from './lib/git-utils.js';
44
- import { Logger } from './lib/Logger.js';
45
- import { PROJECT_ROOT } from './lib/project-root.js';
46
-
47
- function currentBranch(cwd) {
48
- const res = gitSpawn(cwd, 'rev-parse', '--abbrev-ref', 'HEAD');
49
- return res.status === 0 ? res.stdout.trim() : null;
50
- }
51
-
52
- function listConflictedFiles(cwd) {
53
- const res = gitSpawn(cwd, 'diff', '--name-only', '--diff-filter=U');
54
- if (res.status !== 0) return [];
55
- return res.stdout
56
- .split('\n')
57
- .map((l) => l.trim())
58
- .filter(Boolean);
59
- }
60
-
61
- /**
62
- * Perform a rebase and classify the outcome. Pure seam: callers inject a
63
- * `git` runner for tests.
64
- *
65
- * @param {{
66
- * onto: string,
67
- * head?: string,
68
- * cwd?: string,
69
- * git?: { spawn: (cwd: string, ...args: string[]) => { status: number, stdout: string, stderr: string } },
70
- * }} opts
71
- */
72
- export function runRebase({
73
- onto,
74
- head,
75
- cwd = PROJECT_ROOT,
76
- git = { spawn: gitSpawn },
77
- }) {
78
- if (!onto) throw new Error('[rebase] --onto is required.');
79
-
80
- const fetch = git.spawn(cwd, 'fetch', 'origin');
81
- if (fetch.status !== 0) {
82
- return {
83
- outcome: 'error',
84
- head: head ?? null,
85
- onto,
86
- conflictedFiles: [],
87
- stderr: fetch.stderr ?? '',
88
- };
89
- }
90
-
91
- if (head) {
92
- const checkout = git.spawn(cwd, 'checkout', head);
93
- if (checkout.status !== 0) {
94
- return {
95
- outcome: 'error',
96
- head,
97
- onto,
98
- conflictedFiles: [],
99
- stderr: checkout.stderr ?? '',
100
- };
101
- }
102
- git.spawn(cwd, 'pull', 'origin', head);
103
- }
104
-
105
- const rebase = git.spawn(cwd, 'rebase', onto);
106
- if (rebase.status === 0) {
107
- return {
108
- outcome: 'clean',
109
- head: head ?? currentBranch(cwd),
110
- onto,
111
- conflictedFiles: [],
112
- stderr: '',
113
- };
114
- }
115
-
116
- // Non-zero exit: likely conflict. Enumerate the unmerged files so the caller
117
- // can route to the conflict-template procedure.
118
- const conflicted = listConflictedFiles(cwd);
119
- return {
120
- outcome: conflicted.length > 0 ? 'conflict' : 'error',
121
- head: head ?? currentBranch(cwd),
122
- onto,
123
- conflictedFiles: conflicted,
124
- stderr: rebase.stderr ?? '',
125
- };
126
- }
127
-
128
- export function continueRebase({
129
- cwd = PROJECT_ROOT,
130
- git = { spawn: gitSpawn },
131
- }) {
132
- const res = git.spawn(cwd, 'rebase', '--continue');
133
- if (res.status === 0) {
134
- return { outcome: 'continued', conflictedFiles: [], stderr: '' };
135
- }
136
- return {
137
- outcome: 'conflict',
138
- conflictedFiles: listConflictedFiles(cwd),
139
- stderr: res.stderr ?? '',
140
- };
141
- }
142
-
143
- export function abortRebase({ cwd = PROJECT_ROOT, git = { spawn: gitSpawn } }) {
144
- const res = git.spawn(cwd, 'rebase', '--abort');
145
- return {
146
- outcome: res.status === 0 ? 'aborted' : 'error',
147
- stderr: res.stderr ?? '',
148
- };
149
- }
150
-
151
- /**
152
- * Pure: parse argv into the normalized rebase-action option bag.
153
- *
154
- * @param {string[]} argv
155
- * @returns {{ onto?: string, head?: string, continueFlag: boolean, abortFlag: boolean, json: boolean }}
156
- */
157
- export function parseRebaseArgs(argv) {
158
- const { values } = parseArgs({
159
- args: argv,
160
- options: {
161
- onto: { type: 'string' },
162
- head: { type: 'string' },
163
- continue: { type: 'boolean', default: false },
164
- abort: { type: 'boolean', default: false },
165
- json: { type: 'boolean', default: false },
166
- },
167
- strict: false,
168
- });
169
- return {
170
- onto: values.onto,
171
- head: values.head,
172
- continueFlag: values.continue === true,
173
- abortFlag: values.abort === true,
174
- json: values.json === true,
175
- };
176
- }
177
-
178
- /** Pure: did the rebase end in a clean / non-failure outcome? */
179
- export function isCleanRebaseOutcome(outcome) {
180
- return (
181
- outcome === 'clean' || outcome === 'continued' || outcome === 'aborted'
182
- );
183
- }
184
-
185
- /** Pure: render the human-facing output lines for a rebase result. */
186
- export function renderRebaseHumanLines(result) {
187
- const lines = [`[rebase] outcome: ${result.outcome}`];
188
- if (result.conflictedFiles?.length > 0) {
189
- lines.push(`[rebase] conflicted files (${result.conflictedFiles.length}):`);
190
- for (const f of result.conflictedFiles) lines.push(` - ${f}`);
191
- }
192
- return lines;
193
- }
194
-
195
- /**
196
- * Pure: dispatch the parsed args to the matching rebase action. Throws when
197
- * the implicit-rebase path was selected without `--onto`. Exported for tests.
198
- */
199
- export function selectRebaseAction(args) {
200
- if (args.abortFlag) return { kind: 'abort' };
201
- if (args.continueFlag) return { kind: 'continue' };
202
- if (!args.onto) {
203
- return {
204
- kind: 'usage-error',
205
- message:
206
- 'Usage: node git-rebase-and-resolve.js --onto <base> [--head <branch>] [--json]',
207
- };
208
- }
209
- return { kind: 'rebase', onto: args.onto, head: args.head };
210
- }
211
-
212
- function runSelectedAction(action, cwd) {
213
- if (action.kind === 'abort') return abortRebase({ cwd });
214
- if (action.kind === 'continue') return continueRebase({ cwd });
215
- return runRebase({ onto: action.onto, head: action.head, cwd });
216
- }
217
-
218
- function emitRebaseHuman(result) {
219
- for (const line of renderRebaseHumanLines(result)) Logger.info(line);
220
- if (result.stderr?.trim()) Logger.error(result.stderr.trim());
221
- }
222
-
223
- /* node:coverage ignore next */
224
- async function main() {
225
- const args = parseRebaseArgs(process.argv.slice(2));
226
- const action = selectRebaseAction(args);
227
- if (action.kind === 'usage-error') throw new Error(action.message);
228
- const result = runSelectedAction(action, PROJECT_ROOT);
229
- if (args.json) process.stdout.write(`${JSON.stringify(result)}\n`);
230
- else emitRebaseHuman(result);
231
- if (!isCleanRebaseOutcome(result.outcome)) process.exit(1);
232
- }
233
-
234
- runAsCli(import.meta.url, main, { source: 'git-rebase-and-resolve' });
@@ -1,192 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * .agents/scripts/hierarchy-gate.js — Hierarchy Completeness Gate
5
- *
6
- * Walks the Epic's full sub-issue graph (Stories) and verifies
7
- * every descendant is closed. Where the wave gate asks "did the sprint
8
- * complete what it committed to?" (manifest view), this gate asks "is
9
- * anything still open under this Epic?" (live GitHub graph view).
10
- *
11
- * The two gates catch different problems and are intentionally distinct:
12
- * - The wave gate misses descendants that exist on GitHub but were never
13
- * in the manifest — mid-sprint additions, recuts that bypassed the
14
- * dispatcher, or legacy `context::*` artifacts on historical Epics.
15
- * - The hierarchy gate misses parked follow-ons that live as separate
16
- * top-level Stories outside the Epic's sub-issue graph.
17
- *
18
- * Per ticket type the rule is:
19
- * - Stories — must be closed.
20
- * - Auxiliary (legacy `context::*` artifacts) — ignored. Story #4324
21
- * folded planning content into the Epic body; historical Epics keep
22
- * their old context tickets, which are reference-only here.
23
- *
24
- * **2-tier hierarchy (Story #4041).** Mandrel ships only Epic / Story
25
- * tickets. `getSubTickets(<storyId>)` returns `[]`; the walk
26
- * terminates at the Story. Acceptance criteria live inline on the
27
- * Story body.
28
- *
29
- * Usage:
30
- * node .agents/scripts/hierarchy-gate.js --epic <EPIC_ID>
31
- *
32
- * Exit codes:
33
- * 0 — every descendant ticket is closed.
34
- * 1 — one or more descendants are still open.
35
- * 2 — configuration or provider error.
36
- */
37
-
38
- import { parseArgs } from 'node:util';
39
- import { runAsCli } from './lib/cli-utils.js';
40
- import { resolveConfig } from './lib/config-resolver.js';
41
- import { Logger } from './lib/Logger.js';
42
- import { TYPE_LABELS } from './lib/label-constants.js';
43
- import { createProvider } from './lib/provider-factory.js';
44
- import { concurrentMap } from './lib/util/concurrent-map.js';
45
-
46
- /**
47
- * Bounded fan-out for per-level `getSubTickets` calls. Matches the
48
- * wave-record-io.js precedent (Story #3024).
49
- */
50
- const SUB_TICKET_FETCH_CONCURRENCY = 4;
51
-
52
- function classify(ticket) {
53
- const labels = ticket.labels ?? [];
54
- if (labels.includes(TYPE_LABELS.STORY)) return 'story';
55
- // Legacy planning artifacts (pre-#4324 `context::*` tickets on
56
- // historical Epics) are ignored — they are reference artifacts, not
57
- // deliverables. New Epics carry planning content on the body itself.
58
- if (labels.some((l) => typeof l === 'string' && l.startsWith('context::'))) {
59
- return 'auxiliary';
60
- }
61
- return 'other';
62
- }
63
-
64
- function ticketIsComplete(ticket) {
65
- if (ticket.state !== 'closed') {
66
- return { ok: false, reason: 'open' };
67
- }
68
- return { ok: true };
69
- }
70
-
71
- /**
72
- * BFS the sub-issue graph from the Epic. Returns one entry per descendant
73
- * ticket with full metadata — the caller checks completeness and formats
74
- * the failure list.
75
- */
76
- async function collectDescendants(provider, epicId) {
77
- const visited = new Set([epicId]);
78
- const out = [];
79
- // Level-order BFS: each round fetches the whole frontier's children with a
80
- // bounded-parallel map instead of one awaited round-trip per node. Stories
81
- // are leaves (no sub-issues by contract), so they are never expanded
82
- // — that skip alone removes the largest class of wasted GraphQL calls.
83
- let frontier = [epicId];
84
- while (frontier.length > 0) {
85
- const levels = await concurrentMap(
86
- frontier,
87
- async (parentId) => {
88
- try {
89
- return await provider.getSubTickets(parentId);
90
- } catch (err) {
91
- throw new Error(`getSubTickets(#${parentId}) failed: ${err.message}`);
92
- }
93
- },
94
- { concurrency: SUB_TICKET_FETCH_CONCURRENCY },
95
- );
96
- const next = [];
97
- for (const children of levels) {
98
- for (const child of children) {
99
- if (visited.has(child.id)) continue;
100
- visited.add(child.id);
101
- out.push(child);
102
- const labels = child.labels ?? [];
103
- if (!labels.includes(TYPE_LABELS.STORY)) next.push(child.id);
104
- }
105
- }
106
- frontier = next;
107
- }
108
- return out;
109
- }
110
-
111
- export async function runHierarchyGate({ epicId, injectedProvider } = {}) {
112
- if (!epicId || Number.isNaN(epicId) || epicId <= 0) {
113
- throw new Error('Usage: node hierarchy-gate.js --epic <EPIC_ID>');
114
- }
115
-
116
- const config = resolveConfig();
117
- const provider = injectedProvider || createProvider(config);
118
-
119
- let descendants;
120
- try {
121
- descendants = await collectDescendants(provider, epicId);
122
- } catch (err) {
123
- Logger.error(`[hierarchy-gate] ${err.message}`);
124
- process.exit(2);
125
- }
126
-
127
- const failures = { story: [], other: [] };
128
- let auxiliaryDeferred = 0;
129
- for (const ticket of descendants) {
130
- const kind = classify(ticket);
131
- if (kind === 'auxiliary') {
132
- auxiliaryDeferred += 1;
133
- continue;
134
- }
135
- const verdict = ticketIsComplete(ticket);
136
- if (!verdict.ok) {
137
- failures[kind].push({
138
- id: ticket.id,
139
- title: ticket.title,
140
- reason: verdict.reason,
141
- });
142
- }
143
- }
144
-
145
- const totalOpen = failures.story.length + failures.other.length;
146
-
147
- if (totalOpen > 0) {
148
- Logger.error(
149
- `[hierarchy-gate] ❌ Hierarchy-completeness gate FAILED for Epic #${epicId}: ${totalOpen} descendant(s) incomplete.`,
150
- );
151
- const sections = [
152
- ['story', 'Stories'],
153
- ['other', 'Untyped descendants'],
154
- ];
155
- for (const [key, label] of sections) {
156
- if (failures[key].length === 0) continue;
157
- Logger.error(`\n ${label}:`);
158
- for (const item of failures[key]) {
159
- Logger.error(` - #${item.id} (${item.reason}) — ${item.title}`);
160
- }
161
- }
162
- Logger.error('\nClose the open descendants and re-run `/deliver`.');
163
- process.exit(1);
164
- }
165
-
166
- const auxNote =
167
- auxiliaryDeferred > 0
168
- ? ` (${auxiliaryDeferred} legacy auxiliary ticket${auxiliaryDeferred === 1 ? '' : 's'} ignored)`
169
- : '';
170
- Logger.info(
171
- `[hierarchy-gate] ✅ All ${descendants.length - auxiliaryDeferred} planned descendant(s) under Epic #${epicId} are closed${auxNote}.`,
172
- );
173
- return {
174
- success: true,
175
- total: descendants.length,
176
- checked: descendants.length - auxiliaryDeferred,
177
- auxiliaryDeferred,
178
- };
179
- }
180
-
181
- async function main() {
182
- const { values } = parseArgs({
183
- options: {
184
- epic: { type: 'string' },
185
- },
186
- strict: false,
187
- });
188
- const epicId = Number.parseInt(values.epic ?? '', 10);
189
- await runHierarchyGate({ epicId });
190
- }
191
-
192
- runAsCli(import.meta.url, main, { source: 'hierarchy-gate' });
@@ -1,179 +0,0 @@
1
- #!/usr/bin/env node
2
- /* node:coverage ignore file */
3
-
4
- /**
5
- * hydrate-context.js — CLI wrapper for context hydration.
6
- *
7
- * Successor to the retired mandrel MCP tools. See ADR 20260424-702a in docs/decisions.md for the migration table.
8
- *
9
- * Delegates to `hydrateContext` from `lib/orchestration/context-hydration-engine.js`
10
- * and emits the `{ prompt }` JSON envelope on stdout. Pass `--emit prompt`
11
- * to write the raw hydrated prompt (no JSON wrapper) instead — this is the
12
- * sole supported hydration entry point.
13
- *
14
- * Usage:
15
- * node .agents/scripts/hydrate-context.js --ticket <id> [--epic <id>]
16
- * node .agents/scripts/hydrate-context.js --ticket <id> --emit prompt
17
- *
18
- * If `--epic` is omitted, the epic id is parsed from the ticket body
19
- * (`Epic: #N`). Persona / skills are derived from the ticket's labels.
20
- */
21
-
22
- import { parseArgs } from 'node:util';
23
- import { runAsCli } from './lib/cli-utils.js';
24
- import { resolveConfig } from './lib/config-resolver.js';
25
- import { getEpicBranch, getStoryBranch } from './lib/git-utils.js';
26
- import { envelopeToPrompt } from './lib/orchestration/context-envelope.js';
27
- import {
28
- hydrateContext,
29
- parseHierarchy,
30
- } from './lib/orchestration/context-hydration-engine.js';
31
- import { createProvider } from './lib/provider-factory.js';
32
-
33
- const HELP = `Usage: node .agents/scripts/hydrate-context.js --ticket <id> [--epic <id>]
34
-
35
- Flags:
36
- --ticket GitHub issue number to hydrate (required).
37
- --epic Epic id (optional; parsed from the ticket body when omitted).
38
- --help Show this message.
39
-
40
- Output: a single JSON object {"prompt": "..."} on stdout by default.
41
- --emit envelope Write {"envelope": {...}} instead (debug / inspection).
42
- --emit prompt Write the raw hydrated prompt (no JSON wrapper) to stdout.
43
- `;
44
-
45
- /**
46
- * Build the normalized task object the hydration engine expects from a
47
- * full ticket fetched via the provider. Persona and skills come from the
48
- * `persona::*` and `skill::*` labels.
49
- */
50
- export function ticketToTask(ticket) {
51
- const labels = ticket.labels ?? [];
52
- const persona = labels
53
- .find((l) => l.startsWith('persona::'))
54
- ?.replace('persona::', '');
55
- const skills = labels
56
- .filter((l) => l.startsWith('skill::'))
57
- .map((l) => l.replace('skill::', ''));
58
-
59
- return {
60
- id: ticket.id ?? ticket.number,
61
- title: ticket.title,
62
- body: ticket.body ?? '',
63
- labels,
64
- persona,
65
- skills,
66
- };
67
- }
68
-
69
- /**
70
- * Core: build the hydrated prompt and return the MCP-compatible envelope.
71
- * Exported so tests can pin parity against direct SDK invocation without a
72
- * subprocess.
73
- */
74
- export async function runHydrateContext({ ticketId, epicId, provider }) {
75
- const ticket = await provider.getTicket(ticketId);
76
- const hierarchy = parseHierarchy(ticket.body ?? '');
77
-
78
- const resolvedEpicId = epicId ?? hierarchy.epic ?? null;
79
-
80
- const storyId =
81
- hierarchy.story ??
82
- hierarchy.parent ??
83
- ticket.id ??
84
- ticket.number ??
85
- ticketId;
86
-
87
- if (!resolvedEpicId) {
88
- throw new Error(
89
- `[hydrate-context] Could not resolve epic id for ticket #${ticketId}; ` +
90
- `pass --epic explicitly or ensure the body contains "Epic: #N".`,
91
- );
92
- }
93
-
94
- const epicBranch = getEpicBranch(resolvedEpicId);
95
- const taskBranch = getStoryBranch(resolvedEpicId, storyId);
96
-
97
- const task = ticketToTask({ ...ticket, id: ticketId });
98
- const envelope = await hydrateContext(
99
- task,
100
- provider,
101
- epicBranch,
102
- taskBranch,
103
- resolvedEpicId,
104
- );
105
- const prompt = envelopeToPrompt(envelope);
106
- return { prompt, envelope };
107
- }
108
-
109
- export function parseArgv(argv) {
110
- const { values } = parseArgs({
111
- args: argv,
112
- options: {
113
- ticket: { type: 'string' },
114
- epic: { type: 'string' },
115
- emit: { type: 'string' },
116
- help: { type: 'boolean' },
117
- },
118
- strict: false,
119
- });
120
- return values;
121
- }
122
-
123
- /**
124
- * Pure: classify parsed CLI values into a runnable intent. Pulling this
125
- * decision out of `main` keeps the side-effecting wrapper at CC ≤ 2 and
126
- * lets the unit tests exercise every branch directly.
127
- *
128
- * Shapes:
129
- * - { kind: 'help' }
130
- * - { kind: 'usage-error', message }
131
- * - { kind: 'run', ticketId, epicId | undefined, emit?: 'envelope' | 'prompt' }
132
- */
133
- export function classifyCliInvocation(values) {
134
- if (values?.help) return { kind: 'help' };
135
- const ticketId = Number.parseInt(values?.ticket ?? '', 10);
136
- if (!Number.isFinite(ticketId) || ticketId <= 0) {
137
- return {
138
- kind: 'usage-error',
139
- message: `[hydrate-context] --ticket <id> is required.\n${HELP}`,
140
- };
141
- }
142
- const epicId = values?.epic ? Number.parseInt(values.epic, 10) : undefined;
143
- const intent = { kind: 'run', ticketId, epicId };
144
- if (values?.emit === 'envelope' || values?.emit === 'prompt') {
145
- intent.emit = values.emit;
146
- }
147
- return intent;
148
- }
149
-
150
- export async function main(argv = process.argv.slice(2)) {
151
- const intent = classifyCliInvocation(parseArgv(argv));
152
- if (intent.kind === 'help') {
153
- process.stdout.write(HELP);
154
- return;
155
- }
156
- if (intent.kind === 'usage-error') {
157
- process.stderr.write(intent.message);
158
- process.exit(2);
159
- }
160
-
161
- const config = resolveConfig();
162
- const provider = createProvider(config);
163
- const result = await runHydrateContext({
164
- ticketId: intent.ticketId,
165
- epicId: intent.epicId,
166
- provider,
167
- });
168
- if (intent.emit === 'prompt') {
169
- process.stdout.write(result.prompt);
170
- return;
171
- }
172
- const stdoutPayload =
173
- intent.emit === 'envelope'
174
- ? { envelope: result.envelope }
175
- : { prompt: result.prompt };
176
- process.stdout.write(`${JSON.stringify(stdoutPayload)}\n`);
177
- }
178
-
179
- runAsCli(import.meta.url, main, { source: 'hydrate-context' });