mandrel 1.90.0 → 1.92.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/README.md +5 -5
- package/.agents/docs/SDLC.md +75 -107
- package/.agents/docs/configuration.md +1 -1
- package/.agents/docs/quality-gates.md +8 -7
- package/.agents/docs/workflows.md +4 -10
- package/.agents/instructions.md +2 -2
- package/.agents/schemas/agentrc.schema.json +1 -1
- package/.agents/schemas/lifecycle/README.md +2 -1
- package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
- package/.agents/schemas/risk-verdict.schema.json +14 -1
- package/.agents/scripts/README.md +0 -46
- package/.agents/scripts/analyze-execution.js +76 -1
- package/.agents/scripts/check-doc-links.js +3 -0
- package/.agents/scripts/check-workflow-cli-lint.js +208 -0
- package/.agents/scripts/epic-audit-recheck.js +1 -1
- package/.agents/scripts/epic-deliver-preflight.js +37 -1
- package/.agents/scripts/epic-plan-clarity.js +16 -6
- package/.agents/scripts/epic-plan-decompose.js +37 -59
- package/.agents/scripts/epic-plan-healthcheck.js +39 -6
- package/.agents/scripts/epic-plan-spec.js +41 -168
- package/.agents/scripts/epic-reconcile.js +7 -1
- package/.agents/scripts/lib/Logger.js +4 -4
- package/.agents/scripts/lib/audit-suite/index.js +4 -5
- package/.agents/scripts/lib/audit-suite/runner.js +6 -4
- package/.agents/scripts/lib/audit-suite/selector.js +2 -3
- package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
- package/.agents/scripts/lib/command-header.js +20 -0
- package/.agents/scripts/lib/config/github.js +1 -3
- package/.agents/scripts/lib/config-settings-schema.js +4 -3
- package/.agents/scripts/lib/degraded-mode.js +1 -1
- package/.agents/scripts/lib/label-constants.js +18 -3
- package/.agents/scripts/lib/label-taxonomy.js +14 -3
- package/.agents/scripts/lib/mutation/baseline-snapshot.js +7 -6
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +18 -8
- package/.agents/scripts/lib/orchestration/context-hydration-engine.js +3 -4
- package/.agents/scripts/lib/orchestration/docs-digest.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +4 -0
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist.js +22 -12
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +6 -1
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +18 -2
- package/.agents/scripts/{lifecycle-diff.js → lib/orchestration/lifecycle/ledger-diff.js} +10 -76
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +32 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +510 -0
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +177 -0
- package/.agents/scripts/lib/orchestration/plan-metrics.js +445 -0
- package/.agents/scripts/lib/orchestration/plan-persist/amend.js +359 -0
- package/.agents/scripts/lib/orchestration/plan-persist/delivery-mode.js +127 -0
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +978 -0
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +191 -0
- package/.agents/scripts/lib/orchestration/plan-reachability.js +160 -0
- package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +3 -3
- package/.agents/scripts/lib/orchestration/planning-risk.js +1 -1
- package/.agents/scripts/lib/orchestration/remote-verifier.js +165 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +10 -0
- package/.agents/scripts/lib/orchestration/spec-freshness.js +1 -1
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +1 -1
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +11 -2
- package/.agents/scripts/lib/plan-phase-cleanup.js +16 -0
- package/.agents/scripts/lib/presentation/manifest-persistence.js +2 -3
- package/.agents/scripts/lib/templates/spec-author-prompts.js +74 -0
- package/.agents/scripts/plan-context.js +186 -0
- package/.agents/scripts/plan-critics.js +227 -0
- package/.agents/scripts/plan-persist.js +383 -0
- package/.agents/scripts/pr-watch-with-update.js +8 -7
- package/.agents/scripts/run-lint.js +10 -11
- package/.agents/scripts/single-story-init.js +22 -0
- package/.agents/scripts/story-plan.js +19 -7
- package/.agents/scripts/sync-claude-commands.js +10 -5
- package/.agents/skills/core/knowledge-transfer/SKILL.md +11 -7
- package/.agents/workflows/audit-documentation.md +5 -7
- package/.agents/workflows/audit-lighthouse.md +1 -0
- package/.agents/workflows/audit-security.md +1 -0
- package/.agents/workflows/audit-to-stories.md +16 -5
- package/.agents/workflows/deliver.md +8 -0
- package/.agents/workflows/git-deliver.md +76 -243
- package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
- package/.agents/workflows/helpers/code-review.md +7 -6
- package/.agents/workflows/helpers/deliver-epic.md +16 -4
- package/.agents/workflows/helpers/plan-epic-reference.md +109 -96
- package/.agents/workflows/helpers/plan-epic.md +246 -999
- package/.agents/workflows/helpers/plan-story.md +14 -14
- package/.agents/workflows/helpers/scope-triage-gate.md +6 -4
- package/.agents/workflows/helpers/single-story-deliver.md +13 -4
- package/.agents/workflows/helpers/worktree-lifecycle.md +9 -7
- package/.agents/workflows/mandrel-update.md +144 -466
- package/.agents/workflows/plan.md +73 -143
- package/docs/CHANGELOG.md +20 -0
- package/lib/cli/registry.js +21 -3
- package/package.json +1 -1
- package/.agents/schemas/loop-unit.schema.json +0 -70
- package/.agents/scripts/assert-branch.js +0 -81
- package/.agents/scripts/check-loop-units.js +0 -204
- package/.agents/scripts/detect-merges.js +0 -111
- package/.agents/scripts/git-pr-quality-gate.js +0 -205
- package/.agents/scripts/git-rebase-and-resolve.js +0 -234
- package/.agents/scripts/hierarchy-gate.js +0 -192
- package/.agents/scripts/hydrate-context.js +0 -179
- package/.agents/scripts/lib/loop-units/validate-loop-unit.js +0 -197
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/cli.js +0 -167
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/cli-args.js +0 -69
- package/.agents/scripts/lib/orchestration/plan-runner/plan-router.js +0 -86
- package/.agents/scripts/loc-delta.js +0 -205
- package/.agents/scripts/render-manifest.js +0 -143
- package/.agents/scripts/run-audit-suite.js +0 -97
- package/.agents/scripts/select-audits.js +0 -155
- package/.agents/scripts/update-mutation-baseline.js +0 -189
- package/.agents/workflows/explain.md +0 -118
- package/.agents/workflows/git-merge-pr.md +0 -377
- package/.agents/workflows/helpers/epic-plan-decompose.md +0 -22
- package/.agents/workflows/helpers/epic-plan-spec.md +0 -22
- package/.agents/workflows/loops/README.md +0 -65
- package/.agents/workflows/loops/fix-failing-tests.md +0 -74
- package/.agents/workflows/loops/nightly-audit.md +0 -81
- package/.agents/workflows/loops/watch-ci.md +0 -68
|
@@ -0,0 +1,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 }) {
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
executeFastForward,
|
|
60
60
|
planFastForward,
|
|
61
61
|
} from './lib/orchestration/git-cleanup/phases/fast-forward.js';
|
|
62
|
+
import { verifyRemote } from './lib/orchestration/remote-verifier.js';
|
|
62
63
|
import { acquireStoryLease } from './lib/orchestration/single-story-lease-guard.js';
|
|
63
64
|
import {
|
|
64
65
|
STATE_LABELS,
|
|
@@ -446,6 +447,7 @@ export async function runSingleStoryInit({
|
|
|
446
447
|
injectedAcquireLease,
|
|
447
448
|
steal = false,
|
|
448
449
|
leaseNow,
|
|
450
|
+
injectedVerifyRemote,
|
|
449
451
|
} = {}) {
|
|
450
452
|
const parsed =
|
|
451
453
|
storyIdParam !== undefined
|
|
@@ -487,6 +489,20 @@ export async function runSingleStoryInit({
|
|
|
487
489
|
);
|
|
488
490
|
progress('INIT', `Initializing standalone Story #${storyId}...`);
|
|
489
491
|
|
|
492
|
+
// Issue #4483 — deterministic remote evidence at the standalone entry
|
|
493
|
+
// seam (the counterpart to `epic-deliver-preflight.js`'s probe). The
|
|
494
|
+
// probe is read-only, so it runs under --dry-run too. The CLI records
|
|
495
|
+
// the fact; the workflow owns the `agent::blocked` transition on
|
|
496
|
+
// `remoteVerified: false` — inline delivery to local `main` is never a
|
|
497
|
+
// sanctioned fallback.
|
|
498
|
+
const remote = (injectedVerifyRemote ?? verifyRemote)({ cwd });
|
|
499
|
+
progress(
|
|
500
|
+
'REMOTE',
|
|
501
|
+
remote.remoteVerified
|
|
502
|
+
? `✅ remoteVerified=true — ${remote.detail}`
|
|
503
|
+
: `⛔ remoteVerified=false — ${remote.detail}`,
|
|
504
|
+
);
|
|
505
|
+
|
|
490
506
|
const story = await provider.getTicket(storyId);
|
|
491
507
|
assertDeliverableStory(story, storyId);
|
|
492
508
|
|
|
@@ -563,6 +579,9 @@ export async function runSingleStoryInit({
|
|
|
563
579
|
dependenciesInstalled,
|
|
564
580
|
installFailed: installStatus.status === 'failed',
|
|
565
581
|
dryRun,
|
|
582
|
+
// Issue #4483 — verified remote evidence for the orchestrating agent.
|
|
583
|
+
remoteVerified: remote.remoteVerified,
|
|
584
|
+
remoteProbe: { remoteUrl: remote.remoteUrl, detail: remote.detail },
|
|
566
585
|
};
|
|
567
586
|
|
|
568
587
|
// Upsert the `story-init` structured comment + flip Story to executing.
|
|
@@ -632,6 +651,8 @@ export function renderSingleStoryInitComment(result) {
|
|
|
632
651
|
worktreeCreated: result.worktreeCreated,
|
|
633
652
|
dependenciesInstalled: result.dependenciesInstalled,
|
|
634
653
|
installStatus: result.installStatus,
|
|
654
|
+
remoteVerified: result.remoteVerified,
|
|
655
|
+
remoteProbe: result.remoteProbe,
|
|
635
656
|
};
|
|
636
657
|
return [
|
|
637
658
|
'## Story init (standalone)',
|
|
@@ -641,6 +662,7 @@ export function renderSingleStoryInitComment(result) {
|
|
|
641
662
|
`- **baseBranch:** \`${result.baseBranch}\``,
|
|
642
663
|
`- **workCwd:** \`${result.workCwd}\``,
|
|
643
664
|
`- **worktreeEnabled:** \`${result.worktreeEnabled}\``,
|
|
665
|
+
`- **remoteVerified:** \`${result.remoteVerified}\``,
|
|
644
666
|
`- **dependenciesInstalled:** \`${result.dependenciesInstalled}\``,
|
|
645
667
|
'',
|
|
646
668
|
'```json',
|
|
@@ -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,
|