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.
- 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/{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 +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/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/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 +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 +5 -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 +13 -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
|
@@ -41,6 +41,16 @@
|
|
|
41
41
|
* node .agents/scripts/analyze-execution.js --story <sid> --epic <eid> \
|
|
42
42
|
* [--phase-timings <path>]
|
|
43
43
|
* node .agents/scripts/analyze-execution.js --epic <eid>
|
|
44
|
+
* node .agents/scripts/analyze-execution.js --epic <eid> --plan-metrics-json
|
|
45
|
+
*
|
|
46
|
+
* `--plan-metrics-json` (Epic #4474 PR7 — the G2 extraction surface) is a
|
|
47
|
+
* **local read-only** mode: it reads the per-Epic plan-metrics ledger
|
|
48
|
+
* (`temp/epic-<eid>/plan-metrics.json`), rolls it up with
|
|
49
|
+
* `summarizePlanMetrics`, and prints one compact JSON envelope
|
|
50
|
+
* `{ epicId, planMetrics, summaryLine }` to stdout — no provider call, no
|
|
51
|
+
* comment write, all logs on stderr — so a bench cohort can extract
|
|
52
|
+
* turns-per-plan / per-mode invocation counts / critic skips without
|
|
53
|
+
* scraping GitHub or the human log.
|
|
44
54
|
*
|
|
45
55
|
* @see docs/data-dictionary.md §StoryPerfSummary, §EpicPerfReport
|
|
46
56
|
*/
|
|
@@ -51,7 +61,7 @@ import { parseArgs } from 'node:util';
|
|
|
51
61
|
import { runAsCli } from './lib/cli-utils.js';
|
|
52
62
|
import { signalsFile } from './lib/config/temp-paths.js';
|
|
53
63
|
import { PROJECT_ROOT, resolveConfig } from './lib/config-resolver.js';
|
|
54
|
-
import { Logger } from './lib/Logger.js';
|
|
64
|
+
import { Logger, routeAllOutputToStderr } from './lib/Logger.js';
|
|
55
65
|
import { computeBaselineRefreshRate } from './lib/observability/baseline-refresh-rate.js';
|
|
56
66
|
import {
|
|
57
67
|
computeEpicPerfReport,
|
|
@@ -72,6 +82,11 @@ import {
|
|
|
72
82
|
renderStoryBody,
|
|
73
83
|
STORY_PERF_TYPE,
|
|
74
84
|
} from './lib/observability/perf-report-render.js';
|
|
85
|
+
import {
|
|
86
|
+
readPlanMetrics,
|
|
87
|
+
renderPlanMetricsSummaryLine,
|
|
88
|
+
summarizePlanMetrics,
|
|
89
|
+
} from './lib/orchestration/plan-metrics.js';
|
|
75
90
|
import { upsertStructuredComment } from './lib/orchestration/ticketing.js';
|
|
76
91
|
import { createProvider } from './lib/provider-factory.js';
|
|
77
92
|
|
|
@@ -284,11 +299,34 @@ export async function runEpicMode(ctx) {
|
|
|
284
299
|
logger.info?.(
|
|
285
300
|
`[analyze-execution] epic-perf-report upserted on Epic #${epicId} (commentId=${result.commentId}, stories=${summaries.length})`,
|
|
286
301
|
);
|
|
302
|
+
|
|
303
|
+
// Plan-metrics roll-up (#4474 PR1) — surface the plan-CLI invocation
|
|
304
|
+
// ledger (`temp/epic-<id>/plan-metrics.json`) alongside the perf report.
|
|
305
|
+
// Additive and non-fatal: a missing or unreadable ledger yields `null`.
|
|
306
|
+
let planMetrics = null;
|
|
307
|
+
try {
|
|
308
|
+
planMetrics = summarizePlanMetrics(
|
|
309
|
+
await readPlanMetrics(epicId, ctx.config),
|
|
310
|
+
);
|
|
311
|
+
if (planMetrics) {
|
|
312
|
+
logger.info?.(
|
|
313
|
+
`[analyze-execution] ${renderPlanMetricsSummaryLine(planMetrics)}`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
} catch (err) {
|
|
317
|
+
logger.warn?.(
|
|
318
|
+
`[analyze-execution] plan-metrics read failed (non-fatal): ${
|
|
319
|
+
err instanceof Error ? err.message : String(err)
|
|
320
|
+
}`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
287
324
|
return {
|
|
288
325
|
commentId: result.commentId,
|
|
289
326
|
payload,
|
|
290
327
|
baselineRefreshRate,
|
|
291
328
|
qualityGateFriction,
|
|
329
|
+
planMetrics,
|
|
292
330
|
};
|
|
293
331
|
}
|
|
294
332
|
|
|
@@ -300,6 +338,7 @@ function parseCli(argv) {
|
|
|
300
338
|
epic: { type: 'string' },
|
|
301
339
|
'phase-timings': { type: 'string' },
|
|
302
340
|
'window-days': { type: 'string' },
|
|
341
|
+
'plan-metrics-json': { type: 'boolean', default: false },
|
|
303
342
|
cwd: { type: 'string' },
|
|
304
343
|
},
|
|
305
344
|
strict: false,
|
|
@@ -316,10 +355,33 @@ function parseCli(argv) {
|
|
|
316
355
|
phaseTimingsPath: values['phase-timings'] ?? null,
|
|
317
356
|
windowDays:
|
|
318
357
|
Number.isInteger(windowDays) && windowDays > 0 ? windowDays : null,
|
|
358
|
+
planMetricsJson: values['plan-metrics-json'] === true,
|
|
319
359
|
cwd: values.cwd ?? null,
|
|
320
360
|
};
|
|
321
361
|
}
|
|
322
362
|
|
|
363
|
+
/**
|
|
364
|
+
* `--plan-metrics-json` — local, read-only G2 extraction surface (Epic
|
|
365
|
+
* #4474 PR7). Reads the per-Epic plan-metrics ledger and prints one
|
|
366
|
+
* compact JSON envelope to stdout. No provider, no comment write; stdout
|
|
367
|
+
* is reserved for the envelope (all logs on stderr). A missing ledger is
|
|
368
|
+
* not an error — it yields `planMetrics: null` so a cohort script can
|
|
369
|
+
* branch on the field instead of the exit code.
|
|
370
|
+
*
|
|
371
|
+
* @param {{ epicId: number, config: object }} input
|
|
372
|
+
* @returns {Promise<{ epicId: number, planMetrics: object|null, summaryLine: string }>}
|
|
373
|
+
*/
|
|
374
|
+
export async function runPlanMetricsJsonMode({ epicId, config }) {
|
|
375
|
+
const planMetrics = summarizePlanMetrics(
|
|
376
|
+
await readPlanMetrics(epicId, config),
|
|
377
|
+
);
|
|
378
|
+
return {
|
|
379
|
+
epicId,
|
|
380
|
+
planMetrics,
|
|
381
|
+
summaryLine: renderPlanMetricsSummaryLine(planMetrics),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
323
385
|
async function main(argv = process.argv.slice(2)) {
|
|
324
386
|
const args = parseCli(argv);
|
|
325
387
|
|
|
@@ -331,6 +393,19 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
331
393
|
|
|
332
394
|
const cwd = path.resolve(args.cwd ?? PROJECT_ROOT);
|
|
333
395
|
const config = resolveConfig({ cwd });
|
|
396
|
+
|
|
397
|
+
if (args.planMetricsJson) {
|
|
398
|
+
// Stdout is reserved for the compact envelope — flip every log sink to
|
|
399
|
+
// stderr before any pipeline code runs (Story #2278 discipline).
|
|
400
|
+
routeAllOutputToStderr();
|
|
401
|
+
const result = await runPlanMetricsJsonMode({
|
|
402
|
+
epicId: args.epicId,
|
|
403
|
+
config,
|
|
404
|
+
});
|
|
405
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
334
409
|
const provider = createProvider(config);
|
|
335
410
|
|
|
336
411
|
if (args.storyId) {
|
|
@@ -50,6 +50,9 @@ export const RETIRED_COMMANDS = new Set([
|
|
|
50
50
|
'agents-bootstrap-github',
|
|
51
51
|
'single-story-plan',
|
|
52
52
|
'mandrel',
|
|
53
|
+
// #4482 — dead workflow surface retired (host-native equivalents).
|
|
54
|
+
'explain',
|
|
55
|
+
'git-merge-pr',
|
|
53
56
|
]);
|
|
54
57
|
|
|
55
58
|
// Tokens that look like `/foo` in prose but are not slash commands. Tokens
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* node:coverage ignore file */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* check-workflow-cli-lint.js — enforce the "no workflow instructs a
|
|
6
|
+
* no-CLI library call" rule (Epic #4474 PR5; sibling of
|
|
7
|
+
* check-lifecycle-lint.js).
|
|
8
|
+
*
|
|
9
|
+
* The measured failure mode this rule kills: workflow prose that tells the
|
|
10
|
+
* host LLM to "Call `someExportedFunction({...})` exported from
|
|
11
|
+
* `lib/whatever.js`". There is no runnable form of that instruction, so the
|
|
12
|
+
* model greps the framework source and writes throwaway `.mjs` shims to
|
|
13
|
+
* invoke the export — the mandrel-bench N=2 cohort measured ~12–15 turns of
|
|
14
|
+
* shim-writing per plan for exactly this pattern. Workflows must instruct
|
|
15
|
+
* `node .agents/scripts/<cli>.js …` commands instead.
|
|
16
|
+
*
|
|
17
|
+
* Scope: every `*.md` under `.agents/workflows/`.
|
|
18
|
+
*
|
|
19
|
+
* Heuristic (tuned to zero false positives on the surviving corpus —
|
|
20
|
+
* descriptive mentions like "the automatic paths call `foo()`" are prose
|
|
21
|
+
* about script internals, not instructions, and are NOT flagged):
|
|
22
|
+
*
|
|
23
|
+
* Rule 1 — imperative library call. A paragraph (fenced code blocks
|
|
24
|
+
* stripped; lines joined) matching /\b(Call|Invoke)\s+`ident\s*\(/ —
|
|
25
|
+
* a capitalized imperative directly instructing a function call.
|
|
26
|
+
*
|
|
27
|
+
* Rule 2 — "exported from" instruction. A paragraph containing both a
|
|
28
|
+
* backticked call token (`ident(`) and the phrase "exported from" —
|
|
29
|
+
* the canonical shape of the retired Phase 2/3/4 prose.
|
|
30
|
+
*
|
|
31
|
+
* Rule 3 — prose-level lib import. `import(` / `require(` naming a
|
|
32
|
+
* `scripts/lib/` path OUTSIDE a fenced code block. (A complete,
|
|
33
|
+
* runnable `node -e` one-liner inside a fenced block is exempt: it
|
|
34
|
+
* costs zero shim-writing turns because it is executable as written.)
|
|
35
|
+
*
|
|
36
|
+
* Exit codes:
|
|
37
|
+
* 0 — clean.
|
|
38
|
+
* 1 — at least one violation; offending file + paragraph line printed
|
|
39
|
+
* to stderr.
|
|
40
|
+
*
|
|
41
|
+
* Ships as part of `npm run lint` (run-lint.js task list), alongside the
|
|
42
|
+
* lifecycle lint and the label-vocabulary lint.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
46
|
+
import path from 'node:path';
|
|
47
|
+
import { fileURLToPath } from 'node:url';
|
|
48
|
+
|
|
49
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
50
|
+
|
|
51
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
52
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
53
|
+
export const DEFAULT_WORKFLOWS_DIR = path.join(
|
|
54
|
+
REPO_ROOT,
|
|
55
|
+
'.agents',
|
|
56
|
+
'workflows',
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const IMPERATIVE_CALL_RE = /\b(?:Call|Invoke)\s+`[A-Za-z_$][\w$]*\s*\(/;
|
|
60
|
+
const BACKTICK_CALL_RE = /`[A-Za-z_$][\w$]*\s*\(/;
|
|
61
|
+
const EXPORTED_FROM_RE = /\bexported from\b/;
|
|
62
|
+
const LIB_IMPORT_RE = /\b(?:import|require)\(\s*['"`][^'"`]*scripts\/lib\//;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Strip fenced code blocks (``` / ~~~), replacing their lines with empty
|
|
66
|
+
* strings so line numbers stay stable. Complete runnable commands live in
|
|
67
|
+
* fences and are exempt by design (see header).
|
|
68
|
+
*
|
|
69
|
+
* @param {string} source
|
|
70
|
+
* @returns {string[]} lines with fenced content blanked.
|
|
71
|
+
*/
|
|
72
|
+
export function stripFences(source) {
|
|
73
|
+
const lines = source.split('\n');
|
|
74
|
+
let inFence = false;
|
|
75
|
+
return lines.map((line) => {
|
|
76
|
+
const fence = /^\s*(```|~~~)/.test(line);
|
|
77
|
+
if (fence) {
|
|
78
|
+
inFence = !inFence;
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
return inFence ? '' : line;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Split blanked lines into paragraphs — runs of consecutive non-empty
|
|
87
|
+
* lines — keeping the 1-based line number of each paragraph's first line.
|
|
88
|
+
*
|
|
89
|
+
* @param {string[]} lines
|
|
90
|
+
* @returns {Array<{ text: string, line: number }>}
|
|
91
|
+
*/
|
|
92
|
+
export function toParagraphs(lines) {
|
|
93
|
+
const paragraphs = [];
|
|
94
|
+
let buf = [];
|
|
95
|
+
let start = 0;
|
|
96
|
+
for (let i = 0; i < lines.length; i++) {
|
|
97
|
+
if (lines[i].trim().length > 0) {
|
|
98
|
+
if (buf.length === 0) start = i + 1;
|
|
99
|
+
buf.push(lines[i]);
|
|
100
|
+
} else if (buf.length > 0) {
|
|
101
|
+
paragraphs.push({ text: buf.join(' '), line: start });
|
|
102
|
+
buf = [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (buf.length > 0) paragraphs.push({ text: buf.join(' '), line: start });
|
|
106
|
+
return paragraphs;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Lint one markdown source. Returns violations
|
|
111
|
+
* `{ rule, line, hint }[]` (empty when clean).
|
|
112
|
+
*
|
|
113
|
+
* @param {string} source markdown content.
|
|
114
|
+
* @returns {Array<{ rule: string, line: number, hint: string }>}
|
|
115
|
+
*/
|
|
116
|
+
export function lintWorkflowSource(source) {
|
|
117
|
+
const violations = [];
|
|
118
|
+
const lines = stripFences(source);
|
|
119
|
+
for (const para of toParagraphs(lines)) {
|
|
120
|
+
if (IMPERATIVE_CALL_RE.test(para.text)) {
|
|
121
|
+
violations.push({
|
|
122
|
+
rule: 'no-cli-library-call',
|
|
123
|
+
line: para.line,
|
|
124
|
+
hint:
|
|
125
|
+
'Workflow prose instructs calling a function directly ("Call/Invoke `fn(...)`"). ' +
|
|
126
|
+
'There is no runnable form of that instruction — the model must write a throwaway shim. ' +
|
|
127
|
+
'Instruct a `node .agents/scripts/<cli>.js …` command instead (add a CLI if none exists).',
|
|
128
|
+
});
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (EXPORTED_FROM_RE.test(para.text) && BACKTICK_CALL_RE.test(para.text)) {
|
|
132
|
+
violations.push({
|
|
133
|
+
rule: 'no-cli-library-call',
|
|
134
|
+
line: para.line,
|
|
135
|
+
hint:
|
|
136
|
+
'Workflow prose points at an exported library function ("`fn(...)` exported from …") ' +
|
|
137
|
+
'with no CLI entrypoint. Instruct a `node .agents/scripts/<cli>.js …` command instead.',
|
|
138
|
+
});
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (LIB_IMPORT_RE.test(para.text)) {
|
|
142
|
+
violations.push({
|
|
143
|
+
rule: 'no-prose-lib-import',
|
|
144
|
+
line: para.line,
|
|
145
|
+
hint:
|
|
146
|
+
'Workflow prose (outside a fenced code block) instructs importing a scripts/lib module. ' +
|
|
147
|
+
'Give a complete runnable command in a fenced block, or add a CLI entrypoint.',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return violations;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Recursively collect `*.md` files under a directory.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} dir
|
|
158
|
+
* @returns {string[]} absolute paths.
|
|
159
|
+
*/
|
|
160
|
+
export function collectMarkdown(dir) {
|
|
161
|
+
const out = [];
|
|
162
|
+
for (const entry of readdirSync(dir)) {
|
|
163
|
+
const abs = path.join(dir, entry);
|
|
164
|
+
const st = statSync(abs);
|
|
165
|
+
if (st.isDirectory()) out.push(...collectMarkdown(abs));
|
|
166
|
+
else if (entry.endsWith('.md')) out.push(abs);
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Run the check over a workflows directory. Exported for tests (pass a
|
|
173
|
+
* fixture directory).
|
|
174
|
+
*
|
|
175
|
+
* @param {string} [workflowsDir]
|
|
176
|
+
* @returns {Array<{ file: string, rule: string, line: number, hint: string }>}
|
|
177
|
+
*/
|
|
178
|
+
export function runCheck(workflowsDir = DEFAULT_WORKFLOWS_DIR) {
|
|
179
|
+
const violations = [];
|
|
180
|
+
for (const file of collectMarkdown(workflowsDir)) {
|
|
181
|
+
const source = readFileSync(file, 'utf8');
|
|
182
|
+
for (const v of lintWorkflowSource(source)) {
|
|
183
|
+
violations.push({ file: path.relative(REPO_ROOT, file), ...v });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return violations;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function main() {
|
|
190
|
+
const violations = runCheck();
|
|
191
|
+
if (violations.length === 0) {
|
|
192
|
+
process.stdout.write(
|
|
193
|
+
'[workflow-cli-lint] clean: no workflow instructs a no-CLI library call.\n',
|
|
194
|
+
);
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
for (const v of violations) {
|
|
198
|
+
process.stderr.write(
|
|
199
|
+
`[workflow-cli-lint][${v.rule}] ${v.file}:${v.line}\n ${v.hint}\n`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
await runAsCli(import.meta.url, main, {
|
|
206
|
+
source: 'check-workflow-cli-lint',
|
|
207
|
+
propagateExitCode: true,
|
|
208
|
+
});
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* `.agents/workflows/helpers/code-review.md` Step 4.6) and emit the
|
|
10
10
|
* subset of audit lenses whose `filePatterns` overlap that list.
|
|
11
11
|
*
|
|
12
|
-
* The CLI is intentionally narrower than `
|
|
12
|
+
* The CLI is intentionally narrower than `lib/audit-suite/selector.js`: it does NOT
|
|
13
13
|
* run a git diff and does NOT consult the Epic ticket body for keyword
|
|
14
14
|
* triggers. The auto-fix tail already knows exactly which paths it touched —
|
|
15
15
|
* only file-pattern overlap is relevant for deciding which lenses are stale
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from './lib/config-resolver.js';
|
|
35
35
|
import { scoreEpicBody } from './lib/epic-plan-clarity.js';
|
|
36
36
|
import { Logger, routeAllOutputToStderr } from './lib/Logger.js';
|
|
37
|
+
import { recordPlanInvocation } from './lib/orchestration/plan-metrics.js';
|
|
37
38
|
import { upsertStructuredComment } from './lib/orchestration/ticketing.js';
|
|
38
39
|
import { createProvider } from './lib/provider-factory.js';
|
|
39
40
|
|
|
@@ -175,7 +176,11 @@ async function main() {
|
|
|
175
176
|
// additions cannot silently corrupt the captured file.
|
|
176
177
|
if (values['emit-context']) {
|
|
177
178
|
routeAllOutputToStderr();
|
|
178
|
-
|
|
179
|
+
// Plan-metrics ledger (#4474 PR1): stamp entry/exit + mode.
|
|
180
|
+
const envelope = await recordPlanInvocation(
|
|
181
|
+
{ cli: 'epic-plan-clarity', mode: 'emit-context', epicId, config },
|
|
182
|
+
() => buildClarityContext({ epicId, provider }),
|
|
183
|
+
);
|
|
179
184
|
const json = values.pretty
|
|
180
185
|
? JSON.stringify(envelope, null, 2)
|
|
181
186
|
: JSON.stringify(envelope);
|
|
@@ -190,11 +195,16 @@ async function main() {
|
|
|
190
195
|
}
|
|
191
196
|
|
|
192
197
|
const updatedBody = await readFile(values['updated-body'], 'utf8');
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
+
// Plan-metrics ledger (#4474 PR1): stamp entry/exit + mode.
|
|
199
|
+
const result = await recordPlanInvocation(
|
|
200
|
+
{ cli: 'epic-plan-clarity', mode: 'persist', epicId, config },
|
|
201
|
+
() =>
|
|
202
|
+
persistClarityUpdate({
|
|
203
|
+
epicId,
|
|
204
|
+
updatedBody,
|
|
205
|
+
provider,
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
198
208
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
199
209
|
}
|
|
200
210
|
|
|
@@ -1,76 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
|
|
2
3
|
/* node:coverage ignore file */
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* epic-plan-decompose.js —
|
|
6
|
-
*
|
|
7
|
-
* Phase 8 (decompose) entry point for the split planning flow. The
|
|
8
|
-
* deterministic decomposer engine and reconciler-based persist flow
|
|
9
|
-
* have been split into phase modules under
|
|
10
|
-
* `lib/orchestration/epic-plan-decompose/phases/`:
|
|
11
|
-
*
|
|
12
|
-
* 1. planning-artifacts — `resolveConflictPolicy`.
|
|
13
|
-
* 2. dag — `resolveDependencies`,
|
|
14
|
-
* `orderTicketsForCreation`.
|
|
15
|
-
* 3. context — `buildDecomposerSystemPrompt`,
|
|
16
|
-
* `buildDecompositionContext`.
|
|
17
|
-
* 4. creation — sub-issue link reconciliation, Epic label
|
|
18
|
-
* transitions, and the advisory ticket-cap
|
|
19
|
-
* warning (`reconcileSubIssueLinks`,
|
|
20
|
-
* `setEpicLabel`, `warnTicketCapNearLimit`).
|
|
21
|
-
* 5. persist — `runDecomposePhase` (reconciler-based flow).
|
|
22
|
-
* 6. cli — argument parsing + `main()` pipeline.
|
|
23
|
-
*
|
|
24
|
-
* Modes:
|
|
25
|
-
* --emit-context Prints the decomposer authoring context (Epic body,
|
|
26
|
-
* Tech Spec body, risk heuristics, system prompt,
|
|
27
|
-
* ticket cap) as JSON. The authoring middle is the
|
|
28
|
-
* `epic-plan-decompose-author` Skill
|
|
29
|
-
* (`.agents/skills/core/epic-plan-decompose-author/SKILL.md`).
|
|
6
|
+
* epic-plan-decompose.js — RETIRED delegate CLI (Epic #4474, PR7).
|
|
30
7
|
*
|
|
31
|
-
*
|
|
32
|
-
* persists the Feature/Story/Task hierarchy via the
|
|
33
|
-
* structural reconciler, flips the Epic to
|
|
34
|
-
* `agent::ready`, and updates the `epic-plan-state`
|
|
35
|
-
* structured comment.
|
|
8
|
+
* The 12-phase plan pipeline collapsed to context → author → persist:
|
|
36
9
|
*
|
|
37
|
-
*
|
|
10
|
+
* - `--emit-context` moved to `plan-context.js` (the single authoring
|
|
11
|
+
* envelope carries the decomposer context — ticket schema, risk
|
|
12
|
+
* heuristics, ticket cap — alongside the spec half).
|
|
13
|
+
* - The persist half (ticket validator, file-assumption gate, DAG,
|
|
14
|
+
* budget, story creation, healthcheck, `agent::ready` flip) moved to
|
|
15
|
+
* `plan-persist.js` (single GitHub-write surface).
|
|
38
16
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
17
|
+
* This file is a **re-export shim only** — it carries external importers of
|
|
18
|
+
* the historic named-export surface one more release (#4474 design §6 PR7
|
|
19
|
+
* risk note) and is deleted in the next release. Internal consumers import
|
|
20
|
+
* the phase modules directly; do not add new imports of this file.
|
|
42
21
|
*
|
|
43
|
-
*
|
|
44
|
-
* refactor implementation.
|
|
22
|
+
* Invoking it as a CLI is refused with a pointer to the successor CLIs.
|
|
45
23
|
*/
|
|
46
24
|
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
import '
|
|
51
|
-
import { runAsCli } from './lib/cli-utils.js';
|
|
52
|
-
import { main } from './lib/orchestration/epic-plan-decompose/phases/cli.js';
|
|
53
|
-
import {
|
|
54
|
-
buildDecomposerSystemPrompt,
|
|
55
|
-
buildDecompositionContext,
|
|
56
|
-
} from './lib/orchestration/epic-plan-decompose/phases/context.js';
|
|
57
|
-
import {
|
|
58
|
-
orderTicketsForCreation,
|
|
59
|
-
resolveDependencies,
|
|
60
|
-
} from './lib/orchestration/epic-plan-decompose/phases/dag.js';
|
|
61
|
-
import { runDecomposePhase } from './lib/orchestration/epic-plan-decompose/phases/persist.js';
|
|
25
|
+
// cli-opt-out: retired delegate shim (Epic #4474 PR7) — deliberately
|
|
26
|
+
// refuses CLI execution with a pointer to plan-context.js/plan-persist.js
|
|
27
|
+
// instead of wiring runAsCli around a dead main().
|
|
28
|
+
import { pathToFileURL } from 'node:url';
|
|
62
29
|
|
|
63
|
-
// Named exports preserved for the existing test surface. The pre-refactor
|
|
64
|
-
// module published these and the consumers (`tests/ticket-decomposer.test.js`,
|
|
65
|
-
// `tests/scripts/epic-plan-decompose.body-preservation.test.js`,
|
|
66
|
-
// `tests/scripts/epic-plan-decompose.sub-issue-safety-net.test.js`,
|
|
67
|
-
// `tests/scripts/epic-plan.spec-flow.test.js`) still depend on them.
|
|
68
30
|
export {
|
|
69
31
|
buildDecomposerSystemPrompt,
|
|
70
32
|
buildDecompositionContext,
|
|
33
|
+
} from './lib/orchestration/epic-plan-decompose/phases/context.js';
|
|
34
|
+
export {
|
|
71
35
|
orderTicketsForCreation,
|
|
72
36
|
resolveDependencies,
|
|
73
|
-
|
|
74
|
-
};
|
|
37
|
+
} from './lib/orchestration/epic-plan-decompose/phases/dag.js';
|
|
38
|
+
export { runDecomposePhase } from './lib/orchestration/epic-plan-decompose/phases/persist.js';
|
|
75
39
|
|
|
76
|
-
|
|
40
|
+
// CLI execution is retired — fail loudly with the successor surface instead
|
|
41
|
+
// of silently doing nothing (a stale automation script should break visibly).
|
|
42
|
+
if (
|
|
43
|
+
process.argv[1] &&
|
|
44
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
45
|
+
) {
|
|
46
|
+
process.stderr.write(
|
|
47
|
+
'[epic-plan-decompose] retired (Epic #4474): the plan pipeline is ' +
|
|
48
|
+
'context → author → persist.\n' +
|
|
49
|
+
' - authoring envelope: node .agents/scripts/plan-context.js --epic <id>\n' +
|
|
50
|
+
' - persist (all gates): node .agents/scripts/plan-persist.js --epic <id> --tickets ...\n' +
|
|
51
|
+
'This file survives one release as an import shim only.\n',
|
|
52
|
+
);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* epic-plan-healthcheck.js — Post-Plan Readiness Check
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Library home of `runPlanHealthcheck`, the **inline** blocking check the
|
|
9
|
+
* collapsed persist surface runs as the `agent::ready` exit condition
|
|
10
|
+
* (`plan-persist.js` step 9 — the authoritative invocation since Epic
|
|
11
|
+
* #4474 retired the standalone Phase 10 workflow step). The CLI form
|
|
12
|
+
* remains for manual re-validation and the opt-in slow paths below; no
|
|
13
|
+
* workflow mandates running it standalone.
|
|
11
14
|
*
|
|
12
15
|
* Modes (additive — the fast checks below always run):
|
|
13
16
|
* (default) — config validation + git remote check only.
|
|
@@ -30,7 +33,7 @@
|
|
|
30
33
|
* node epic-plan-healthcheck.js --epic <EPIC_ID> \
|
|
31
34
|
* [--paranoid] [--prime-install] [--dry-run]
|
|
32
35
|
*
|
|
33
|
-
* @see .agents/workflows/helpers/plan-epic.md
|
|
36
|
+
* @see .agents/workflows/helpers/plan-epic.md (persist step — inline gate)
|
|
34
37
|
*/
|
|
35
38
|
|
|
36
39
|
import { spawnSync } from 'node:child_process';
|
|
@@ -47,6 +50,7 @@ import {
|
|
|
47
50
|
import { gitSpawn } from './lib/git-utils.js';
|
|
48
51
|
import { Logger } from './lib/Logger.js';
|
|
49
52
|
import { TYPE_LABELS } from './lib/label-constants.js';
|
|
53
|
+
import { recordPlanInvocation } from './lib/orchestration/plan-metrics.js';
|
|
50
54
|
import { createProvider } from './lib/provider-factory.js';
|
|
51
55
|
|
|
52
56
|
const progress = Logger.createProgress('plan-healthcheck', { stderr: true });
|
|
@@ -454,7 +458,11 @@ async function timed(name, fn) {
|
|
|
454
458
|
* checks: Array<{name: string, ok: boolean, durationMs: number, detail: string}>}>}
|
|
455
459
|
*/
|
|
456
460
|
// exported for tests — direct-unit coverage of the reachability semantics.
|
|
457
|
-
|
|
461
|
+
// (`resolveNavConfig` / `extractStoryPaths` / `globToRegExp` are also the
|
|
462
|
+
// shared mechanics of the persist-side draft reachability check —
|
|
463
|
+
// `lib/orchestration/plan-reachability.js`, Epic #4474 PR6 — so the two
|
|
464
|
+
// surfaces can never drift apart.)
|
|
465
|
+
export { checkReachability, extractStoryPaths, globToRegExp, resolveNavConfig };
|
|
458
466
|
|
|
459
467
|
// exported for tests — Story-level reuse runner reserved for future test coverage
|
|
460
468
|
export async function runPlanHealthcheck(opts = {}) {
|
|
@@ -543,6 +551,31 @@ export async function runPlanHealthcheck(opts = {}) {
|
|
|
543
551
|
// Main guard
|
|
544
552
|
// ---------------------------------------------------------------------------
|
|
545
553
|
|
|
546
|
-
|
|
554
|
+
/**
|
|
555
|
+
* CLI wrapper: stamp the plan-metrics ledger (#4474 PR1) around the direct
|
|
556
|
+
* invocation only. In-process callers (the decompose persist gate imports
|
|
557
|
+
* `runPlanHealthcheck` directly) are already covered by their own CLI's
|
|
558
|
+
* stamp — wrapping here too would double-count the inline gate.
|
|
559
|
+
*/
|
|
560
|
+
async function cliMain() {
|
|
561
|
+
const { epicId } = parseHealthcheckArgs();
|
|
562
|
+
let config;
|
|
563
|
+
try {
|
|
564
|
+
config = resolveConfig();
|
|
565
|
+
} catch {
|
|
566
|
+
config = undefined;
|
|
567
|
+
}
|
|
568
|
+
return recordPlanInvocation(
|
|
569
|
+
{
|
|
570
|
+
cli: 'epic-plan-healthcheck',
|
|
571
|
+
mode: 'healthcheck',
|
|
572
|
+
epicId: epicId ?? null,
|
|
573
|
+
config,
|
|
574
|
+
},
|
|
575
|
+
() => runPlanHealthcheck(),
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
runAsCli(import.meta.url, cliMain, {
|
|
547
580
|
source: 'epic-plan-healthcheck',
|
|
548
581
|
});
|