mandrel 2.59.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 +11 -9
- package/.agents/agents/acceptance-critic.md +24 -43
- package/.agents/agents/story-worker.md +18 -19
- package/.agents/docs/SDLC.md +6 -6
- package/.agents/docs/agentrc-reference.json +1 -2
- package/.agents/docs/configuration.md +29 -46
- package/.agents/docs/quality-gates.md +8 -4
- package/.agents/docs/workflows.md +1 -1
- package/.agents/instructions.md +4 -5
- package/.agents/rules/ci-remediation.md +41 -8
- package/.agents/rules/known-tooling-behavior.md +65 -15
- package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
- package/.agents/schemas/agentrc.schema.json +6 -11
- 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/lib/baselines/crap-preview-incremental.js +6 -2
- 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/doc-tiers.js +4 -2
- 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/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 +13 -25
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +8 -6
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +76 -95
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +35 -18
- 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/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.js +44 -183
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +14 -25
- 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 +7 -15
- 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 -28
- 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 -2
- package/.agents/skills/stack/qa/playwright/SKILL.md +26 -0
- 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 +65 -54
- package/.agents/workflows/mandrel-deliver.md +47 -31
- package/.agents/workflows/mandrel-plan.md +22 -21
- package/.agents/workflows/mandrel-update.md +36 -21
- package/docs/CHANGELOG.md +35 -0
- 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 +2 -1
- package/.agents/schemas/model-attribution.schema.json +0 -53
- package/.agents/scripts/lib/orchestration/model-attribution.js +0 -418
- package/.agents/scripts/lib/orchestration/story-plan-state.js +0 -33
- package/.agents/scripts/lib/orchestration/structured-comment-parser.js +0 -67
package/lib/cli/update.js
CHANGED
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
* materialized `.agents/` state (Story #4065). When already on newest AND
|
|
17
17
|
* no drift: nothing to do (true no-op). When already on newest BUT drift
|
|
18
18
|
* detected: skip npm-update/migrations, run sync + sync-commands to heal.
|
|
19
|
-
* 3. install — bump the dependency (lockfile
|
|
19
|
+
* 3. install — bump the dependency (manifest + lockfile rewritten on
|
|
20
|
+
* disk; whether the package manager also stages them is reported, never
|
|
21
|
+
* assumed — see § No git mutation).
|
|
20
22
|
* The package manager is auto-detected from the lockfile in the project
|
|
21
23
|
* root: `pnpm-lock.yaml` ⇒ `pnpm add -D …` (with `-w` at a
|
|
22
24
|
* `pnpm-workspace.yaml` root), `yarn.lock` ⇒ `yarn add -D …`, otherwise
|
|
@@ -56,10 +58,29 @@
|
|
|
56
58
|
*
|
|
57
59
|
* ## No git mutation
|
|
58
60
|
*
|
|
59
|
-
* The
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
61
|
+
* The dependency bump rewrites `package.json` / the lockfile in the working
|
|
62
|
+
* tree but the orchestrator performs **no** `git add` / `git commit`. This
|
|
63
|
+
* module **reads git state to report it; never mutates it** (Story #5339).
|
|
64
|
+
*
|
|
65
|
+
* The final success line used to assert "the lockfile bump is staged for
|
|
66
|
+
* review" unconditionally. That is only ever true where `npm install` happens
|
|
67
|
+
* to stage; the `pnpm add` / `yarn add` seams stage nothing, so on a pnpm
|
|
68
|
+
* consumer the operator was told to review an empty index while the manifest,
|
|
69
|
+
* the lockfile and every re-materialized `.agents/` path sat unstaged — an
|
|
70
|
+
* invitation to commit a half-upgrade. The orchestrator now probes the real
|
|
71
|
+
* index through the injectable `gitStatus` seam — two read-only calls, a
|
|
72
|
+
* pathspec-anchored `git status --porcelain` plus `git ls-files .agents` — and
|
|
73
|
+
* prints one of three truthful lines: staged, not staged (with the exact
|
|
74
|
+
* `git add` command), or a neutral "review the working tree" line when git is
|
|
75
|
+
* unavailable or the probe fails. A failed probe never fails the update: the
|
|
76
|
+
* run still exits 0.
|
|
77
|
+
*
|
|
78
|
+
* **Staged means both halves** (Story #5364): the index differs from HEAD
|
|
79
|
+
* *and* the worktree agrees with the index. Reading only the index reported a
|
|
80
|
+
* manifest pair the operator staged *before* running the command as staged
|
|
81
|
+
* afterwards, even though the install had since rewritten both files on disk —
|
|
82
|
+
* the exact false claim this report exists to remove. The drift-heal path
|
|
83
|
+
* reports the re-materialized payload the same way.
|
|
63
84
|
*
|
|
64
85
|
* ## `--dry-run`
|
|
65
86
|
*
|
|
@@ -99,6 +120,16 @@
|
|
|
99
120
|
* sole post-install execution path. See § Re-exec of
|
|
100
121
|
* post-install phases.
|
|
101
122
|
* - `surfaceChangelog` — emits the target changelog section
|
|
123
|
+
* - `gitStatus` — read-only git probe backing the staging report;
|
|
124
|
+
* receives `({ cwd, lockfile })` and returns
|
|
125
|
+
* `{ ok, stagedManifest, stagedLockfile,
|
|
126
|
+
* stagedPayload, tracksAgents }` (Story #5339,
|
|
127
|
+
* #5364). Never mutates the repository.
|
|
128
|
+
* - `detectLockfile` — resolves the lockfile name the staging report
|
|
129
|
+
* names — and the probe's pathspec uses — from the
|
|
130
|
+
* uncoerced package-manager probe, so a bun
|
|
131
|
+
* consumer is named its real lockfile; receives
|
|
132
|
+
* `(cwd)`
|
|
102
133
|
* - `write` / `writeErr` — stdout / stderr sinks
|
|
103
134
|
* - `exit` — process.exit replacement
|
|
104
135
|
* - `cwd` — process.cwd() replacement (used to resolve the
|
|
@@ -374,6 +405,26 @@ function repairInstallCommand(packageManager) {
|
|
|
374
405
|
* @returns {{ packageManager: 'pnpm' | 'yarn' | 'npm', workspaceRoot: boolean }}
|
|
375
406
|
*/
|
|
376
407
|
export function detectPackageManager(cwd = process.cwd(), fs = nodeFs) {
|
|
408
|
+
const result = probePackageManager(cwd, fs);
|
|
409
|
+
// Coerce `bun` → `npm` because this orchestrator's install-command builder
|
|
410
|
+
// only handles pnpm / yarn / npm today.
|
|
411
|
+
const packageManager =
|
|
412
|
+
result.packageManager === 'bun' ? 'npm' : result.packageManager;
|
|
413
|
+
return { packageManager, workspaceRoot: result.workspaceRoot };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* The raw, **uncoerced** package-manager probe. `detectPackageManager` above
|
|
418
|
+
* flattens `bun` to `npm` for the install-command builder's benefit; the
|
|
419
|
+
* staging report must not inherit that flattening, or a bun consumer is told
|
|
420
|
+
* to stage a `package-lock.json` its tree does not have (Story #5364). Reading
|
|
421
|
+
* the probe before the coercion is the whole fix.
|
|
422
|
+
*
|
|
423
|
+
* @param {string} cwd - Project root to probe.
|
|
424
|
+
* @param {typeof nodeFs} [fs]
|
|
425
|
+
* @returns {{ packageManager: 'pnpm'|'yarn'|'bun'|'npm', workspaceRoot: boolean }}
|
|
426
|
+
*/
|
|
427
|
+
function probePackageManager(cwd, fs = nodeFs) {
|
|
377
428
|
const exists = (p) => {
|
|
378
429
|
try {
|
|
379
430
|
return fs.existsSync(p);
|
|
@@ -381,12 +432,7 @@ export function detectPackageManager(cwd = process.cwd(), fs = nodeFs) {
|
|
|
381
432
|
return false;
|
|
382
433
|
}
|
|
383
434
|
};
|
|
384
|
-
|
|
385
|
-
// Coerce `bun` → `npm` because this orchestrator's install-command builder
|
|
386
|
-
// only handles pnpm / yarn / npm today.
|
|
387
|
-
const packageManager =
|
|
388
|
-
result.packageManager === 'bun' ? 'npm' : result.packageManager;
|
|
389
|
-
return { packageManager, workspaceRoot: result.workspaceRoot };
|
|
435
|
+
return detectPackageManagerWithWorkspace(cwd, exists);
|
|
390
436
|
}
|
|
391
437
|
|
|
392
438
|
/**
|
|
@@ -434,10 +480,292 @@ export function resolveInstallCmd(
|
|
|
434
480
|
return `npm install ${PACKAGE_NAME}@${target}`;
|
|
435
481
|
}
|
|
436
482
|
|
|
483
|
+
/**
|
|
484
|
+
* The lockfile each supported package manager writes. The staging report names
|
|
485
|
+
* the detected one so the `git add` hint is copy-pasteable in the consumer's
|
|
486
|
+
* real tree rather than always saying `package-lock.json` (Story #5339).
|
|
487
|
+
*/
|
|
488
|
+
const LOCKFILE_BY_PACKAGE_MANAGER = {
|
|
489
|
+
pnpm: 'pnpm-lock.yaml',
|
|
490
|
+
yarn: 'yarn.lock',
|
|
491
|
+
bun: 'bun.lockb',
|
|
492
|
+
npm: 'package-lock.json',
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
/** The manifest the dependency bump rewrites alongside the lockfile. */
|
|
496
|
+
const MANIFEST_FILENAME = 'package.json';
|
|
497
|
+
|
|
498
|
+
/** The materialized payload directory the sync phase rewrites. */
|
|
499
|
+
const AGENTS_DIR = '.agents';
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Default `detectLockfile` seam: resolve the lockfile name for the staging
|
|
503
|
+
* report from the **uncoerced** package-manager probe (`pnpm-lock.yaml` /
|
|
504
|
+
* `yarn.lock` / `bun.lockb` / `package-lock.json`). Reading
|
|
505
|
+
* `probePackageManager` rather than `detectPackageManager` is deliberate: the
|
|
506
|
+
* latter flattens `bun` to `npm` so the install-command builder has a command
|
|
507
|
+
* to emit, and inheriting that here would name a lockfile the consumer's tree
|
|
508
|
+
* does not carry (Story #5364). A directory with no recognizable toolchain
|
|
509
|
+
* still resolves to `npm`, so the report always names a concrete file.
|
|
510
|
+
*
|
|
511
|
+
* @param {string} cwd - Consumer project root.
|
|
512
|
+
* @param {typeof nodeFs} [fs]
|
|
513
|
+
* @returns {string}
|
|
514
|
+
*/
|
|
515
|
+
export function defaultDetectLockfile(cwd, fs = nodeFs) {
|
|
516
|
+
const { packageManager } = probePackageManager(cwd, fs);
|
|
517
|
+
return LOCKFILE_BY_PACKAGE_MANAGER[packageManager] ?? 'package-lock.json';
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** The line printed when the probe cannot report the index at all. */
|
|
521
|
+
const NEUTRAL_STAGING_LINE =
|
|
522
|
+
'Review the working tree and commit the bump (git not available to report staging state).';
|
|
523
|
+
|
|
524
|
+
/** What the probe reports when it cannot read the index at all. */
|
|
525
|
+
const DEGRADED_GIT_STATE = Object.freeze({
|
|
526
|
+
ok: false,
|
|
527
|
+
stagedManifest: false,
|
|
528
|
+
stagedLockfile: false,
|
|
529
|
+
stagedPayload: false,
|
|
530
|
+
tracksAgents: false,
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
/** True when a `spawnSync` result is a clean, non-throwing exit. */
|
|
534
|
+
const spawnOk = (result) =>
|
|
535
|
+
Boolean(result) && !result.error && result.status === 0;
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* The path one `git status --porcelain` v1 record reports as **staged**, or
|
|
539
|
+
* `null` when that record is not staged (or carries no path).
|
|
540
|
+
*
|
|
541
|
+
* Staged means both halves of the contract (Story #5364): the index differs
|
|
542
|
+
* from HEAD *and* the worktree agrees with the index. Porcelain v1 states that
|
|
543
|
+
* as `XY<space>path` — `X` the index column, `Y` the worktree column — so a
|
|
544
|
+
* path the operator staged before the install and the install then rewrote on
|
|
545
|
+
* disk reports `MM` and is correctly NOT staged. `?` in the index column is an
|
|
546
|
+
* untracked file, which is never staged.
|
|
547
|
+
*
|
|
548
|
+
* A rename carries `orig -> new`; the post-rename path is the one the operator
|
|
549
|
+
* stages. Porcelain quotes paths containing unusual bytes, so the quoting is
|
|
550
|
+
* stripped.
|
|
551
|
+
*
|
|
552
|
+
* @param {string} record
|
|
553
|
+
* @returns {string | null}
|
|
554
|
+
*/
|
|
555
|
+
function stagedPathFromPorcelain(record) {
|
|
556
|
+
if (record.length < 4 || record[1] !== ' ') return null;
|
|
557
|
+
if (record[0] === ' ' || record[0] === '?') return null;
|
|
558
|
+
const raw = record.slice(3).trim();
|
|
559
|
+
const arrow = raw.lastIndexOf(' -> ');
|
|
560
|
+
const pathPart = arrow === -1 ? raw : raw.slice(arrow + 4);
|
|
561
|
+
const path =
|
|
562
|
+
pathPart.startsWith('"') && pathPart.endsWith('"')
|
|
563
|
+
? pathPart.slice(1, -1)
|
|
564
|
+
: pathPart;
|
|
565
|
+
return path.length > 0 ? path : null;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Which slot of the probe state one staged path fills, or `null` for a path
|
|
570
|
+
* that fills none.
|
|
571
|
+
*
|
|
572
|
+
* The payload test is anchored on a path separator, so a sibling like
|
|
573
|
+
* `my.agents/` never matches, and it runs first: a `package.json` *inside*
|
|
574
|
+
* `.agents/` is payload, never the consumer's root manifest. The manifest and
|
|
575
|
+
* lockfile tests tolerate one leading prefix because git reports porcelain
|
|
576
|
+
* paths relative to the repository root, which need not be the probe root —
|
|
577
|
+
* safe only because the caller's pathspec already anchored the query there, so
|
|
578
|
+
* a staged `packages/app/package.json` never reaches this function at all
|
|
579
|
+
* (Story #5364).
|
|
580
|
+
*
|
|
581
|
+
* @param {string} path
|
|
582
|
+
* @param {string} lockfile
|
|
583
|
+
* @returns {'stagedPayload' | 'stagedManifest' | 'stagedLockfile' | null}
|
|
584
|
+
*/
|
|
585
|
+
function stagedSlotFor(path, lockfile) {
|
|
586
|
+
if (path === AGENTS_DIR || /(^|\/)\.agents\//.test(path))
|
|
587
|
+
return 'stagedPayload';
|
|
588
|
+
const isRootFile = (name) => path === name || path.endsWith(`/${name}`);
|
|
589
|
+
if (isRootFile(MANIFEST_FILENAME)) return 'stagedManifest';
|
|
590
|
+
return isRootFile(lockfile) ? 'stagedLockfile' : null;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Default `gitStatus` seam: probe the consumer repository's real index state.
|
|
595
|
+
*
|
|
596
|
+
* **Two** read-only plumbing calls, no mutation of any kind (§ No git
|
|
597
|
+
* mutation):
|
|
598
|
+
* - `git status --porcelain -- package.json <lockfile> .agents` — one read
|
|
599
|
+
* that answers every branch the report chooses between. The pathspecs are
|
|
600
|
+
* resolved relative to `cwd`, which anchors the query at the probe root:
|
|
601
|
+
* a nested workspace manifest is simply not in the output. Both status
|
|
602
|
+
* columns come back, so "staged" can mean index-differs-**and**-worktree-
|
|
603
|
+
* clean rather than the index alone.
|
|
604
|
+
* - `git ls-files .agents` — whether the consumer *tracks* the materialized
|
|
605
|
+
* payload, which decides whether the `git add` hint names it.
|
|
606
|
+
*
|
|
607
|
+
* Any failure — git absent, not a repository, a non-zero exit, a throw from the
|
|
608
|
+
* spawn boundary — degrades to `{ ok: false }` so the caller prints the neutral
|
|
609
|
+
* line and the update still exits 0. The probe never throws.
|
|
610
|
+
*
|
|
611
|
+
* Security (security-baseline § Output & Rendering): every argv segment here is
|
|
612
|
+
* a fixed vector or a lockfile name drawn from `LOCKFILE_BY_PACKAGE_MANAGER`,
|
|
613
|
+
* never operator input, and no shell is used.
|
|
614
|
+
*
|
|
615
|
+
* @param {{
|
|
616
|
+
* cwd?: string,
|
|
617
|
+
* lockfile?: string,
|
|
618
|
+
* spawnSync?: typeof spawnSync,
|
|
619
|
+
* }} [opts]
|
|
620
|
+
* @returns {{
|
|
621
|
+
* ok: boolean,
|
|
622
|
+
* stagedManifest: boolean,
|
|
623
|
+
* stagedLockfile: boolean,
|
|
624
|
+
* stagedPayload: boolean,
|
|
625
|
+
* tracksAgents: boolean,
|
|
626
|
+
* }}
|
|
627
|
+
*/
|
|
628
|
+
export function defaultGitStatus({
|
|
629
|
+
cwd = process.cwd(),
|
|
630
|
+
lockfile = LOCKFILE_BY_PACKAGE_MANAGER.npm,
|
|
631
|
+
spawnSync: spawn = spawnSync,
|
|
632
|
+
} = {}) {
|
|
633
|
+
const run = (args) => spawn('git', args, { cwd, encoding: 'utf8' });
|
|
634
|
+
try {
|
|
635
|
+
const status = run([
|
|
636
|
+
'status',
|
|
637
|
+
'--porcelain',
|
|
638
|
+
'--',
|
|
639
|
+
MANIFEST_FILENAME,
|
|
640
|
+
lockfile,
|
|
641
|
+
AGENTS_DIR,
|
|
642
|
+
]);
|
|
643
|
+
if (!spawnOk(status)) return DEGRADED_GIT_STATE;
|
|
644
|
+
const agents = run(['ls-files', AGENTS_DIR]);
|
|
645
|
+
const state = {
|
|
646
|
+
...DEGRADED_GIT_STATE,
|
|
647
|
+
ok: true,
|
|
648
|
+
tracksAgents: spawnOk(agents) && String(agents.stdout).trim().length > 0,
|
|
649
|
+
};
|
|
650
|
+
for (const record of String(status.stdout).split('\n')) {
|
|
651
|
+
const path = stagedPathFromPorcelain(record);
|
|
652
|
+
const slot = path && stagedSlotFor(path, lockfile);
|
|
653
|
+
if (slot) state[slot] = true;
|
|
654
|
+
}
|
|
655
|
+
return state;
|
|
656
|
+
} catch {
|
|
657
|
+
return DEGRADED_GIT_STATE;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Render the one truthful staging line a successful run closes with
|
|
663
|
+
* (Story #5339, corrected by Story #5364).
|
|
664
|
+
*
|
|
665
|
+
* `scope: 'bump'` (the full upgrade) reports the manifest + lockfile pair:
|
|
666
|
+
*
|
|
667
|
+
* - **staged** — both are staged, so "staged for review" is a fact.
|
|
668
|
+
* - **not staged** — anything else: the line says so and prints the exact
|
|
669
|
+
* `git add` command, naming `.agents/` too when the consumer tracks that
|
|
670
|
+
* tree (the sync re-materialized it, and an operator who stages only the
|
|
671
|
+
* manifest pair silently drops that diff).
|
|
672
|
+
*
|
|
673
|
+
* `scope: 'payload'` (the drift heal, which bumps no dependency) reports the
|
|
674
|
+
* re-materialized `.agents/` tree instead, and returns `''` when the consumer
|
|
675
|
+
* does not track it — there is then nothing to stage and nothing to say.
|
|
676
|
+
*
|
|
677
|
+
* Either scope degrades to a line that claims nothing about the index when the
|
|
678
|
+
* probe could not report.
|
|
679
|
+
*
|
|
680
|
+
* Pure: takes the probe result and the resolved lockfile name, touches no fs.
|
|
681
|
+
*
|
|
682
|
+
* @param {{
|
|
683
|
+
* ok?: boolean,
|
|
684
|
+
* stagedManifest?: boolean,
|
|
685
|
+
* stagedLockfile?: boolean,
|
|
686
|
+
* stagedPayload?: boolean,
|
|
687
|
+
* tracksAgents?: boolean,
|
|
688
|
+
* }} gitState - The `gitStatus` seam's return value.
|
|
689
|
+
* @param {string} lockfile - The detected lockfile name.
|
|
690
|
+
* @param {{ scope?: 'bump' | 'payload' }} [opts]
|
|
691
|
+
* @returns {string} A single sentence-run with no trailing newline, or `''`
|
|
692
|
+
* when there is nothing to report.
|
|
693
|
+
*/
|
|
694
|
+
export function formatStagingReport(
|
|
695
|
+
gitState,
|
|
696
|
+
lockfile,
|
|
697
|
+
{ scope = 'bump' } = {},
|
|
698
|
+
) {
|
|
699
|
+
const {
|
|
700
|
+
ok = false,
|
|
701
|
+
stagedManifest = false,
|
|
702
|
+
stagedLockfile = false,
|
|
703
|
+
stagedPayload = false,
|
|
704
|
+
tracksAgents = false,
|
|
705
|
+
} = gitState ?? {};
|
|
706
|
+
|
|
707
|
+
if (!ok) return NEUTRAL_STAGING_LINE;
|
|
708
|
+
|
|
709
|
+
if (scope === 'payload') {
|
|
710
|
+
if (!tracksAgents) return '';
|
|
711
|
+
return stagedPayload
|
|
712
|
+
? `The re-materialized ${AGENTS_DIR}/ payload is staged for review.`
|
|
713
|
+
: `The re-materialized ${AGENTS_DIR}/ payload is NOT staged. Review and stage it: git add ${AGENTS_DIR}/`;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (stagedManifest && stagedLockfile) {
|
|
717
|
+
return `The dependency bump is staged for review (${MANIFEST_FILENAME}, ${lockfile}).`;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const targets = [MANIFEST_FILENAME, lockfile];
|
|
721
|
+
let note = '';
|
|
722
|
+
if (tracksAgents) {
|
|
723
|
+
targets.push(`${AGENTS_DIR}/`);
|
|
724
|
+
note = ` ${AGENTS_DIR}/ is tracked here, so stage the re-materialized payload too.`;
|
|
725
|
+
}
|
|
726
|
+
return `The dependency bump is NOT staged.${note} Review and stage it: git add ${targets.join(' ')}`;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Resolve the staging line for a success surface, absorbing any failure in
|
|
731
|
+
* either seam. Reporting the index is a courtesy on top of completed work: a
|
|
732
|
+
* probe that throws (git missing, a seam raising) must degrade to the neutral
|
|
733
|
+
* line, never turn a successful run into a crash.
|
|
734
|
+
*
|
|
735
|
+
* The lockfile is resolved first because the probe's pathspec names it — that
|
|
736
|
+
* is what anchors the query at the probe root.
|
|
737
|
+
*
|
|
738
|
+
* @param {{
|
|
739
|
+
* gitStatus: (opts: { cwd: string, lockfile: string }) => object,
|
|
740
|
+
* detectLockfile: (cwd: string) => string,
|
|
741
|
+
* projectRoot: string,
|
|
742
|
+
* scope?: 'bump' | 'payload',
|
|
743
|
+
* }} deps
|
|
744
|
+
* @returns {string}
|
|
745
|
+
*/
|
|
746
|
+
function resolveStagingReport({
|
|
747
|
+
gitStatus,
|
|
748
|
+
detectLockfile,
|
|
749
|
+
projectRoot,
|
|
750
|
+
scope = 'bump',
|
|
751
|
+
}) {
|
|
752
|
+
try {
|
|
753
|
+
const lockfile = detectLockfile(projectRoot);
|
|
754
|
+
return formatStagingReport(
|
|
755
|
+
gitStatus({ cwd: projectRoot, lockfile }),
|
|
756
|
+
lockfile,
|
|
757
|
+
{ scope },
|
|
758
|
+
);
|
|
759
|
+
} catch {
|
|
760
|
+
return NEUTRAL_STAGING_LINE;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
437
764
|
/**
|
|
438
765
|
* Default `npmUpdate` seam: install the resolved target version. The install
|
|
439
|
-
* rewrites `package.json` / the lockfile on disk
|
|
440
|
-
*
|
|
766
|
+
* rewrites `package.json` / the lockfile on disk for the operator to review;
|
|
767
|
+
* this performs no git mutation. Whether the package manager also staged them
|
|
768
|
+
* is reported by the staging probe, never assumed (Story #5339).
|
|
441
769
|
*
|
|
442
770
|
* The package manager is auto-detected from `cwd`'s lockfile (Story #3575) so
|
|
443
771
|
* the bump lands in the operator's real lockfile rather than running
|
|
@@ -1119,6 +1447,8 @@ async function executePlan({
|
|
|
1119
1447
|
* checkDrift?: () => (boolean | Promise<boolean>),
|
|
1120
1448
|
* spawnPhase?: (phase: string, args: string[], opts: { binPath: string, cwd: string, write: (s: string) => void, writeErr: (s: string) => void }) => Promise<{ ok: boolean, stdout: string, stderr: string }> | { ok: boolean, stdout: string, stderr: string },
|
|
1121
1449
|
* surfaceChangelog?: (version: string) => unknown | Promise<unknown>,
|
|
1450
|
+
* gitStatus?: (opts: { cwd: string }) => { ok: boolean, staged?: string[], unstaged?: string[], tracksAgents?: boolean },
|
|
1451
|
+
* detectLockfile?: (cwd: string) => string,
|
|
1122
1452
|
* write?: (s: string) => void,
|
|
1123
1453
|
* writeErr?: (s: string) => void,
|
|
1124
1454
|
* exit?: (code: number) => void,
|
|
@@ -1142,6 +1472,8 @@ export async function runUpdate({
|
|
|
1142
1472
|
checkDrift,
|
|
1143
1473
|
spawnPhase,
|
|
1144
1474
|
surfaceChangelog,
|
|
1475
|
+
gitStatus = defaultGitStatus,
|
|
1476
|
+
detectLockfile = defaultDetectLockfile,
|
|
1145
1477
|
write = (s) => process.stdout.write(s),
|
|
1146
1478
|
writeErr = (s) => process.stderr.write(s),
|
|
1147
1479
|
exit = (code) => process.exit(code),
|
|
@@ -1245,8 +1577,19 @@ export async function runUpdate({
|
|
|
1245
1577
|
});
|
|
1246
1578
|
|
|
1247
1579
|
if (plan.action === 'resynced') {
|
|
1580
|
+
// The heal re-materializes `.agents/` and stops — no dependency is bumped,
|
|
1581
|
+
// so the payload IS the diff. A consumer that tracks that tree used to be
|
|
1582
|
+
// told nothing at all about it (Story #5364), the same silent-diff hazard
|
|
1583
|
+
// the staging report exists to close, one branch earlier.
|
|
1584
|
+
const payloadLine = resolveStagingReport({
|
|
1585
|
+
gitStatus,
|
|
1586
|
+
detectLockfile,
|
|
1587
|
+
projectRoot,
|
|
1588
|
+
scope: 'payload',
|
|
1589
|
+
});
|
|
1248
1590
|
write(
|
|
1249
|
-
`✅ Healed .agents/ drift (v${current}). The materialized payload is now current
|
|
1591
|
+
`✅ Healed .agents/ drift (v${current}). The materialized payload is now current.` +
|
|
1592
|
+
`${payloadLine ? ` ${payloadLine}` : ''}\n`,
|
|
1250
1593
|
);
|
|
1251
1594
|
return {
|
|
1252
1595
|
ok: true,
|
|
@@ -1270,7 +1613,12 @@ export async function runUpdate({
|
|
|
1270
1613
|
};
|
|
1271
1614
|
}
|
|
1272
1615
|
|
|
1273
|
-
|
|
1616
|
+
// Report the real index state rather than asserting one (Story #5339). Both
|
|
1617
|
+
// seams are read-only and failure-tolerant: a probe that cannot answer yields
|
|
1618
|
+
// the neutral line, and the update still reports success with a zero exit.
|
|
1619
|
+
write(
|
|
1620
|
+
`✅ Updated to v${target}. ${resolveStagingReport({ gitStatus, detectLockfile, projectRoot })}\n`,
|
|
1621
|
+
);
|
|
1274
1622
|
return {
|
|
1275
1623
|
ok: true,
|
|
1276
1624
|
action: 'updated',
|
|
@@ -1292,8 +1640,8 @@ export async function runUpdate({
|
|
|
1292
1640
|
* a current baseline after the upgrade.
|
|
1293
1641
|
* - `npmUpdate` runs the install command — auto-detected from the project
|
|
1294
1642
|
* lockfile (`pnpm`/`yarn`/`npm`), or the `--install-cmd` override —
|
|
1295
|
-
* through the shared `runInstallCommand` helper — no git mutation;
|
|
1296
|
-
*
|
|
1643
|
+
* through the shared `runInstallCommand` helper — no git mutation; the
|
|
1644
|
+
* resulting index state is reported by the staging probe, not asserted.
|
|
1297
1645
|
* - `spawnPhase` is wired to `defaultSpawnPhase`, which spawns each
|
|
1298
1646
|
* post-install phase (sync, sync-commands, migrate, doctor) as
|
|
1299
1647
|
* `node <packageRoot>/bin/mandrel.js …` (Story #4613 — the resolved bin
|
|
@@ -1336,6 +1684,8 @@ export async function runUpdate({
|
|
|
1336
1684
|
* cwd?: () => string,
|
|
1337
1685
|
* resolveBinScript?: (projectRoot: string) => string,
|
|
1338
1686
|
* checkDrift?: () => (boolean | Promise<boolean>),
|
|
1687
|
+
* gitStatus?: (opts: { cwd: string }) => object,
|
|
1688
|
+
* detectLockfile?: (cwd: string) => string,
|
|
1339
1689
|
* write?: (s: string) => void,
|
|
1340
1690
|
* writeErr?: (s: string) => void,
|
|
1341
1691
|
* exit?: (code: number) => void,
|
|
@@ -1361,6 +1711,8 @@ export default async function run(argv = [], deps = {}) {
|
|
|
1361
1711
|
cwd,
|
|
1362
1712
|
resolveBinScript,
|
|
1363
1713
|
checkDrift,
|
|
1714
|
+
gitStatus,
|
|
1715
|
+
detectLockfile,
|
|
1364
1716
|
} = deps;
|
|
1365
1717
|
|
|
1366
1718
|
const cwdFn = typeof cwd === 'function' ? cwd : () => process.cwd();
|
|
@@ -1405,6 +1757,13 @@ export default async function run(argv = [], deps = {}) {
|
|
|
1405
1757
|
fs,
|
|
1406
1758
|
}),
|
|
1407
1759
|
...(checkDrift ? { checkDrift } : {}),
|
|
1760
|
+
// Story #5339 — the staging probe. Production leaves both undefined so
|
|
1761
|
+
// runUpdate applies the real read-only git seam and the lockfile detection
|
|
1762
|
+
// that mirrors the install seam's; tests stub them for a fake tree.
|
|
1763
|
+
...(gitStatus ? { gitStatus } : {}),
|
|
1764
|
+
...(detectLockfile
|
|
1765
|
+
? { detectLockfile }
|
|
1766
|
+
: { detectLockfile: (dir) => defaultDetectLockfile(dir, fs) }),
|
|
1408
1767
|
spawnPhase: productionSpawnPhase,
|
|
1409
1768
|
surfaceChangelog: (target) =>
|
|
1410
1769
|
defaultSurfaceChangelog(target, {
|
package/lib/migrations/index.js
CHANGED
|
@@ -61,6 +61,7 @@ import { retireCodebaseSnapshot } from './steps/2.20.0-retire-codebase-snapshot.
|
|
|
61
61
|
import { retireLintBaselineCommand } from './steps/2.32.0-retire-lint-baseline-command.js';
|
|
62
62
|
import { retireDeliveryLimitKnobs } from './steps/2.57.0-retire-delivery-limit-knobs.js';
|
|
63
63
|
import { retirePlanningLimitKnobs } from './steps/2.57.0-retire-planning-limit-knobs.js';
|
|
64
|
+
import { retireAuditResultsAutoFile } from './steps/2.60.0-retire-audit-results-autofile.js';
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
67
|
* Ordered registry of migration steps. MUST stay sorted ascending by
|
|
@@ -82,6 +83,7 @@ export const migrations = [
|
|
|
82
83
|
retireLintBaselineCommand,
|
|
83
84
|
retirePlanningLimitKnobs,
|
|
84
85
|
retireDeliveryLimitKnobs,
|
|
86
|
+
retireAuditResultsAutoFile,
|
|
85
87
|
];
|
|
86
88
|
|
|
87
89
|
/**
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// lib/migrations/steps/2.60.0-retire-audit-results-autofile.js
|
|
2
|
+
/**
|
|
3
|
+
* Story #5366 — strip the retired `delivery.feedbackLoop.auditResultsAutoFile`
|
|
4
|
+
* key from a consumer's config.
|
|
5
|
+
*
|
|
6
|
+
* The audit-results graduator this toggle switched was deleted two releases
|
|
7
|
+
* ago, so the key had no runtime reader left: Story #5341's flip of its
|
|
8
|
+
* default from `true` to `false` changed nothing at all, because nothing read
|
|
9
|
+
* either value. A toggle with no reader is worse than no toggle — it reads as
|
|
10
|
+
* a live control, and a consumer that set it was configuring nothing.
|
|
11
|
+
*
|
|
12
|
+
* `delivery.feedbackLoop` carries `additionalProperties: false`, so a config
|
|
13
|
+
* that still sets the key fails AJV validation outright on upgrade rather than
|
|
14
|
+
* warning. That is what makes this a migration rather than a docs change.
|
|
15
|
+
*
|
|
16
|
+
* Both config surfaces are swept: `config-resolver.js` deep-merges
|
|
17
|
+
* `.agentrc.local.json` over `.agentrc.json` **before** the AJV gate runs, so
|
|
18
|
+
* a key surviving in the gitignored overlay fails exactly as a base one would.
|
|
19
|
+
*
|
|
20
|
+
* Pruning: `pruneDepth: 2` prunes an emptied `feedbackLoop` and then an
|
|
21
|
+
* emptied `delivery`, both of which are optional. The sibling `retroProposals`
|
|
22
|
+
* toggle is deliberately untouched — it has a live reader — so a consumer who
|
|
23
|
+
* set both keeps its block.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { createRetireAgentrcKeyStep } from '../helpers/retire-agentrc-key.js';
|
|
27
|
+
|
|
28
|
+
export const retireAuditResultsAutoFile = createRetireAgentrcKeyStep({
|
|
29
|
+
version: '2.60.0',
|
|
30
|
+
description:
|
|
31
|
+
'strip the retired delivery.feedbackLoop.auditResultsAutoFile key from ' +
|
|
32
|
+
'.agentrc.json — its graduator was deleted, so the toggle had no ' +
|
|
33
|
+
'runtime reader (Story #5366)',
|
|
34
|
+
keys: [
|
|
35
|
+
{
|
|
36
|
+
path: ['delivery', 'feedbackLoop', 'auditResultsAutoFile'],
|
|
37
|
+
pruneDepth: 2,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mandrel",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.60.0",
|
|
4
4
|
"description": "Claude Code-first opinionated workflow framework: instructions, skills, rules, and SDLC workflows that govern AI coding assistants.",
|
|
5
5
|
"files": [
|
|
6
6
|
".agents/",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"maintainability:update": "node .agents/scripts/update-maintainability-baseline.js",
|
|
46
46
|
"maintainability:reanchor": "node .agents/scripts/update-maintainability-baseline.js --full-scope",
|
|
47
47
|
"check:arch": "node .agents/scripts/check-arch-cycles.js",
|
|
48
|
+
"//resident-context-reports": "check:context-budget and check:workflow-citations are REPORTS, not ratchets (Story #5340, ADR 20260917-5340). check:context-budget still fails when the alwaysLoaded tier exceeds its recorded total plus the committed tolerance; its workflow tier, and check:workflow-citations in full, print and always exit 0. A green run is not evidence the numbers held — read the output.",
|
|
48
49
|
"check:context-budget": "node .agents/scripts/check-context-budget.js",
|
|
49
50
|
"check:cyclomatic": "node .agents/scripts/check-cyclomatic.js",
|
|
50
51
|
"cyclomatic:update": "node .agents/scripts/check-cyclomatic.js --update",
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "https://github.com/dsj1984/mandrel/blob/main/.agents/schemas/model-attribution.schema.json",
|
|
4
|
-
"title": "ModelAttribution",
|
|
5
|
-
"description": "Payload of the <!-- structured:model-attribution --> comment upserted onto a Task ticket at the moment it transitions to agent::executing (Story #2813). One entry per Task. Story- and Epic-level breakdowns are derived at query time from the child Tasks' attribution comments — there is no Story/Epic-scope emission.",
|
|
6
|
-
"x-mandrel-uncompiled": {
|
|
7
|
-
"reason": "Deliberate: this document is the SSOT for the shape, but no AJV instance compiles it. The framework does not pull AJV into the structured-comment path — that path uses hand-rolled shape guards — so the runtime gate below mirrors this document by hand. Keep the two in step: an edit here is only real once the validator enforces it. Declared in-file per Story #4938 so a reader is never left inferring authority from the file's mere existence.",
|
|
8
|
-
"runtimeGate": ".agents/scripts/lib/orchestration/model-attribution.js#validateModelAttributionPayload"
|
|
9
|
-
},
|
|
10
|
-
"type": "object",
|
|
11
|
-
"additionalProperties": false,
|
|
12
|
-
"required": ["kind", "ticketId", "model", "source", "recordedAt"],
|
|
13
|
-
"properties": {
|
|
14
|
-
"kind": { "type": "string", "const": "model-attribution" },
|
|
15
|
-
"ticketId": {
|
|
16
|
-
"type": "integer",
|
|
17
|
-
"minimum": 1,
|
|
18
|
-
"description": "GitHub Issue number of the Task this attribution is recorded on."
|
|
19
|
-
},
|
|
20
|
-
"model": {
|
|
21
|
-
"type": "object",
|
|
22
|
-
"additionalProperties": false,
|
|
23
|
-
"required": ["id"],
|
|
24
|
-
"properties": {
|
|
25
|
-
"id": {
|
|
26
|
-
"type": "string",
|
|
27
|
-
"minLength": 1,
|
|
28
|
-
"description": "Canonical model identifier (e.g. 'claude-opus-4-7', 'claude-sonnet-4-6'), or the sentinel 'unknown' when neither SDK metadata nor a runtime env var supplied an identity."
|
|
29
|
-
},
|
|
30
|
-
"family": {
|
|
31
|
-
"type": "string",
|
|
32
|
-
"minLength": 1,
|
|
33
|
-
"description": "Coarse model family label for rollup grouping (e.g. 'Opus', 'Sonnet', 'Haiku'). Optional; derived from the id when present."
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
"source": {
|
|
38
|
-
"type": "string",
|
|
39
|
-
"enum": ["sdk-metadata", "env", "unknown"],
|
|
40
|
-
"description": "Provenance of the model identification, in fallback order: 'sdk-metadata' (resolved from the SDK response), 'env' (resolved from a runtime env var), 'unknown' (neither was available)."
|
|
41
|
-
},
|
|
42
|
-
"recordedAt": {
|
|
43
|
-
"type": "string",
|
|
44
|
-
"format": "date-time",
|
|
45
|
-
"description": "ISO-8601 timestamp at which the attribution was recorded."
|
|
46
|
-
},
|
|
47
|
-
"sdkMetadata": {
|
|
48
|
-
"type": "object",
|
|
49
|
-
"description": "Opaque pass-through fields lifted from the SDK response metadata when available (e.g. response id, usage hints). Never required; never relied upon by the rollup helper.",
|
|
50
|
-
"additionalProperties": true
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|