mandrel 2.58.0 → 2.60.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 +17 -12
- package/.agents/agents/acceptance-critic.md +24 -43
- package/.agents/agents/story-worker.md +18 -19
- package/.agents/docs/SDLC.md +12 -13
- package/.agents/docs/agentrc-reference.json +1 -2
- package/.agents/docs/configuration.md +29 -46
- package/.agents/docs/quality-gates.md +9 -5
- package/.agents/docs/workflows.md +1 -1
- package/.agents/instructions.md +5 -7
- package/.agents/rules/ci-remediation.md +41 -8
- package/.agents/rules/known-tooling-behavior.md +65 -15
- package/.agents/runtime-deps.json +7 -2
- package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
- package/.agents/schemas/agentrc.schema.json +6 -11
- package/.agents/schemas/crap-baseline.schema.json +1 -1
- package/.agents/schemas/crap-report.schema.json +1 -1
- package/.agents/schemas/story-deliver-terminal.schema.json +3 -3
- package/.agents/scripts/README.md +11 -1
- package/.agents/scripts/acceptance-eval.js +25 -27
- package/.agents/scripts/ceremony-derive.js +15 -10
- package/.agents/scripts/check-context-budget.js +148 -228
- package/.agents/scripts/check-schema-references.js +5 -3
- package/.agents/scripts/check-workflow-citations.js +33 -147
- package/.agents/scripts/coverage-capture.js +7 -4
- package/.agents/scripts/deliver-light.js +41 -100
- package/.agents/scripts/deliver-run.js +631 -0
- package/.agents/scripts/file-ci-gap.js +59 -11
- package/.agents/scripts/install-matrix-assert.js +48 -3
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +51 -33
- package/.agents/scripts/lib/baselines/crap-preview-incremental.js +6 -2
- package/.agents/scripts/lib/baselines/kinds/_crap-read.js +0 -8
- package/.agents/scripts/lib/baselines/kinds/crap.js +35 -18
- package/.agents/scripts/lib/changed-files.js +30 -0
- package/.agents/scripts/lib/config/delivery-routing.js +5 -4
- package/.agents/scripts/lib/config/explain.js +1 -3
- package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +1 -1
- package/.agents/scripts/lib/config-resolver.js +1 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +28 -21
- package/.agents/scripts/lib/coverage-capture-fullscope.js +10 -2
- package/.agents/scripts/lib/coverage-capture-incremental.js +3 -2
- package/.agents/scripts/lib/coverage-capture-usage.js +4 -1
- package/.agents/scripts/lib/crap-engine.js +2 -2
- package/.agents/scripts/lib/crap-utils.js +21 -5
- package/.agents/scripts/lib/doc-tiers.js +4 -2
- package/.agents/scripts/lib/escomplex-ast-compat.js +39 -17
- package/.agents/scripts/lib/escomplex-kernel.js +298 -0
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +7 -6
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +7 -5
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/gh-exec.js +160 -0
- package/.agents/scripts/lib/maintainability-engine.js +3 -3
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +74 -132
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +123 -12
- package/.agents/scripts/lib/orchestration/complexity-gate.js +180 -352
- package/.agents/scripts/lib/orchestration/light-suitability.js +71 -136
- package/.agents/scripts/lib/orchestration/plan-context.js +44 -50
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +8 -6
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +104 -119
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +41 -25
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +11 -11
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +63 -29
- package/.agents/scripts/lib/orchestration/plan-persist/wave-collision-gate.js +107 -0
- package/.agents/scripts/lib/orchestration/review-depth.js +14 -11
- package/.agents/scripts/lib/orchestration/run-epilogue.js +260 -182
- package/.agents/scripts/lib/orchestration/run-scoped-config.js +63 -99
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +3 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/graphql-preflight.js +137 -0
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +105 -18
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +4 -3
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +156 -39
- package/.agents/scripts/lib/orchestration/story-init-envelope.js +71 -0
- package/.agents/scripts/lib/orchestration/task-body-validator.js +8 -17
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +25 -209
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +8 -5
- package/.agents/scripts/lib/orchestration/ticket-validator.js +44 -183
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +14 -25
- package/.agents/scripts/lib/runtime-deps/dep-resolution.js +155 -0
- package/.agents/scripts/lib/runtime-deps/ensure-installed.js +44 -9
- package/.agents/scripts/lib/runtime-deps/parser-major.js +110 -0
- package/.agents/scripts/lib/runtime-deps/preflight.js +6 -25
- package/.agents/scripts/lib/runtime-deps/scan-imports.js +46 -1
- package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
- package/.agents/scripts/lib/story-body/body-format-lints.js +58 -12
- package/.agents/scripts/lib/story-body/story-body.js +83 -29
- package/.agents/scripts/lib/templates/decomposer-prompts.js +28 -33
- package/.agents/scripts/lib/wave-runner/live-probe.js +31 -5
- package/.agents/scripts/merge-baseline.js +4 -5
- package/.agents/scripts/plan-context.js +117 -28
- package/.agents/scripts/plan-persist.js +79 -39
- package/.agents/scripts/plan-run-epilogue.js +11 -8
- package/.agents/scripts/pr-watch-with-update.js +9 -2
- package/.agents/scripts/run-verify.js +13 -6
- package/.agents/scripts/single-story-init.js +7 -57
- package/.agents/scripts/stories-wave-tick.js +160 -26
- package/.agents/skills/core/gates-and-baselines/reference.md +0 -1
- package/.agents/skills/skills.index.json +2 -12
- package/.agents/skills/stack/qa/playwright/SKILL.md +26 -0
- package/.agents/workflows/audit-to-stories.md +14 -11
- package/.agents/workflows/helpers/acceptance-self-eval.md +84 -157
- package/.agents/workflows/helpers/code-review.md +4 -2
- package/.agents/workflows/helpers/deliver-digest.md +31 -24
- package/.agents/workflows/helpers/deliver-light.md +92 -101
- package/.agents/workflows/helpers/deliver-reference.md +116 -100
- package/.agents/workflows/helpers/deliver-story-reference.md +58 -124
- package/.agents/workflows/helpers/deliver-story.md +17 -18
- package/.agents/workflows/helpers/plan-reference.md +82 -60
- package/.agents/workflows/mandrel-deliver.md +47 -31
- package/.agents/workflows/mandrel-plan.md +32 -30
- package/.agents/workflows/mandrel-update.md +36 -21
- package/README.md +3 -3
- package/docs/CHANGELOG.md +43 -0
- package/lib/cli/registry.js +45 -25
- package/lib/cli/update.js +376 -17
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.60.0-retire-audit-results-autofile.js +40 -0
- package/package.json +8 -2
- package/.agents/schemas/model-attribution.schema.json +0 -53
- package/.agents/scripts/lib/orchestration/model-attribution.js +0 -418
- package/.agents/scripts/lib/orchestration/split-policy-validator.js +0 -188
- package/.agents/scripts/lib/orchestration/story-plan-state.js +0 -33
- package/.agents/scripts/lib/orchestration/structured-comment-parser.js +0 -67
- package/.agents/scripts/lib/templates/spec-author-prompts.js +0 -76
- package/.agents/skills/core/scope-triage/SKILL.md +0 -48
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Story authoring guidance — the two prose constants the story-author prompt
|
|
3
|
-
*
|
|
4
|
-
* cannot drift.
|
|
3
|
+
* cites, stated once so no second copy can drift.
|
|
5
4
|
*
|
|
6
5
|
* Story #5312 deleted the numeric sizing model that used to live beside
|
|
7
6
|
* them: `DEFAULT_MODEL_CAPACITY` with its soft / hard session-mass ceilings,
|
|
@@ -18,12 +17,16 @@
|
|
|
18
17
|
/**
|
|
19
18
|
* `DELIVERABLE_GRANULARITY_GUIDANCE` is the **single source of truth** for the
|
|
20
19
|
* deliverable-granularity definition of a Story (Story #3777). It is stated
|
|
21
|
-
* ONCE here and consumed by
|
|
22
|
-
*
|
|
20
|
+
* ONCE here and consumed by the story-author prompt template.
|
|
21
|
+
*
|
|
22
|
+
* Story #5332 re-anchored the definition off "a single reviewer-sized PR":
|
|
23
|
+
* that anchor read as a size ceiling and fragmented cohesive sweeps, so the
|
|
24
|
+
* only stated sizing test is now cohesion — one coherent change with one
|
|
25
|
+
* reason to exist.
|
|
23
26
|
*/
|
|
24
27
|
export const DELIVERABLE_GRANULARITY_GUIDANCE = Object.freeze({
|
|
25
28
|
definition:
|
|
26
|
-
'A Story is a **capability slice a frontier model delivers and self-verifies in one pass** —
|
|
29
|
+
'A Story is a **capability slice a frontier model delivers and self-verifies in one pass** — one coherent change with one reason to exist, a capability or user-visible surface, **not a single module or file**. Fold module-level slices into the capability they belong to rather than emitting one Story per module. A remediation sweep over one subsystem is one Story; its stages belong in `## Slicing`, not in sibling tickets.',
|
|
27
30
|
singleConsumerRule:
|
|
28
31
|
'**Single-consumer merge rule.** A Story whose only consumer is one sibling Story should be **merged into that sibling** rather than emitted separately — a single-consumer downstream slice is not its own unit of work.',
|
|
29
32
|
envelopeFloor:
|
|
@@ -3,7 +3,6 @@ import { normalizeOwnedProvenance } from '../findings/provenance-field.js';
|
|
|
3
3
|
import { detectCycle } from '../Graph.js';
|
|
4
4
|
import { gitSpawn } from '../git-utils.js';
|
|
5
5
|
|
|
6
|
-
import { Logger } from '../Logger.js';
|
|
7
6
|
import { validateStoryFileAssumptions } from './file-assumptions.js';
|
|
8
7
|
import { isExternalDependencyRef } from './plan-persist/external-deps.js';
|
|
9
8
|
import {
|
|
@@ -41,38 +40,6 @@ function collectPathsFromText(text, paths) {
|
|
|
41
40
|
}
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
/**
|
|
45
|
-
* Resolve every acceptance line a Story declares, across both authoring
|
|
46
|
-
* shapes (Story #4541).
|
|
47
|
-
*
|
|
48
|
-
* The canonical shape is a **serialized string body** with the criteria at
|
|
49
|
-
* the ticket's **top level** — the machine contract persist syncs into the
|
|
50
|
-
* body. `validateAcceptanceSubjectPrefix` used to read `body.acceptance` on
|
|
51
|
-
* an object body only, so on every real plan it scanned nothing and the gate
|
|
52
|
-
* silently passed. Union both sources (deduplicated) so the gate fires on
|
|
53
|
-
* whichever surface the author used.
|
|
54
|
-
*
|
|
55
|
-
* @param {object} story
|
|
56
|
-
* @returns {string[]}
|
|
57
|
-
*/
|
|
58
|
-
function resolveAcceptanceLines(story) {
|
|
59
|
-
const lines = new Set();
|
|
60
|
-
if (Array.isArray(story?.acceptance)) {
|
|
61
|
-
for (const item of story.acceptance) lines.add(String(item ?? ''));
|
|
62
|
-
}
|
|
63
|
-
const body = story?.body;
|
|
64
|
-
let bodyAcceptance = null;
|
|
65
|
-
if (typeof body === 'string' && body.trim().length > 0) {
|
|
66
|
-
bodyAcceptance = parseStoryBodyOrThrow(story).acceptance;
|
|
67
|
-
} else if (body !== null && typeof body === 'object') {
|
|
68
|
-
bodyAcceptance = body.acceptance;
|
|
69
|
-
}
|
|
70
|
-
if (Array.isArray(bodyAcceptance)) {
|
|
71
|
-
for (const item of bodyAcceptance) lines.add(String(item ?? ''));
|
|
72
|
-
}
|
|
73
|
-
return [...lines];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
43
|
function collectTaskPathReferences(task) {
|
|
77
44
|
const paths = new Set();
|
|
78
45
|
const body = task.body;
|
|
@@ -283,117 +250,6 @@ export function validateAcFreshness({
|
|
|
283
250
|
return misses.map((m) => renderMissLine(m, baseBranchRef));
|
|
284
251
|
}
|
|
285
252
|
|
|
286
|
-
/**
|
|
287
|
-
* Allowed leading Conventional-Commits types. Mirrors the `changelog-sections`
|
|
288
|
-
* keys in `release-please-config.json` and the `type-enum` list in
|
|
289
|
-
* `commitlint.config.js`. When a planner LLM prescribes a commit subject in a
|
|
290
|
-
* Task acceptance item via the "Commit subject begins with '<prefix>:'" form,
|
|
291
|
-
* the captured prefix must reduce to one of these types (optionally followed
|
|
292
|
-
* by a `(scope)` qualifier) — anything else fails commitlint locally and
|
|
293
|
-
* release-please's changelog parser on `main`, so the decompose is rejected
|
|
294
|
-
* before the Story branch is ever cut.
|
|
295
|
-
*
|
|
296
|
-
* Epic #2501 introduced this guard after the legacy `baseline-refresh`
|
|
297
|
-
* leading-token prescription created a wave of commit-msg hook failures
|
|
298
|
-
* across story-deliver sub-agents. See
|
|
299
|
-
* `.agents/skills/core/gates-and-baselines/SKILL.md` for the canonical refresh
|
|
300
|
-
* shape (Conventional-Commits subject + `baseline-refresh: true` body
|
|
301
|
-
* trailer).
|
|
302
|
-
*/
|
|
303
|
-
const ALLOWED_COMMIT_TYPES = new Set([
|
|
304
|
-
'feat',
|
|
305
|
-
'fix',
|
|
306
|
-
'chore',
|
|
307
|
-
'refactor',
|
|
308
|
-
'perf',
|
|
309
|
-
'docs',
|
|
310
|
-
'style',
|
|
311
|
-
'test',
|
|
312
|
-
'build',
|
|
313
|
-
'ci',
|
|
314
|
-
'revert',
|
|
315
|
-
]);
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Regex matching the canonical "Commit subject begins with '<prefix>:'"
|
|
319
|
-
* prescription shape the planner emits in `body.acceptance[]` entries.
|
|
320
|
-
* The leading quote is captured loosely (single, double, or backtick) so the
|
|
321
|
-
* three quoting styles the decomposer LLM has historically emitted all
|
|
322
|
-
* match. The captured group is the prefix token *without* the trailing
|
|
323
|
-
* colon — callers normalize by stripping an optional `(scope)` qualifier
|
|
324
|
-
* before comparing against the allowed-types set.
|
|
325
|
-
*/
|
|
326
|
-
const SUBJECT_PREFIX_RE = /Commit subject begins with ['"`]([^'"`]+):['"`]/g;
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Scan every Story's `body.acceptance[]` for "Commit subject begins with
|
|
330
|
-
* '<prefix>:'" prescriptions and reject the decompose when any captured
|
|
331
|
-
* prefix is not a valid Conventional-Commits type.
|
|
332
|
-
*
|
|
333
|
-
* A captured prefix of the form `chore(baselines)` is accepted — the
|
|
334
|
-
* leading `chore` is in the allowed-types set, and the `(scope)` qualifier
|
|
335
|
-
* is the standard Conventional-Commits scope shape. A captured prefix of
|
|
336
|
-
* the form `baseline-refresh` is rejected because no Conventional-Commits
|
|
337
|
-
* type starts with that token.
|
|
338
|
-
*
|
|
339
|
-
* Only acceptance criteria are scanned; `body.goal` / `body.verify` /
|
|
340
|
-
* `body.changes` are not commit-subject prescriptions by convention and
|
|
341
|
-
* scanning them would surface false positives from prose that happens to
|
|
342
|
-
* quote a forbidden prefix while explaining why it's forbidden.
|
|
343
|
-
*
|
|
344
|
-
* Both authoring shapes are covered (Story #4541): the canonical top-level
|
|
345
|
-
* `acceptance[]` on a serialized string body, and the pre-serialize
|
|
346
|
-
* `body.acceptance[]` object shape. Scanning only the latter made the gate
|
|
347
|
-
* inert on every real plan.
|
|
348
|
-
*
|
|
349
|
-
* @param {object} opts
|
|
350
|
-
* @param {object[]} opts.tickets - Validated ticket hierarchy.
|
|
351
|
-
* @throws {ValidationError} when one or more Story acceptance items
|
|
352
|
-
* prescribe a forbidden subject prefix. The error carries
|
|
353
|
-
* `code: 'forbidden-subject-prefix'` and a `violations[]` payload
|
|
354
|
-
* listing each `{ slug, prefix, line }` so the decompose loop can
|
|
355
|
-
* surface the exact offending text to the operator.
|
|
356
|
-
*/
|
|
357
|
-
export function validateAcceptanceSubjectPrefix({ tickets }) {
|
|
358
|
-
const violations = [];
|
|
359
|
-
const stories = (tickets ?? []).filter((t) => t.type === 'story');
|
|
360
|
-
for (const story of stories) {
|
|
361
|
-
for (const line of resolveAcceptanceLines(story)) {
|
|
362
|
-
// Reset the global regex between iterations.
|
|
363
|
-
SUBJECT_PREFIX_RE.lastIndex = 0;
|
|
364
|
-
let match = SUBJECT_PREFIX_RE.exec(line);
|
|
365
|
-
while (match !== null) {
|
|
366
|
-
const rawPrefix = match[1];
|
|
367
|
-
// Strip an optional `(scope)` qualifier — `chore(baselines)` reduces
|
|
368
|
-
// to `chore` for the allowed-types check.
|
|
369
|
-
const type = rawPrefix.replace(/\(.*\)$/, '').trim();
|
|
370
|
-
if (!ALLOWED_COMMIT_TYPES.has(type)) {
|
|
371
|
-
violations.push({
|
|
372
|
-
slug: story.slug ?? '<unknown>',
|
|
373
|
-
prefix: rawPrefix,
|
|
374
|
-
line,
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
match = SUBJECT_PREFIX_RE.exec(line);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
if (violations.length === 0) return;
|
|
382
|
-
const allowed = [...ALLOWED_COMMIT_TYPES].join('|');
|
|
383
|
-
const lines = violations
|
|
384
|
-
.map(
|
|
385
|
-
(v) =>
|
|
386
|
-
` - "${v.slug}" → forbidden subject prefix "${v.prefix}:" in acceptance item: ${v.line}`,
|
|
387
|
-
)
|
|
388
|
-
.join('\n');
|
|
389
|
-
const err = new ValidationError(
|
|
390
|
-
`Cross-Validation Failed: ${violations.length} Story acceptance item(s) prescribe a non-Conventional-Commits subject prefix:\n${lines}\n\nAllowed leading types: ${allowed}. Use a Conventional-Commits subject (e.g. "chore(baselines): refresh ...") and a body trailer (e.g. "baseline-refresh: true") for machine-readable markers. See Epic #2501.`,
|
|
391
|
-
{ violations },
|
|
392
|
-
);
|
|
393
|
-
err.code = 'forbidden-subject-prefix';
|
|
394
|
-
throw err;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
253
|
/**
|
|
398
254
|
* Render one missing-path warning with a remediation hint pointing at the
|
|
399
255
|
* Story's `changes[]`. For `tests/**` paths we suggest the explicit
|
|
@@ -482,44 +338,55 @@ function assertAllTicketsAreStories({ tickets, stories }) {
|
|
|
482
338
|
}
|
|
483
339
|
|
|
484
340
|
/**
|
|
485
|
-
* Return true when a Story
|
|
486
|
-
*
|
|
487
|
-
* implementation unit and
|
|
488
|
-
*
|
|
341
|
+
* Return true when a Story carries a non-empty top-level `acceptance[]` —
|
|
342
|
+
* the inline-contract shape (Epic #3078) where the Story is itself the
|
|
343
|
+
* implementation unit and its criteria live on the Story rather than in
|
|
344
|
+
* child Task tickets.
|
|
489
345
|
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
* inline-contract invariant every Story must satisfy.
|
|
346
|
+
* Story #5342 narrowed the invariant to `acceptance[]` alone. A Story with
|
|
347
|
+
* no observable criterion is genuinely unimplementable and nothing
|
|
348
|
+
* downstream can recover it; an empty `verify[]` only means the deliverer
|
|
349
|
+
* picks the commands, which the close gate chain runs regardless — so that
|
|
350
|
+
* half is a warning ({@link collectMissingVerifyWarnings}), not a refusal.
|
|
496
351
|
*/
|
|
497
|
-
function
|
|
352
|
+
function hasInlineAcceptance(story) {
|
|
498
353
|
if (story === null || typeof story !== 'object') return false;
|
|
499
|
-
const { acceptance
|
|
500
|
-
return (
|
|
501
|
-
Array.isArray(acceptance) &&
|
|
502
|
-
acceptance.length > 0 &&
|
|
503
|
-
Array.isArray(verify) &&
|
|
504
|
-
verify.length > 0
|
|
505
|
-
);
|
|
354
|
+
const { acceptance } = story;
|
|
355
|
+
return Array.isArray(acceptance) && acceptance.length > 0;
|
|
506
356
|
}
|
|
507
357
|
|
|
508
358
|
function assertEveryStoryHasInlineContract({ stories }) {
|
|
509
|
-
|
|
510
|
-
// unit and MUST carry a non-empty inline contract — top-level
|
|
511
|
-
// `acceptance[]` AND `verify[]`. A Story missing either is the legacy
|
|
512
|
-
// 4-tier shape that expected child Tasks; there is no Task tier any
|
|
513
|
-
// more, so such a Story is unimplementable and the decompose is
|
|
514
|
-
// rejected outright.
|
|
515
|
-
const missing = stories.filter((s) => !hasInlineAcceptanceAndVerify(s));
|
|
359
|
+
const missing = stories.filter((s) => !hasInlineAcceptance(s));
|
|
516
360
|
if (missing.length === 0) return;
|
|
517
361
|
const list = missing.map((s) => `"${s.title}" (${s.slug})`).join(', ');
|
|
518
362
|
throw new Error(
|
|
519
|
-
`Cross-Validation Failed: ${missing.length} Story/Stories lack an inline acceptance
|
|
363
|
+
`Cross-Validation Failed: ${missing.length} Story/Stories lack an inline acceptance contract: ${list}. Every Story must carry a non-empty top-level acceptance[] — the outcomes a PR reviewer confirms once it lands.`,
|
|
520
364
|
);
|
|
521
365
|
}
|
|
522
366
|
|
|
367
|
+
/**
|
|
368
|
+
* One warning per Story with no `verify[]` entry (Story #5342).
|
|
369
|
+
*
|
|
370
|
+
* Demoted from the hard refusal above: an absent verify list costs the
|
|
371
|
+
* acceptance critic its cheapest evidence, which is worth saying on the
|
|
372
|
+
* dry-run, but it never makes the Story unimplementable — the deliverer
|
|
373
|
+
* derives the commands and the close gate chain runs either way.
|
|
374
|
+
*
|
|
375
|
+
* @param {object[]} stories
|
|
376
|
+
* @returns {string[]}
|
|
377
|
+
*/
|
|
378
|
+
function collectMissingVerifyWarnings(stories) {
|
|
379
|
+
return (stories ?? [])
|
|
380
|
+
.filter((s) => !Array.isArray(s?.verify) || s.verify.length === 0)
|
|
381
|
+
.map(
|
|
382
|
+
(s) =>
|
|
383
|
+
`Story "${s.slug ?? s.title ?? '<unknown>'}" lists no verify[] entry — ` +
|
|
384
|
+
'the deliverer and the acceptance critic have no mechanical check to ' +
|
|
385
|
+
'read as evidence. Add the exact command or test path unless the ' +
|
|
386
|
+
'Story genuinely has none.',
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
523
390
|
/**
|
|
524
391
|
* Shape-check the optional per-Story `provenance` field (Story #5045).
|
|
525
392
|
*
|
|
@@ -619,19 +486,12 @@ export function validateAndNormalizeTickets(tickets, opts = {}) {
|
|
|
619
486
|
assertAcyclic(slugAdjacency);
|
|
620
487
|
|
|
621
488
|
// Story #4541 — refuse an unparseable Story body up front, with a named
|
|
622
|
-
// error pointing at the offending section + entry. Must precede
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
// Story had legitimately declared.
|
|
489
|
+
// error pointing at the offending section + entry. Must precede the
|
|
490
|
+
// freshness gate: it parses the body, and its net-new whitelist comes from
|
|
491
|
+
// `body.changes`, so a malformed body used to surface as a stale-path miss
|
|
492
|
+
// naming the paths the Story had legitimately declared.
|
|
627
493
|
assertStoryBodiesParse({ tickets });
|
|
628
494
|
|
|
629
|
-
// Reject any Task acceptance item that prescribes a non-Conventional-Commits
|
|
630
|
-
// subject prefix (e.g. legacy "Commit subject begins with 'baseline-refresh:'"
|
|
631
|
-
// from pre-Epic-#2501 planner output). Runs before the freshness gate so
|
|
632
|
-
// the failure mode is reported up-front rather than after a git probe.
|
|
633
|
-
validateAcceptanceSubjectPrefix({ tickets });
|
|
634
|
-
|
|
635
495
|
// Hoist a single memoized (ref, path) → boolean probe shared across both
|
|
636
496
|
// git-probe gates below. Without this, `validateAcFreshness` and
|
|
637
497
|
// `validateStoryFileAssumptions` each maintain an independent cache, so a
|
|
@@ -643,7 +503,7 @@ export function validateAndNormalizeTickets(tickets, opts = {}) {
|
|
|
643
503
|
? makeMemoizedGitRunner(opts.gitRunner ?? defaultGitRunner)
|
|
644
504
|
: null;
|
|
645
505
|
|
|
646
|
-
const warnings = [];
|
|
506
|
+
const warnings = [...collectMissingVerifyWarnings(stories)];
|
|
647
507
|
// Story #5312: a goal / acceptance / verify path absent at base is a
|
|
648
508
|
// warning the dry-run lists, not a refusal. Skipped when the caller omits
|
|
649
509
|
// `baseBranchRef` so unit tests keep their semantics; production
|
|
@@ -702,5 +562,6 @@ export const _internal = {
|
|
|
702
562
|
assertNoUnknownDeps,
|
|
703
563
|
assertAcyclic,
|
|
704
564
|
attachFindingsAndErrors,
|
|
705
|
-
|
|
565
|
+
hasInlineAcceptance,
|
|
566
|
+
collectMissingVerifyWarnings,
|
|
706
567
|
};
|
|
@@ -77,20 +77,19 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
|
|
|
77
77
|
'follow-ups',
|
|
78
78
|
// v2 plan-run epilogue artifacts (posted on the primary Story)
|
|
79
79
|
'plan-run-audit-roster',
|
|
80
|
-
'plan-run-sibling-coherence',
|
|
81
80
|
'epic-run-state',
|
|
82
81
|
'epic-run-progress',
|
|
83
82
|
'epic-plan-state',
|
|
84
83
|
// `parked-follow-ons` retired in Story #5114 with the module that was its
|
|
85
|
-
// only writer
|
|
86
|
-
//
|
|
84
|
+
// only writer, and `plan-run-sibling-coherence` in Story #5341 with the
|
|
85
|
+
// epilogue step that was its. Story #5367 retired two more on the same
|
|
86
|
+
// rule: `story-init` (its write went away in #5343 — the receipt is read
|
|
87
|
+
// off disk now) and `model-attribution` (its writer went with the per-Task
|
|
88
|
+
// progress writer in #3157, and its reader modules are deleted). A kind the
|
|
89
|
+
// reader still recognises but nothing emits is the same dead wiring in a
|
|
90
|
+
// new place.
|
|
87
91
|
// Story #566 — per-phase wall-clock summary posted by single-story-close.js.
|
|
88
92
|
'phase-timings',
|
|
89
|
-
// Story #831 — story-init upserts a `story-init` comment that
|
|
90
|
-
// surfaces `dependenciesInstalled` (and the underlying installStatus) so
|
|
91
|
-
// downstream workflow steps don't have to infer install state from
|
|
92
|
-
// node_modules presence.
|
|
93
|
-
'story-init',
|
|
94
93
|
// Story #2128 — Phase 6 Epic Clarity Gate (CLI retired). Historical
|
|
95
94
|
// `clarity-gate-update` comments may still exist on older tickets.
|
|
96
95
|
'clarity-gate-update',
|
|
@@ -100,16 +99,6 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
|
|
|
100
99
|
// operator can correct drift before Phase 8 decomposes from a stale
|
|
101
100
|
// spec. Advisory: the run continues regardless of the report contents.
|
|
102
101
|
'spec-freshness',
|
|
103
|
-
// Story #2813 — the per-Task progress writer (since retired under
|
|
104
|
-
// #3157) upserted a `model-attribution` comment on a Task ticket at
|
|
105
|
-
// the moment it transitioned to `agent::executing`, recording which
|
|
106
|
-
// Claude model was actively executing the work. One entry per Task
|
|
107
|
-
// (upsert is idempotent across resume re-runs). Story- and Epic-level
|
|
108
|
-
// rollups are computed at query time by `rollupModelAttribution` in
|
|
109
|
-
// `lib/orchestration/model-attribution.js` — no Story/Epic-scope
|
|
110
|
-
// emissions are written. Schema:
|
|
111
|
-
// `.agents/schemas/model-attribution.schema.json`.
|
|
112
|
-
'model-attribution',
|
|
113
102
|
// Story #2894 — `finalize/post-handoff-comment.js` upserts an
|
|
114
103
|
// `epic-handoff` comment on the Epic at the end of the bus-owned
|
|
115
104
|
// finalize flow (after `open-or-locate-pr`
|
|
@@ -157,13 +146,13 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
|
|
|
157
146
|
// `graduator="<name>"` attr so independent graduators do not clobber each
|
|
158
147
|
// other's comment; re-runs upsert in place.
|
|
159
148
|
'cross-repo-deferred',
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
//
|
|
166
|
-
//
|
|
149
|
+
// v2 Stage 3 — the flat Story persist comment on every created Story
|
|
150
|
+
// (replaces epic-plan-state for new plans). Since Story #5343 it is the
|
|
151
|
+
// ONLY comment persist posts: the primary-Story-only `plan-summary` marker
|
|
152
|
+
// was retired and its content — story set, delivery order, deliver command
|
|
153
|
+
// — rides this marker on every Story. Story #5367 deleted the machine
|
|
154
|
+
// checkpoint that used to lead the body; the marker remains, and it is what
|
|
155
|
+
// makes a re-persist upsert in place.
|
|
167
156
|
'story-plan-state',
|
|
168
157
|
// Story #4535 — `plan-persist.js` upserts a `superseded-by` comment on
|
|
169
158
|
// each `/mandrel-plan --tickets` source issue at persist time, naming the single
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runtime-deps/dep-resolution — is a declared runtime dependency actually
|
|
3
|
+
* there, is it the right major, and how do we say so.
|
|
4
|
+
*
|
|
5
|
+
* `.agents/` materializes into the consumer's repository root, so every
|
|
6
|
+
* framework runtime dependency resolves from *their* `node_modules`. A range
|
|
7
|
+
* in `.agents/runtime-deps.json` therefore documents a requirement it cannot
|
|
8
|
+
* enforce, and the preflight guard needs to compare the range against what
|
|
9
|
+
* actually resolved.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately major-only, and deliberately not `semver`. The framework's
|
|
12
|
+
* runtime ranges are all `^`, whose whole contract is "this major"; pulling in
|
|
13
|
+
* a semver implementation to decide one comparison would add a dependency to
|
|
14
|
+
* the very closure this module exists to keep honest.
|
|
15
|
+
*
|
|
16
|
+
* @module lib/runtime-deps/dep-resolution
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Leading major number of a version or a caret/tilde range, or `null`.
|
|
21
|
+
*
|
|
22
|
+
* Module-local: `majorMismatch` is the only question callers have.
|
|
23
|
+
*
|
|
24
|
+
* Anything this cannot read as `<major>.` — `*`, a tag, a git URL, a
|
|
25
|
+
* `>=x <y` span — yields `null` and is treated as "not range-checked". That
|
|
26
|
+
* asymmetry is intentional: a conservative miss is a no-op, while a false
|
|
27
|
+
* positive blocks a working install.
|
|
28
|
+
*
|
|
29
|
+
* @param {string|null|undefined} spec
|
|
30
|
+
* @returns {number|null}
|
|
31
|
+
*/
|
|
32
|
+
function majorOf(spec) {
|
|
33
|
+
if (typeof spec !== 'string') return null;
|
|
34
|
+
const match = /^[\^~]?(\d+)\./.exec(spec.trim());
|
|
35
|
+
return match ? Number(match[1]) : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Does `resolved` sit outside the major `range` names?
|
|
40
|
+
*
|
|
41
|
+
* Module-local: `checkRuntimeDeps` is the only caller, and exporting it only
|
|
42
|
+
* for a test would be a production-dead export.
|
|
43
|
+
*
|
|
44
|
+
* `false` whenever either side is unreadable, so an unparseable range or an
|
|
45
|
+
* unreadable installed version is never reported as a mismatch.
|
|
46
|
+
*
|
|
47
|
+
* `0.x` majors compare as written: `^0.1.0` and `0.2.1` differ in minor, not
|
|
48
|
+
* major, so this does not separate them. Accepted — the `0.x` packages in the
|
|
49
|
+
* closure are terminal, and the range this exists to enforce is
|
|
50
|
+
* `@babel/parser`'s `^7`.
|
|
51
|
+
*
|
|
52
|
+
* @param {string|null|undefined} range
|
|
53
|
+
* @param {string|null|undefined} resolved
|
|
54
|
+
* @returns {boolean}
|
|
55
|
+
*/
|
|
56
|
+
function majorMismatch(range, resolved) {
|
|
57
|
+
const want = majorOf(range);
|
|
58
|
+
if (want === null) return false;
|
|
59
|
+
const got = majorOf(resolved);
|
|
60
|
+
if (got === null) return false;
|
|
61
|
+
return want !== got;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Is a package present in the resolvable tree?
|
|
66
|
+
*
|
|
67
|
+
* The bare specifier is tried first, then `<name>/package.json`. The fallback
|
|
68
|
+
* is not belt-and-braces: a package with no `main` and no `exports` — which
|
|
69
|
+
* `typhonjs-escomplex-commons` and `babel-runtime` both are — cannot be
|
|
70
|
+
* resolved by name at all, and is reached only by deep path. Probing the bare
|
|
71
|
+
* name alone would report such a package missing while it sits installed, and
|
|
72
|
+
* this guard exits the process on that verdict.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} dep
|
|
75
|
+
* @param {(specifier: string) => string} resolve
|
|
76
|
+
* @returns {boolean}
|
|
77
|
+
*/
|
|
78
|
+
export function isResolvable(dep, resolve) {
|
|
79
|
+
try {
|
|
80
|
+
resolve(dep);
|
|
81
|
+
return true;
|
|
82
|
+
} catch {
|
|
83
|
+
// fall through to the manifest probe
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
resolve(`${dep}/package.json`);
|
|
87
|
+
return true;
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Remediation text for a resolved dependency whose major differs from the
|
|
95
|
+
* range the framework declares.
|
|
96
|
+
*
|
|
97
|
+
* Named separately from the missing-deps message because the remedy differs:
|
|
98
|
+
* the package is installed, so installing again changes nothing. What is
|
|
99
|
+
* wrong is the version the consumer's own tree resolves, which only they can
|
|
100
|
+
* change.
|
|
101
|
+
*
|
|
102
|
+
* @param {{name: string, required: string, resolved: string}[]} mismatched
|
|
103
|
+
* @param {{ root: string }} ctx
|
|
104
|
+
* @returns {string}
|
|
105
|
+
*/
|
|
106
|
+
export function formatMismatchedDepsMessage(mismatched, { root }) {
|
|
107
|
+
const lines = mismatched.map(
|
|
108
|
+
(m) => ` - ${m.name}: need ${m.required}, resolved ${m.resolved}`,
|
|
109
|
+
);
|
|
110
|
+
return [
|
|
111
|
+
'Mandrel framework runtime dependency version mismatch:',
|
|
112
|
+
...lines,
|
|
113
|
+
'',
|
|
114
|
+
`Resolved from: ${root}`,
|
|
115
|
+
'These packages are resolved from your repository, not from mandrel, so',
|
|
116
|
+
'the version your tree installs is the version the framework gets. Pin a',
|
|
117
|
+
'compatible major in your package.json and reinstall.',
|
|
118
|
+
].join('\n');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Resolve each required package via the injected `resolve` seam and collect
|
|
123
|
+
* the ones that fail. `resolve` is typically `require.resolve` bound to the
|
|
124
|
+
* framework module location; it throws `MODULE_NOT_FOUND` when a package is
|
|
125
|
+
* absent from the resolvable `node_modules`.
|
|
126
|
+
*
|
|
127
|
+
* @param {{ required: string[], resolve: (specifier: string) => string }} opts
|
|
128
|
+
* @returns {{ ok: boolean, missing: string[] }}
|
|
129
|
+
*/
|
|
130
|
+
export function checkRuntimeDeps({
|
|
131
|
+
required,
|
|
132
|
+
resolve,
|
|
133
|
+
ranges = null,
|
|
134
|
+
readVersion = null,
|
|
135
|
+
}) {
|
|
136
|
+
const missing = [];
|
|
137
|
+
const mismatched = [];
|
|
138
|
+
for (const dep of required) {
|
|
139
|
+
if (!isResolvable(dep, resolve)) {
|
|
140
|
+
missing.push(dep);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (!ranges || !readVersion) continue;
|
|
144
|
+
const range = ranges[dep];
|
|
145
|
+
const resolved = readVersion(dep);
|
|
146
|
+
if (majorMismatch(range, resolved)) {
|
|
147
|
+
mismatched.push({ name: dep, required: range, resolved });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
ok: missing.length === 0 && mismatched.length === 0,
|
|
152
|
+
missing,
|
|
153
|
+
mismatched,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -28,12 +28,13 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { createRequire } from 'node:module';
|
|
31
|
-
import {
|
|
31
|
+
import { resolveDependencyVersion } from '../dependency-version.js';
|
|
32
32
|
import {
|
|
33
33
|
checkRuntimeDeps,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
} from './
|
|
34
|
+
formatMismatchedDepsMessage,
|
|
35
|
+
} from './dep-resolution.js';
|
|
36
|
+
import { loadRuntimeDepsManifest } from './manifest.js';
|
|
37
|
+
import { detectPackageManager, formatMissingDepsMessage } from './preflight.js';
|
|
37
38
|
|
|
38
39
|
// `require.resolve` bound to this module's location walks `node_modules`
|
|
39
40
|
// upward from `.agents/scripts/lib/runtime-deps/` to the consumer root —
|
|
@@ -50,7 +51,8 @@ const frameworkRequire = createRequire(import.meta.url);
|
|
|
50
51
|
* cwd?: string,
|
|
51
52
|
* stderr?: { write: (s: string) => void },
|
|
52
53
|
* exit?: (code: number) => void,
|
|
53
|
-
* manifest?: { required: string[] },
|
|
54
|
+
* manifest?: { required: string[], dependencies?: Record<string,string> },
|
|
55
|
+
* readVersion?: (name: string) => string | null,
|
|
54
56
|
* }} [opts]
|
|
55
57
|
* @returns {{ ok: boolean, missing: string[] }}
|
|
56
58
|
*/
|
|
@@ -61,6 +63,7 @@ export function ensureRuntimeDepsInstalled(opts = {}) {
|
|
|
61
63
|
stderr = process.stderr,
|
|
62
64
|
exit = process.exit,
|
|
63
65
|
manifest = safeLoadManifest(),
|
|
66
|
+
readVersion,
|
|
64
67
|
} = opts;
|
|
65
68
|
|
|
66
69
|
// A manifest we cannot read is a packaging defect the drift test owns —
|
|
@@ -70,17 +73,49 @@ export function ensureRuntimeDepsInstalled(opts = {}) {
|
|
|
70
73
|
const result = checkRuntimeDeps({
|
|
71
74
|
required: manifest.required,
|
|
72
75
|
resolve: requireResolve,
|
|
76
|
+
ranges: manifest.dependencies ?? null,
|
|
77
|
+
readVersion: readVersion ?? defaultReadVersion,
|
|
73
78
|
});
|
|
74
79
|
if (result.ok) return result;
|
|
75
80
|
|
|
76
|
-
|
|
77
|
-
stderr.write(
|
|
78
|
-
`${formatMissingDepsMessage(result.missing, { root: cwd, packageManager })}\n`,
|
|
79
|
-
);
|
|
81
|
+
stderr.write(`${describeFailure(result, cwd)}\n`);
|
|
80
82
|
exit(1);
|
|
81
83
|
return result;
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Remediation text for a failed check.
|
|
88
|
+
*
|
|
89
|
+
* Absence is reported first: a package that is not installed cannot have a
|
|
90
|
+
* version, and installing it is the prerequisite for any version complaint
|
|
91
|
+
* being actionable.
|
|
92
|
+
*
|
|
93
|
+
* @param {{ missing: string[], mismatched: {name: string, required: string, resolved: string}[] }} result
|
|
94
|
+
* @param {string} cwd
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
function describeFailure(result, cwd) {
|
|
98
|
+
if (result.missing.length === 0) {
|
|
99
|
+
return formatMismatchedDepsMessage(result.mismatched, { root: cwd });
|
|
100
|
+
}
|
|
101
|
+
const packageManager = detectPackageManager(cwd);
|
|
102
|
+
return formatMissingDepsMessage(result.missing, {
|
|
103
|
+
root: cwd,
|
|
104
|
+
packageManager,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Read a resolved package's version through the framework's own resolution,
|
|
110
|
+
* so the version checked is the one the framework's imports will load.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} name
|
|
113
|
+
* @returns {string | null}
|
|
114
|
+
*/
|
|
115
|
+
function defaultReadVersion(name) {
|
|
116
|
+
return resolveDependencyVersion(name, frameworkRequire);
|
|
117
|
+
}
|
|
118
|
+
|
|
84
119
|
/**
|
|
85
120
|
* Load the manifest, swallowing a read/parse failure to `null` so the guard
|
|
86
121
|
* stays inert on a packaging defect (see `ensureRuntimeDepsInstalled`).
|