mandrel 1.91.0 → 1.93.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/README.md +5 -5
- package/.agents/docs/SDLC.md +75 -107
- package/.agents/docs/configuration.md +1 -1
- package/.agents/docs/quality-gates.md +8 -7
- package/.agents/docs/workflows.md +4 -10
- package/.agents/instructions.md +2 -2
- package/.agents/schemas/agentrc.schema.json +1 -1
- package/.agents/schemas/lifecycle/README.md +2 -1
- package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
- package/.agents/schemas/risk-verdict.schema.json +14 -1
- package/.agents/scripts/README.md +0 -46
- package/.agents/scripts/analyze-execution.js +76 -1
- package/.agents/scripts/check-doc-links.js +3 -0
- package/.agents/scripts/check-workflow-cli-lint.js +208 -0
- package/.agents/scripts/epic-audit-recheck.js +1 -1
- package/.agents/scripts/epic-plan-clarity.js +16 -6
- package/.agents/scripts/epic-plan-decompose.js +37 -59
- package/.agents/scripts/epic-plan-healthcheck.js +39 -6
- package/.agents/scripts/epic-plan-spec.js +41 -168
- package/.agents/scripts/epic-reconcile.js +7 -1
- package/.agents/scripts/lib/Logger.js +4 -4
- package/.agents/scripts/lib/audit-suite/index.js +4 -5
- package/.agents/scripts/lib/audit-suite/runner.js +6 -4
- package/.agents/scripts/lib/audit-suite/selector.js +2 -3
- package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
- package/.agents/scripts/lib/command-header.js +20 -0
- package/.agents/scripts/lib/config/github.js +1 -3
- package/.agents/scripts/lib/config-settings-schema.js +4 -3
- package/.agents/scripts/lib/degraded-mode.js +1 -1
- package/.agents/scripts/lib/label-constants.js +18 -3
- package/.agents/scripts/lib/label-taxonomy.js +14 -3
- package/.agents/scripts/lib/mutation/baseline-snapshot.js +7 -6
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +18 -8
- package/.agents/scripts/lib/orchestration/context-hydration-engine.js +3 -4
- package/.agents/scripts/lib/orchestration/docs-digest.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +4 -0
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist.js +22 -12
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +6 -1
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +18 -2
- package/.agents/scripts/lib/orchestration/file-assumptions.js +68 -7
- package/.agents/scripts/{lifecycle-diff.js → lib/orchestration/lifecycle/ledger-diff.js} +10 -76
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +1 -1
- package/.agents/scripts/lib/orchestration/plan-context.js +696 -0
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +177 -0
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +99 -0
- package/.agents/scripts/lib/orchestration/plan-metrics.js +445 -0
- package/.agents/scripts/lib/orchestration/plan-persist/amend.js +359 -0
- package/.agents/scripts/lib/orchestration/plan-persist/delivery-mode.js +127 -0
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +1015 -0
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +206 -0
- package/.agents/scripts/lib/orchestration/plan-reachability.js +160 -0
- package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +3 -3
- package/.agents/scripts/lib/orchestration/planning-risk.js +1 -1
- package/.agents/scripts/lib/orchestration/spec-freshness.js +1 -1
- package/.agents/scripts/lib/orchestration/ticket-validator.js +18 -1
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +1 -1
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +11 -2
- package/.agents/scripts/lib/plan-phase-cleanup.js +16 -0
- package/.agents/scripts/lib/presentation/manifest-persistence.js +2 -3
- package/.agents/scripts/lib/templates/spec-author-prompts.js +74 -0
- package/.agents/scripts/plan-context.js +204 -0
- package/.agents/scripts/plan-critics.js +199 -0
- package/.agents/scripts/plan-persist.js +383 -0
- package/.agents/scripts/pr-watch-with-update.js +8 -7
- package/.agents/scripts/run-lint.js +10 -11
- package/.agents/scripts/story-plan.js +19 -7
- package/.agents/scripts/sync-claude-commands.js +10 -5
- package/.agents/skills/core/knowledge-transfer/SKILL.md +11 -7
- package/.agents/workflows/audit-documentation.md +5 -7
- package/.agents/workflows/audit-lighthouse.md +1 -0
- package/.agents/workflows/audit-security.md +1 -0
- package/.agents/workflows/audit-to-stories.md +16 -5
- package/.agents/workflows/git-deliver.md +76 -243
- package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
- package/.agents/workflows/helpers/code-review.md +7 -6
- package/.agents/workflows/helpers/deliver-epic.md +5 -4
- package/.agents/workflows/helpers/plan-epic-reference.md +120 -96
- package/.agents/workflows/helpers/plan-epic.md +307 -994
- package/.agents/workflows/helpers/plan-story.md +14 -14
- package/.agents/workflows/helpers/scope-triage-gate.md +15 -4
- package/.agents/workflows/helpers/single-story-deliver.md +5 -4
- package/.agents/workflows/helpers/worktree-lifecycle.md +9 -7
- package/.agents/workflows/mandrel-update.md +144 -466
- package/.agents/workflows/plan.md +87 -145
- package/docs/CHANGELOG.md +20 -0
- package/lib/cli/registry.js +21 -3
- package/package.json +1 -1
- package/.agents/schemas/loop-unit.schema.json +0 -70
- package/.agents/scripts/assert-branch.js +0 -81
- package/.agents/scripts/check-loop-units.js +0 -204
- package/.agents/scripts/detect-merges.js +0 -111
- package/.agents/scripts/git-pr-quality-gate.js +0 -205
- package/.agents/scripts/git-rebase-and-resolve.js +0 -234
- package/.agents/scripts/hierarchy-gate.js +0 -192
- package/.agents/scripts/hydrate-context.js +0 -179
- package/.agents/scripts/lib/loop-units/validate-loop-unit.js +0 -197
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/cli.js +0 -167
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/cli-args.js +0 -69
- package/.agents/scripts/lib/orchestration/plan-runner/plan-router.js +0 -86
- package/.agents/scripts/loc-delta.js +0 -205
- package/.agents/scripts/render-manifest.js +0 -143
- package/.agents/scripts/run-audit-suite.js +0 -97
- package/.agents/scripts/select-audits.js +0 -155
- package/.agents/scripts/update-mutation-baseline.js +0 -189
- package/.agents/workflows/explain.md +0 -118
- package/.agents/workflows/git-merge-pr.md +0 -377
- package/.agents/workflows/helpers/epic-plan-decompose.md +0 -22
- package/.agents/workflows/helpers/epic-plan-spec.md +0 -22
- package/.agents/workflows/loops/README.md +0 -65
- package/.agents/workflows/loops/fix-failing-tests.md +0 -74
- package/.agents/workflows/loops/nightly-audit.md +0 -81
- package/.agents/workflows/loops/watch-ci.md +0 -68
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* plan-persist.js — single GitHub-write surface for the collapsed /plan
|
|
5
|
+
* flow (Epic #4474, PR3 — design §1 Step 3).
|
|
6
|
+
*
|
|
7
|
+
* Supersedes the retired 12-phase pipeline's separate persist halves
|
|
8
|
+
* (Epic #4474 PR7 — the delegate CLIs are import shims now, deleted next
|
|
9
|
+
* release). Given the author-written planning artifacts
|
|
10
|
+
* (Tech Spec, optional Acceptance Table, risk verdict, tickets JSON), this
|
|
11
|
+
* CLI performs every GitHub mutation of the plan flow in one ordered,
|
|
12
|
+
* fail-closed pass:
|
|
13
|
+
*
|
|
14
|
+
* section gate → risk-verdict + mode-coherence → ticket validator /
|
|
15
|
+
* file-assumption gate / DAG / budget → (ideation: open the Epic) →
|
|
16
|
+
* Epic lease → managed sections + risk comment + freshness advisory →
|
|
17
|
+
* story creation (structural reconciler) → inline healthcheck →
|
|
18
|
+
* single terminal `agent::ready` flip (no intermediate
|
|
19
|
+
* `agent::review-spec`) → checkpoint v2 + `plan-summary` comment with
|
|
20
|
+
* the dry-run wave table → temp cleanup at terminal success only.
|
|
21
|
+
*
|
|
22
|
+
* Modes:
|
|
23
|
+
* --epic <id> Persist against an existing Epic. Artifact paths
|
|
24
|
+
* default to the per-Epic temp tree
|
|
25
|
+
* (`temp/epic-<id>/techspec.md`, `risk-verdict.json`,
|
|
26
|
+
* `tickets.json`, and `acceptance-spec.md` when
|
|
27
|
+
* present).
|
|
28
|
+
* --one-pager <path> Ideation mode: render + open the Epic from a
|
|
29
|
+
* sharpened one-pager first (folds the former
|
|
30
|
+
* Phase 3/4 steps in). Artifact paths must be
|
|
31
|
+
* explicit (there is no Epic id to derive them from).
|
|
32
|
+
*
|
|
33
|
+
* Modes (Epic #4474 PR4, design §2 mode matrix): the risk verdict's
|
|
34
|
+
* optional `deliveryShape` field selects between the full fan-out persist
|
|
35
|
+
* (default; requires tickets) and the spec-only single-delivery variant
|
|
36
|
+
* (`deliveryShape: "single"`; NO tickets — the ticket validator + DAG are
|
|
37
|
+
* skipped, fenced by construction, and the `delivery::single` routing
|
|
38
|
+
* marker is applied instead of a Story tree; inert until #4475 lands the
|
|
39
|
+
* deliver-side reader). `--amend` selects the change-request delta path:
|
|
40
|
+
* tickets carry `op: add|modify|keep|close` and the persist maps the ops
|
|
41
|
+
* onto the existing tree.
|
|
42
|
+
*
|
|
43
|
+
* Flags:
|
|
44
|
+
* --force Deliberate re-persist: overwrite managed sections,
|
|
45
|
+
* close + recreate the story tree (reconciler
|
|
46
|
+
* --explicit-delete). Reuses on-disk artifacts —
|
|
47
|
+
* cleanup is deferred to terminal success, so a
|
|
48
|
+
* failed run leaves them in place.
|
|
49
|
+
* --resume Continue a partial persist after a crash
|
|
50
|
+
* (rate-limit, network): sections short-circuit
|
|
51
|
+
* idempotently, the reconciler creates only the
|
|
52
|
+
* missing slugs from its per-slug state ledger.
|
|
53
|
+
* --amend Change-request delta persist: every ticket carries
|
|
54
|
+
* `op: add|modify|keep|close`; close-and-recreate is
|
|
55
|
+
* scoped to modify/close slugs only, keeps are
|
|
56
|
+
* untouched, and the DAG is validated over the
|
|
57
|
+
* merged set.
|
|
58
|
+
* --explicit-delete Confirm the close ops of an --amend plan (mirrors
|
|
59
|
+
* epic-reconcile.js). Without it, an amend carrying
|
|
60
|
+
* close ops prints the dry-run diff and exits 2.
|
|
61
|
+
* --steal Force-transfer a live foreign Epic-lease claim.
|
|
62
|
+
* --force-review Operator-forced review routing (recorded in the
|
|
63
|
+
* checkpoint's reviewRouting envelope).
|
|
64
|
+
* --allow-over-budget / --allow-large-fan-out
|
|
65
|
+
* Same overrides as the retired split persist.
|
|
66
|
+
*
|
|
67
|
+
* Exit codes: 0 — persist complete, Epic is `agent::ready`; 1 — fatal
|
|
68
|
+
* error (see stderr); 2 — amend close ops require --explicit-delete (the
|
|
69
|
+
* dry-run diff is printed; nothing was mutated); 3 — reachability orphans
|
|
70
|
+
* (Epic #4474 PR6 named soft failure: the deterministic route-glob vs
|
|
71
|
+
* `planning.navigation.navRegistry` scan found route-adding draft stories
|
|
72
|
+
* with no navigation owner — the orphan-surface list is printed, nothing
|
|
73
|
+
* was mutated; apply one targeted amend and re-run the persist once). The
|
|
74
|
+
* Epic lease is released on every exit path.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
// Fail-fast if the framework's runtime deps are not installed — must be the
|
|
78
|
+
// first import so the check runs before any third-party-importing sibling
|
|
79
|
+
// module is evaluated (Story #3432).
|
|
80
|
+
import './lib/runtime-deps/ensure-installed.js';
|
|
81
|
+
import { readFile } from 'node:fs/promises';
|
|
82
|
+
import path from 'node:path';
|
|
83
|
+
import { parseArgs } from 'node:util';
|
|
84
|
+
|
|
85
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
86
|
+
import { epicArtifactPath } from './lib/config/temp-paths.js';
|
|
87
|
+
import {
|
|
88
|
+
PROJECT_ROOT,
|
|
89
|
+
resolveConfig,
|
|
90
|
+
validateOrchestrationConfig,
|
|
91
|
+
} from './lib/config-resolver.js';
|
|
92
|
+
import { Logger } from './lib/Logger.js';
|
|
93
|
+
import { drainPendingCleanupAtBoot } from './lib/orchestration/epic-plan-spec/phases/drain.js';
|
|
94
|
+
import { loadRiskVerdict } from './lib/orchestration/epic-plan-spec/phases/risk-verdict.js';
|
|
95
|
+
import {
|
|
96
|
+
readPlanMetrics,
|
|
97
|
+
recordPlanInvocation,
|
|
98
|
+
renderPlanMetricsSummaryLine,
|
|
99
|
+
summarizePlanMetrics,
|
|
100
|
+
} from './lib/orchestration/plan-metrics.js';
|
|
101
|
+
import {
|
|
102
|
+
resolveDeliveryMode,
|
|
103
|
+
runPlanPersist,
|
|
104
|
+
writeCheckpointV2,
|
|
105
|
+
} from './lib/orchestration/plan-persist/run-plan-persist.js';
|
|
106
|
+
import {
|
|
107
|
+
buildPlanSummaryCommentBody,
|
|
108
|
+
buildWaveTable,
|
|
109
|
+
PLAN_SUMMARY_COMMENT_TYPE,
|
|
110
|
+
} from './lib/orchestration/plan-persist/summary.js';
|
|
111
|
+
import { createProvider } from './lib/provider-factory.js';
|
|
112
|
+
|
|
113
|
+
// Re-exports for the stable public API (tests import through the CLI
|
|
114
|
+
// module, mirroring the plan-context.js CLI-shell shape).
|
|
115
|
+
export {
|
|
116
|
+
buildPlanSummaryCommentBody,
|
|
117
|
+
buildWaveTable,
|
|
118
|
+
PLAN_SUMMARY_COMMENT_TYPE,
|
|
119
|
+
resolveDeliveryMode,
|
|
120
|
+
runPlanPersist,
|
|
121
|
+
writeCheckpointV2,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const EPIC_FROM_IDEA_TEMPLATE_PATH = path.resolve(
|
|
125
|
+
PROJECT_ROOT,
|
|
126
|
+
'.agents',
|
|
127
|
+
'templates',
|
|
128
|
+
'epic-from-idea.md',
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const CLI_OPTIONS = {
|
|
132
|
+
epic: { type: 'string' },
|
|
133
|
+
'one-pager': { type: 'string' },
|
|
134
|
+
'tech-spec': { type: 'string' },
|
|
135
|
+
'acceptance-table': { type: 'string' },
|
|
136
|
+
'risk-verdict': { type: 'string' },
|
|
137
|
+
tickets: { type: 'string' },
|
|
138
|
+
force: { type: 'boolean', default: false },
|
|
139
|
+
resume: { type: 'boolean', default: false },
|
|
140
|
+
steal: { type: 'boolean', default: false },
|
|
141
|
+
'force-review': { type: 'boolean', default: false },
|
|
142
|
+
'allow-over-budget': { type: 'boolean', default: false },
|
|
143
|
+
'allow-large-fan-out': { type: 'boolean', default: false },
|
|
144
|
+
amend: { type: 'boolean', default: false },
|
|
145
|
+
'explicit-delete': { type: 'boolean', default: false },
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const USAGE =
|
|
149
|
+
'Usage: plan-persist.js (--epic <EpicId> | --one-pager <file>) ' +
|
|
150
|
+
'[--tech-spec <file>] [--acceptance-table <file>] [--risk-verdict <file>] ' +
|
|
151
|
+
'[--tickets <file>] [--force | --resume | --amend [--explicit-delete]] ' +
|
|
152
|
+
'[--steal] [--force-review] [--allow-over-budget] [--allow-large-fan-out]';
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Parse `--epic`; returns null when absent (ideation mode).
|
|
156
|
+
*/
|
|
157
|
+
function parseEpicId(rawEpic) {
|
|
158
|
+
if (rawEpic === undefined) return null;
|
|
159
|
+
const epicId = Number.parseInt(rawEpic, 10);
|
|
160
|
+
if (Number.isNaN(epicId)) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Invalid epic ID: "${rawEpic}" — must be a number.\n${USAGE}`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return epicId;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Resolve the artifact file paths. Existing-Epic mode defaults each path to
|
|
170
|
+
* the per-Epic temp tree — the same locations the authoring skill writes to
|
|
171
|
+
* — so a `--force`/`--resume` re-persist reuses the on-disk artifacts with
|
|
172
|
+
* no re-typing (they survive until terminal success now that cleanup is
|
|
173
|
+
* deferred). Ideation mode has no Epic id to derive from, so the paths must
|
|
174
|
+
* be explicit.
|
|
175
|
+
*/
|
|
176
|
+
function resolveArtifactPaths({ epicId, values, config }) {
|
|
177
|
+
const fallback = (basename) =>
|
|
178
|
+
epicId === null ? undefined : epicArtifactPath(epicId, basename, config);
|
|
179
|
+
const techSpecPath = values['tech-spec'] ?? fallback('techspec.md');
|
|
180
|
+
const riskVerdictPath =
|
|
181
|
+
values['risk-verdict'] ?? fallback('risk-verdict.json');
|
|
182
|
+
const ticketsPath = values.tickets ?? fallback('tickets.json');
|
|
183
|
+
const acceptancePath =
|
|
184
|
+
values['acceptance-table'] ?? fallback('acceptance-spec.md');
|
|
185
|
+
if (!techSpecPath || !riskVerdictPath) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`Missing artifact path(s): ideation mode requires explicit --tech-spec and --risk-verdict.\n${USAGE}`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
techSpecPath,
|
|
192
|
+
riskVerdictPath,
|
|
193
|
+
ticketsPath,
|
|
194
|
+
acceptancePath,
|
|
195
|
+
acceptanceExplicit: values['acceptance-table'] !== undefined,
|
|
196
|
+
ticketsExplicit: values.tickets !== undefined,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function readOptional(filePath, { required }) {
|
|
201
|
+
try {
|
|
202
|
+
return await readFile(filePath, 'utf8');
|
|
203
|
+
} catch (err) {
|
|
204
|
+
if (!required && err?.code === 'ENOENT') return null;
|
|
205
|
+
throw new Error(`Cannot read ${filePath}: ${err.message}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function main() {
|
|
210
|
+
const { values } = parseArgs({ options: CLI_OPTIONS });
|
|
211
|
+
|
|
212
|
+
if (values.force && values.resume) {
|
|
213
|
+
throw new Error('--force and --resume are mutually exclusive.');
|
|
214
|
+
}
|
|
215
|
+
if (values.amend && (values.force || values.resume)) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
'--amend is mutually exclusive with --force/--resume — the amend ' +
|
|
218
|
+
'delta is already an incremental re-persist.',
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const epicId = parseEpicId(values.epic);
|
|
223
|
+
const onePagerPath = values['one-pager'];
|
|
224
|
+
if (epicId === null && !onePagerPath) {
|
|
225
|
+
throw new Error(USAGE);
|
|
226
|
+
}
|
|
227
|
+
if (epicId !== null && onePagerPath) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
'--epic and --one-pager are mutually exclusive (ideation mode opens ' +
|
|
230
|
+
`the Epic itself).\n${USAGE}`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let config;
|
|
235
|
+
try {
|
|
236
|
+
config = resolveConfig();
|
|
237
|
+
validateOrchestrationConfig(config);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
throw new Error(`Config schema validation failed:\n${err.message}`);
|
|
240
|
+
}
|
|
241
|
+
const settings = {
|
|
242
|
+
baseBranch: config.project?.baseBranch,
|
|
243
|
+
paths: config.project?.paths,
|
|
244
|
+
planning: config.planning,
|
|
245
|
+
docsContextFiles: config.project?.docsContextFiles,
|
|
246
|
+
};
|
|
247
|
+
const provider = createProvider(config);
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
await drainPendingCleanupAtBoot({
|
|
251
|
+
repoRoot: PROJECT_ROOT,
|
|
252
|
+
config,
|
|
253
|
+
provider,
|
|
254
|
+
});
|
|
255
|
+
} catch (err) {
|
|
256
|
+
Logger.warn(`[plan-persist] pending-cleanup drain skipped: ${err.message}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const {
|
|
260
|
+
techSpecPath,
|
|
261
|
+
riskVerdictPath,
|
|
262
|
+
ticketsPath,
|
|
263
|
+
acceptancePath,
|
|
264
|
+
acceptanceExplicit,
|
|
265
|
+
ticketsExplicit,
|
|
266
|
+
} = resolveArtifactPaths({ epicId, values, config });
|
|
267
|
+
|
|
268
|
+
// Deterministic local reads + validation before any GitHub call. A
|
|
269
|
+
// malformed risk verdict fails closed here (Epic #3865); the section
|
|
270
|
+
// gate itself runs first inside runPlanPersist.
|
|
271
|
+
const techSpecContent = await readOptional(techSpecPath, { required: true });
|
|
272
|
+
const riskVerdict = loadRiskVerdict(riskVerdictPath);
|
|
273
|
+
// Tickets are mode-dependent (#4474 PR4): required when explicitly
|
|
274
|
+
// passed or in --amend mode; otherwise best-effort — a single-delivery
|
|
275
|
+
// plan authors no tickets file at all, and the mode-coherence gate
|
|
276
|
+
// (`resolveDeliveryMode`) hard-errors on every contradictory combination
|
|
277
|
+
// (including a stale tickets.json next to a `deliveryShape: "single"`
|
|
278
|
+
// verdict).
|
|
279
|
+
const ticketsRequired = ticketsExplicit || values.amend;
|
|
280
|
+
const ticketsRaw = ticketsPath
|
|
281
|
+
? await readOptional(ticketsPath, { required: ticketsRequired })
|
|
282
|
+
: null;
|
|
283
|
+
let tickets = null;
|
|
284
|
+
if (ticketsRaw !== null) {
|
|
285
|
+
try {
|
|
286
|
+
tickets = JSON.parse(ticketsRaw);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
`Failed to parse tickets file "${ticketsPath}" as JSON: ${err.message}`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// Acceptance table: explicit path is required to exist; the per-Epic
|
|
294
|
+
// default is best-effort (absent file → no acceptance section, matching
|
|
295
|
+
// the waived/none dispositions).
|
|
296
|
+
const acceptanceSpecContent = acceptancePath
|
|
297
|
+
? await readOptional(acceptancePath, { required: acceptanceExplicit })
|
|
298
|
+
: null;
|
|
299
|
+
const onePagerContent = onePagerPath
|
|
300
|
+
? await readOptional(onePagerPath, { required: true })
|
|
301
|
+
: null;
|
|
302
|
+
const templateContent = onePagerContent
|
|
303
|
+
? await readOptional(EPIC_FROM_IDEA_TEMPLATE_PATH, { required: true })
|
|
304
|
+
: null;
|
|
305
|
+
|
|
306
|
+
// Plan-metrics ledger (#4474 PR1): stamp entry/exit + mode. Ideation runs
|
|
307
|
+
// have no Epic id at entry, so they stamp on the standalone stream.
|
|
308
|
+
const mode = values.amend
|
|
309
|
+
? 'amend'
|
|
310
|
+
: values.resume
|
|
311
|
+
? 'resume'
|
|
312
|
+
: values.force
|
|
313
|
+
? 'force'
|
|
314
|
+
: 'persist';
|
|
315
|
+
let result;
|
|
316
|
+
try {
|
|
317
|
+
result = await recordPlanInvocation(
|
|
318
|
+
{ cli: 'plan-persist', mode, epicId, config },
|
|
319
|
+
() =>
|
|
320
|
+
runPlanPersist({
|
|
321
|
+
epicId,
|
|
322
|
+
provider,
|
|
323
|
+
artifacts: {
|
|
324
|
+
techSpecContent,
|
|
325
|
+
acceptanceSpecContent,
|
|
326
|
+
riskVerdict,
|
|
327
|
+
tickets,
|
|
328
|
+
onePagerContent,
|
|
329
|
+
templateContent,
|
|
330
|
+
},
|
|
331
|
+
config,
|
|
332
|
+
settings,
|
|
333
|
+
opts: {
|
|
334
|
+
force: values.force,
|
|
335
|
+
resume: values.resume,
|
|
336
|
+
amend: values.amend,
|
|
337
|
+
explicitDelete: values['explicit-delete'],
|
|
338
|
+
steal: values.steal,
|
|
339
|
+
forceReview: values['force-review'],
|
|
340
|
+
allowOverBudget: values['allow-over-budget'],
|
|
341
|
+
allowLargeFanOut: values['allow-large-fan-out'],
|
|
342
|
+
},
|
|
343
|
+
}),
|
|
344
|
+
);
|
|
345
|
+
} catch (err) {
|
|
346
|
+
// Amend close-op confirmation gate (exit 2, mirroring the
|
|
347
|
+
// epic-reconcile.js contract): print the dry-run diff so the operator
|
|
348
|
+
// reviews exactly what would close, mutate nothing, and exit 2 so
|
|
349
|
+
// non-interactive callers can branch on the code.
|
|
350
|
+
if (err?.code === 'PLAN_AMEND_EXPLICIT_DELETE_REQUIRED') {
|
|
351
|
+
process.stdout.write(`${err.diff}\n\n${err.message}\n`);
|
|
352
|
+
process.exitCode = 2;
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
// Reachability soft failure (#4474 PR6): the orphan-surface list is the
|
|
356
|
+
// message; nothing was mutated (the check runs before any provider
|
|
357
|
+
// call). Exit 3 so non-interactive callers can branch on the code.
|
|
358
|
+
if (err?.code === 'PLAN_REACHABILITY_ORPHANS') {
|
|
359
|
+
process.stdout.write(`${err.message}\n`);
|
|
360
|
+
process.exitCode = 3;
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
throw err;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Surface the whole plan run's invocation ledger in the persist summary
|
|
367
|
+
// (#4474 PR1). Additive and best-effort.
|
|
368
|
+
try {
|
|
369
|
+
const summary = summarizePlanMetrics(
|
|
370
|
+
await readPlanMetrics(result.epicId, config),
|
|
371
|
+
);
|
|
372
|
+
if (summary) {
|
|
373
|
+
result.planMetrics = summary;
|
|
374
|
+
Logger.info(`[plan-persist] ${renderPlanMetricsSummaryLine(summary)}`);
|
|
375
|
+
}
|
|
376
|
+
} catch (err) {
|
|
377
|
+
Logger.warn(`[plan-persist] plan-metrics summary skipped: ${err.message}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
runAsCli(import.meta.url, main, { source: 'plan-persist' });
|
|
@@ -19,13 +19,13 @@
|
|
|
19
19
|
* IMMEDIATELY, consuming no resume budget. On red the CLI
|
|
20
20
|
* writes `temp/epic-<id>-ci-digest.{json,md}` (failing check
|
|
21
21
|
* name, run id, a `gh run view --log-failed` tail, and a
|
|
22
|
-
* coarse classification) and prints the
|
|
23
|
-
*
|
|
22
|
+
* coarse classification) and prints the red-green
|
|
23
|
+
* remediation handoff.
|
|
24
24
|
* - STILL-RUNNING — the poll cap fired with checks still pending and
|
|
25
25
|
* none failed; the watcher re-armed up to
|
|
26
26
|
* `delivery.ci.watch.maxResumes` times, then returned a
|
|
27
27
|
* `still-running` verdict → exit 2 (NEVER 1, NEVER
|
|
28
|
-
* `timed_out`). The CLI prints the
|
|
28
|
+
* `timed_out`). The CLI prints the `gh pr checks --watch`
|
|
29
29
|
* handoff so the host can keep polling on its own cadence.
|
|
30
30
|
*
|
|
31
31
|
* Config (Story #4356 namespace, read via `getCiDelivery`):
|
|
@@ -350,9 +350,9 @@ export async function runPrWatch({
|
|
|
350
350
|
.map(([k]) => k)
|
|
351
351
|
.join(', ');
|
|
352
352
|
logger.warn?.(
|
|
353
|
-
`[pr-watch] required check(s) still running after ${result.polls} polls + ${result.resumesApplied} resumes: ${stillPending}.
|
|
353
|
+
`[pr-watch] required check(s) still running after ${result.polls} polls + ${result.resumesApplied} resumes: ${stillPending}. Keep polling natively:`,
|
|
354
354
|
);
|
|
355
|
-
logger.warn?.('[pr-watch]
|
|
355
|
+
logger.warn?.('[pr-watch] gh pr checks <pr> --watch');
|
|
356
356
|
return STILL_RUNNING_EXIT_CODE;
|
|
357
357
|
}
|
|
358
358
|
|
|
@@ -392,8 +392,9 @@ export async function runPrWatch({
|
|
|
392
392
|
if (digestPaths) {
|
|
393
393
|
logger.error?.(`[pr-watch] CI failure digest → ${digestPaths.jsonPath}`);
|
|
394
394
|
}
|
|
395
|
-
logger.error?.(
|
|
396
|
-
|
|
395
|
+
logger.error?.(
|
|
396
|
+
'[pr-watch] a required check failed. Read the digest, apply the smallest fix, and re-run the suite until green.',
|
|
397
|
+
);
|
|
397
398
|
return 1;
|
|
398
399
|
}
|
|
399
400
|
|
|
@@ -56,6 +56,16 @@ const tasks = [
|
|
|
56
56
|
cmd: 'node',
|
|
57
57
|
args: ['.agents/scripts/check-lifecycle-lint.js'],
|
|
58
58
|
},
|
|
59
|
+
{
|
|
60
|
+
// Custom Node-based lint for the workflow prose surface (Epic #4474
|
|
61
|
+
// PR5). Enforces: no workflow may instruct calling an exported
|
|
62
|
+
// library function that has no CLI entrypoint — the measured
|
|
63
|
+
// shim-writing failure mode the /plan collapse killed. See
|
|
64
|
+
// check-workflow-cli-lint.js for the paragraph-level heuristic.
|
|
65
|
+
name: 'workflow-cli-lint',
|
|
66
|
+
cmd: 'node',
|
|
67
|
+
args: ['.agents/scripts/check-workflow-cli-lint.js'],
|
|
68
|
+
},
|
|
59
69
|
{
|
|
60
70
|
// Custom Node-based lint for label-vocabulary citations in
|
|
61
71
|
// `.agents/docs/SDLC.md` and `.agents/workflows/**/*.md` (Story #2892,
|
|
@@ -78,17 +88,6 @@ const tasks = [
|
|
|
78
88
|
cmd: 'node',
|
|
79
89
|
args: ['.agents/scripts/check-arch-cycles.js'],
|
|
80
90
|
},
|
|
81
|
-
{
|
|
82
|
-
// Loop-unit frontmatter gate (Story #4288, Epic #4284). Validates
|
|
83
|
-
// every `.agents/workflows/loops/*.md` loop unit against
|
|
84
|
-
// `.agents/schemas/loop-unit.schema.json`. An absent/empty loops
|
|
85
|
-
// directory is a clean pass; a malformed unit (e.g. a self-paced
|
|
86
|
-
// cadence missing its required `verify`) fails the lint gate with a
|
|
87
|
-
// message naming the offending file + field.
|
|
88
|
-
name: 'loop-units',
|
|
89
|
-
cmd: 'node',
|
|
90
|
-
args: ['.agents/scripts/check-loop-units.js'],
|
|
91
|
-
},
|
|
92
91
|
];
|
|
93
92
|
|
|
94
93
|
function runTask({ name, cmd, args }) {
|
|
@@ -39,6 +39,7 @@ import { PROJECT_ROOT, resolveConfig } from './lib/config-resolver.js';
|
|
|
39
39
|
import { exec as ghExec } from './lib/gh-exec.js';
|
|
40
40
|
import { Logger, routeAllOutputToStderr } from './lib/Logger.js';
|
|
41
41
|
import { TYPE_LABELS } from './lib/label-constants.js';
|
|
42
|
+
import { recordPlanInvocation } from './lib/orchestration/plan-metrics.js';
|
|
42
43
|
import { buildCorpusContext } from './lib/planning-corpus.js';
|
|
43
44
|
import { createProvider } from './lib/provider-factory.js';
|
|
44
45
|
import {
|
|
@@ -298,16 +299,27 @@ async function main() {
|
|
|
298
299
|
if (values['emit-context']) {
|
|
299
300
|
// Reserve stdout for the JSON envelope so a captured file is
|
|
300
301
|
// unconditionally parseable by `JSON.parse`. Mirrors the contract
|
|
301
|
-
// `
|
|
302
|
+
// `plan-context.js` enforces for its emit mode.
|
|
302
303
|
routeAllOutputToStderr();
|
|
303
|
-
|
|
304
|
+
// Plan-metrics ledger (#4474 PR1): standalone plans have no Epic, so
|
|
305
|
+
// `epicId: null` routes the stamp to the standalone stream
|
|
306
|
+
// (`temp/standalone/plan-metrics.json`) — same pattern as friction.
|
|
307
|
+
return recordPlanInvocation(
|
|
308
|
+
{ cli: 'story-plan', mode: 'emit-context', epicId: null, config },
|
|
309
|
+
() => runEmitContext({ values, provider, projectRoot, config }),
|
|
310
|
+
);
|
|
304
311
|
}
|
|
305
312
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
313
|
+
// Plan-metrics ledger (#4474 PR1): stamp entry/exit + mode.
|
|
314
|
+
return recordPlanInvocation(
|
|
315
|
+
{ cli: 'story-plan', mode: 'persist', epicId: null, config },
|
|
316
|
+
() =>
|
|
317
|
+
runPersist({
|
|
318
|
+
values,
|
|
319
|
+
provider,
|
|
320
|
+
dryRun: values['dry-run'],
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
311
323
|
}
|
|
312
324
|
|
|
313
325
|
runAsCli(import.meta.url, main, { source: 'story-plan' });
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
import fs from 'node:fs';
|
|
48
48
|
import path from 'node:path';
|
|
49
49
|
|
|
50
|
-
import { applyHeader } from './lib/command-header.js';
|
|
50
|
+
import { applyHeader, isCommandExcluded } from './lib/command-header.js';
|
|
51
51
|
import { Logger } from './lib/Logger.js';
|
|
52
52
|
|
|
53
53
|
// Resolve the project root from the invocation cwd — the consumer project where
|
|
@@ -154,9 +154,7 @@ const isTopLevelWorkflow = (entry) =>
|
|
|
154
154
|
/**
|
|
155
155
|
* `README.md` (any case) under `loops/` is namespace documentation, not a
|
|
156
156
|
* loop unit — it carries no `loop:` frontmatter and must not project as a
|
|
157
|
-
* `/loops:README` command. Exclude it from the loop-unit enumeration
|
|
158
|
-
* mirrors `check-loop-units.js#isLoopUnitFile`, which excludes it from the
|
|
159
|
-
* lint gate).
|
|
157
|
+
* `/loops:README` command. Exclude it from the loop-unit enumeration.
|
|
160
158
|
*
|
|
161
159
|
* @param {import('node:fs').Dirent} entry
|
|
162
160
|
* @returns {boolean}
|
|
@@ -204,13 +202,20 @@ const entries = SRC_DIRS.flatMap((dir) => [
|
|
|
204
202
|
|
|
205
203
|
// Collision policy: payload wins, warn on a shadowed local file. Keyed by the
|
|
206
204
|
// destination-relative path so a flat `foo.md` and a `loops/foo.md` are
|
|
207
|
-
// distinct entries.
|
|
205
|
+
// distinct entries. A workflow whose frontmatter carries `command: false`
|
|
206
|
+
// (#4482 — dual-use audit lens files with a host-native standalone
|
|
207
|
+
// equivalent) is excluded from projection entirely: it never enters
|
|
208
|
+
// `sourceSet`, so any previously projected copy is reaped below.
|
|
208
209
|
const byRel = new Map();
|
|
209
210
|
for (const e of entries) {
|
|
210
211
|
if (byRel.has(e.rel)) {
|
|
211
212
|
Logger.warn(` shadowed ${e.rel} (local copy ignored; payload wins)`);
|
|
212
213
|
continue;
|
|
213
214
|
}
|
|
215
|
+
if (isCommandExcluded(fs.readFileSync(path.join(e.dir, e.rel), 'utf8'))) {
|
|
216
|
+
Logger.info(` excluded ${e.rel} (frontmatter command: false)`);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
214
219
|
byRel.set(e.rel, e);
|
|
215
220
|
}
|
|
216
221
|
|
|
@@ -151,13 +151,17 @@ host's equivalent structured-choice surface.
|
|
|
151
151
|
|
|
152
152
|
This skill is the engine behind two operator-facing entry points:
|
|
153
153
|
|
|
154
|
-
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
154
|
+
- **An ad-hoc conversational walkthrough** of a realized change (a PR,
|
|
155
|
+
branch, or diff) — the host's native mode. Read this skill and run the
|
|
156
|
+
loop directly when the operator asks to be walked through a change.
|
|
157
|
+
(The former `/explain` workflow wrapper was retired in #4482.)
|
|
158
|
+
- **A post-plan backlog walkthrough** — run the loop over a freshly planned
|
|
159
|
+
backlog (the Epic body's Tech Spec sections, the Stories, the wave table)
|
|
160
|
+
before the operator hands off to `/deliver`. Offer it by LM judgment on
|
|
161
|
+
non-trivial plans; it is no longer a wired phase of
|
|
162
|
+
[`/plan`](../../../workflows/helpers/plan-epic.md) (the comprehension gate
|
|
163
|
+
was unwired from the pipeline in Epic #4474), always advisory, and never
|
|
164
|
+
blocks the hand-off.
|
|
161
165
|
|
|
162
166
|
It is discovered through the standard skill-activation contract in
|
|
163
167
|
[`.agents/instructions.md` § 1.B](../../../instructions.md) — no separate
|
|
@@ -283,14 +283,12 @@ bloated tail over many Epics, and no single change-set-scoped `/deliver`
|
|
|
283
283
|
Phase 4 run sees the whole picture. Run this lens **full-scope** on a
|
|
284
284
|
recurring cadence so the drift is caught before it compounds:
|
|
285
285
|
|
|
286
|
-
- **Cron loop** — [`loops:nightly-audit`](loops/nightly-audit.md)
|
|
287
|
-
runs the audit sweep unattended each night; the host (`/schedule` or a
|
|
288
|
-
cron-driven `/loop`) owns the cadence. Point it at this lens full-scope
|
|
289
|
-
(no `--paths`, no change-set filter — the `{{changedFiles}}` block renders
|
|
290
|
-
the literal token, so the whole target-set union is audited).
|
|
291
286
|
- **Scheduled invocation** — `/schedule` running `/audit-documentation`
|
|
292
|
-
full-scope on a daily/weekly cron
|
|
293
|
-
|
|
287
|
+
full-scope on a daily/weekly cron. Point it at this lens full-scope
|
|
288
|
+
(no `--paths`, no change-set filter — the `{{changedFiles}}` block renders
|
|
289
|
+
the literal token, so the whole target-set union is audited). The nightly
|
|
290
|
+
sweep-then-route recipe lives in
|
|
291
|
+
[`audit-to-stories.md` § Scheduling a nightly sweep](audit-to-stories.md#scheduling-a-nightly-sweep).
|
|
294
292
|
|
|
295
293
|
Route the resulting `audit-documentation-results.md` through
|
|
296
294
|
[`/audit-to-stories`](audit-to-stories.md), which groups the findings,
|
|
@@ -120,7 +120,7 @@ Ask:
|
|
|
120
120
|
> - **Single Epic via `/plan`** **[Recommended]** — opens one Epic,
|
|
121
121
|
> then chains into `/plan --idea` so the standard spec-and-WBS
|
|
122
122
|
> authoring handles decomposition. Grouped Stories become the
|
|
123
|
-
> seed for
|
|
123
|
+
> seed for the authoring step's decomposition.
|
|
124
124
|
> - **Individual standalone Stories** — opens one GitHub Issue per
|
|
125
125
|
> group directly, no Epic wrapper.
|
|
126
126
|
|
|
@@ -138,8 +138,8 @@ node .agents/scripts/audit-to-stories.js --emit-epic-seed \
|
|
|
138
138
|
|
|
139
139
|
The seed renders the canonical one-pager sections — Problem Statement,
|
|
140
140
|
Recommended Direction, Key Assumptions (with links to every source
|
|
141
|
-
report), MVP Scope (the M proposed Stories), Key Files (so `/plan`
|
|
142
|
-
|
|
141
|
+
report), MVP Scope (the M proposed Stories), Key Files (so `/plan`'s
|
|
142
|
+
authoring step has concrete anchors), Not Doing.
|
|
143
143
|
|
|
144
144
|
Chain into the existing planning entrypoint:
|
|
145
145
|
|
|
@@ -147,8 +147,8 @@ Chain into the existing planning entrypoint:
|
|
|
147
147
|
/plan --idea "<path-to-seed>"
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
-
`/plan` then runs
|
|
151
|
-
|
|
150
|
+
`/plan` then runs its 3-step Epic path (interrogate → author →
|
|
151
|
+
persist), as documented in its workflow.
|
|
152
152
|
Each Story it spawns from the seed carries `context::audit:
|
|
153
153
|
<reportLink>` and `audit-fingerprint: <sha>` in its body so future
|
|
154
154
|
Phase 6 idempotency works on the next run.
|
|
@@ -248,6 +248,17 @@ opened. When the Standalone-Stories path ran, list every Issue URL.
|
|
|
248
248
|
creation; fall back to `gh issue create` when the MCP tool is
|
|
249
249
|
unavailable.
|
|
250
250
|
|
|
251
|
+
## Scheduling a nightly sweep
|
|
252
|
+
|
|
253
|
+
To run an unattended maintenance sweep, `/schedule` a nightly (or weekly)
|
|
254
|
+
job that (1) runs the relevant `audit-*` lens workflows full-scope — no
|
|
255
|
+
`--paths`, no change-set filter, so the whole target-set union is audited —
|
|
256
|
+
writing their `temp/audits/audit-*-results.md` reports, then (2) invokes
|
|
257
|
+
`/audit-to-stories` over those results to dedupe and route the findings.
|
|
258
|
+
The host scheduler owns the cadence; this workflow owns the routing. (This
|
|
259
|
+
paragraph folds in the `loops/nightly-audit.md` starter unit retired in
|
|
260
|
+
issue 4482.)
|
|
261
|
+
|
|
251
262
|
## See also
|
|
252
263
|
|
|
253
264
|
- [`/plan`](helpers/plan-epic.md) — the planning pipeline `/audit-to-stories`
|