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
|
@@ -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
|
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
* `storyCount`, `installCostSeconds`, `dependencyDepth`,
|
|
12
12
|
* `githubApiRequests`, `claudeQuotaTokens`, plus `breaches`
|
|
13
13
|
* (the non-empty subset of `delivery.preflight.max*` thresholds the
|
|
14
|
-
* estimate exceeds)
|
|
14
|
+
* estimate exceeds), plus `remoteVerified` / `remoteProbe` — the
|
|
15
|
+
* issue #4483 deterministic remote evidence (`git remote get-url
|
|
16
|
+
* origin` + bounded `git ls-remote origin HEAD`). On
|
|
17
|
+
* `remoteVerified: false` the workflow MUST flip the Epic to
|
|
18
|
+
* `agent::blocked` quoting `remoteProbe.detail` — inline delivery to
|
|
19
|
+
* local `main` is never a sanctioned fallback.
|
|
15
20
|
* 2. When `--post` is set (and `--dry-run` is not), an upserted
|
|
16
21
|
* `delivery-preflight` structured comment on the Epic ticket so
|
|
17
22
|
* reviewers reading the Epic discover the same numbers without
|
|
@@ -66,6 +71,7 @@ import {
|
|
|
66
71
|
computeBaseSha,
|
|
67
72
|
writePreflightCache,
|
|
68
73
|
} from './lib/orchestration/preflight-cache.js';
|
|
74
|
+
import { verifyRemote } from './lib/orchestration/remote-verifier.js';
|
|
69
75
|
import { upsertStructuredComment } from './lib/orchestration/ticketing.js';
|
|
70
76
|
import { createProvider } from './lib/provider-factory.js';
|
|
71
77
|
|
|
@@ -218,10 +224,23 @@ export function renderPreflightBody({
|
|
|
218
224
|
estimate,
|
|
219
225
|
breaches,
|
|
220
226
|
thresholds,
|
|
227
|
+
remote,
|
|
221
228
|
}) {
|
|
222
229
|
const lines = [];
|
|
223
230
|
lines.push(`### 🛫 Delivery preflight — Epic #${epicId}`);
|
|
224
231
|
lines.push('');
|
|
232
|
+
// Issue #4483 — verified remote evidence at entry. Rendered before the
|
|
233
|
+
// metric table so a reviewer (and the orchestrating agent) sees the
|
|
234
|
+
// land-or-block fact first. Omitted when the caller has no probe result
|
|
235
|
+
// (legacy callers / tests that only exercise the estimate math).
|
|
236
|
+
if (remote) {
|
|
237
|
+
lines.push(
|
|
238
|
+
remote.remoteVerified
|
|
239
|
+
? `✅ **remoteVerified: true** — ${remote.detail}`
|
|
240
|
+
: `⛔ **remoteVerified: false** — ${remote.detail} — \`/deliver\` MUST transition the Epic to \`agent::blocked\` quoting this probe; inline delivery to local \`main\` is forbidden.`,
|
|
241
|
+
);
|
|
242
|
+
lines.push('');
|
|
243
|
+
}
|
|
225
244
|
lines.push('| Metric | Estimate | Threshold |');
|
|
226
245
|
lines.push('| --- | ---: | ---: |');
|
|
227
246
|
const rows = [
|
|
@@ -276,6 +295,7 @@ export function renderPreflightBody({
|
|
|
276
295
|
* perStoryClaudeTokens?: number,
|
|
277
296
|
* injectedProvider?: object,
|
|
278
297
|
* injectedConfig?: object,
|
|
298
|
+
* verifyRemoteFn?: typeof verifyRemote,
|
|
279
299
|
* }} args
|
|
280
300
|
*/
|
|
281
301
|
export async function runPreflight({
|
|
@@ -288,6 +308,7 @@ export async function runPreflight({
|
|
|
288
308
|
perStoryClaudeTokens,
|
|
289
309
|
injectedProvider,
|
|
290
310
|
injectedConfig,
|
|
311
|
+
verifyRemoteFn = verifyRemote,
|
|
291
312
|
}) {
|
|
292
313
|
if (!Number.isInteger(epicId) || epicId <= 0) {
|
|
293
314
|
throw new TypeError('runPreflight: --epic must be a positive integer');
|
|
@@ -297,6 +318,13 @@ export async function runPreflight({
|
|
|
297
318
|
const provider = injectedProvider ?? createProvider(config);
|
|
298
319
|
const thresholds = getPreflight(config);
|
|
299
320
|
|
|
321
|
+
// Issue #4483 — deterministic remote evidence at entry. Probe BEFORE any
|
|
322
|
+
// provider work so the envelope always carries verified fact (not the
|
|
323
|
+
// agent's perception) about whether a live, pushable `origin` exists.
|
|
324
|
+
// The CLI records; the `/deliver` workflow owns the `agent::blocked`
|
|
325
|
+
// transition on `remoteVerified: false` (same split as breach handling).
|
|
326
|
+
const remote = verifyRemoteFn({ cwd });
|
|
327
|
+
|
|
300
328
|
// Compose the same two phases /deliver Phase 1 runs so the
|
|
301
329
|
// preflight numbers match the actual dispatch plan.
|
|
302
330
|
const ctx = { epicId, provider };
|
|
@@ -348,6 +376,13 @@ export async function runPreflight({
|
|
|
348
376
|
thresholds,
|
|
349
377
|
baseSha,
|
|
350
378
|
cacheWritten,
|
|
379
|
+
// Issue #4483 — verified remote evidence. `remoteVerified: false`
|
|
380
|
+
// REQUIRES the workflow to block explicitly (never build inline).
|
|
381
|
+
remoteVerified: remote.remoteVerified,
|
|
382
|
+
remoteProbe: {
|
|
383
|
+
remoteUrl: remote.remoteUrl,
|
|
384
|
+
detail: remote.detail,
|
|
385
|
+
},
|
|
351
386
|
};
|
|
352
387
|
|
|
353
388
|
if (post && !dryRun) {
|
|
@@ -356,6 +391,7 @@ export async function runPreflight({
|
|
|
356
391
|
estimate,
|
|
357
392
|
breaches,
|
|
358
393
|
thresholds,
|
|
394
|
+
remote,
|
|
359
395
|
});
|
|
360
396
|
await upsertStructuredComment(provider, epicId, 'delivery-preflight', body);
|
|
361
397
|
envelope.commentUpserted = true;
|
|
@@ -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
|
+
}
|