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
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Order: audit (SCA) → lint (includes docs:check + the arch-cycles ratchet) →
|
|
9
9
|
* full test suite → unified baselines → the standalone ratchets
|
|
10
|
-
* (dead-exports ×2, context-budget, cyclomatic,
|
|
10
|
+
* (dead-exports ×2, context-budget, workflow-citations, cyclomatic,
|
|
11
|
+
* schema-references).
|
|
11
12
|
*
|
|
12
13
|
* The `audit` step runs `npm audit --audit-level=high`, matching CI's
|
|
13
14
|
* "Dependency Vulnerability Audit (SCA)" gate so a local green no longer hides
|
|
@@ -33,9 +34,14 @@
|
|
|
33
34
|
* being added here, so a green `verify` still hid both. Like their neighbours
|
|
34
35
|
* they are pure-Node and cost milliseconds.
|
|
35
36
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
* Story #5340 closed the `check-workflow-citations.js` gap the same way, once
|
|
38
|
+
* demoting it to a report made it free: it had been exempted here because
|
|
39
|
+
* `tests/check-workflow-citations.test.js` re-ran the same ratchet, so a
|
|
40
|
+
* `verify` step would have double-paid it. That test now asserts the report,
|
|
41
|
+
* not a ceiling, so the step is the only local surface that prints the count.
|
|
42
|
+
*
|
|
43
|
+
* Still NOT mirrored: `check-baseline-scope.js` runs in CI's `baselines` job
|
|
44
|
+
* only — `.agents/rules/known-tooling-behavior.md` entry 2 carries the current
|
|
39
45
|
* coverage table. (`prune-baseline-orphans.js --check` used to sit in that
|
|
40
46
|
* list; it no longer runs in CI at all — the un-attributed duplicate of the
|
|
41
47
|
* scope gate's `extra` direction reds every open PR on inherited rows.) Nor are the CI gates this command structurally cannot
|
|
@@ -50,8 +56,8 @@ import { spawnSync } from 'node:child_process';
|
|
|
50
56
|
import { runAsCli } from './lib/cli-utils.js';
|
|
51
57
|
|
|
52
58
|
/**
|
|
53
|
-
* A gate step: `node .agents/scripts/<script>` plus any extra args.
|
|
54
|
-
* the
|
|
59
|
+
* A gate step: `node .agents/scripts/<script>` plus any extra args. Eight of
|
|
60
|
+
* the eleven steps share exactly that shape, so spelling it once leaves the list
|
|
55
61
|
* below readable as what it actually is — a gate *order* — instead of a wall
|
|
56
62
|
* of spawn tuples.
|
|
57
63
|
*
|
|
@@ -79,6 +85,7 @@ const STEPS = [
|
|
|
79
85
|
gate('dead-exports', 'check-dead-exports.js'),
|
|
80
86
|
gate('dead-exports-production', 'check-dead-exports.js', '--production'),
|
|
81
87
|
gate('context-budget', 'check-context-budget.js'),
|
|
88
|
+
gate('workflow-citations', 'check-workflow-citations.js'),
|
|
82
89
|
gate('cyclomatic', 'check-cyclomatic.js'),
|
|
83
90
|
gate('schema-references', 'check-schema-references.js'),
|
|
84
91
|
];
|
|
@@ -70,7 +70,6 @@ import { handleRemoteVerificationFailure } from './lib/orchestration/story-init-
|
|
|
70
70
|
import {
|
|
71
71
|
STATE_LABELS,
|
|
72
72
|
transitionTicketState,
|
|
73
|
-
upsertStructuredComment,
|
|
74
73
|
} from './lib/orchestration/ticketing.js';
|
|
75
74
|
import { createProvider } from './lib/provider-factory.js';
|
|
76
75
|
import { buildProtectionCtx } from './lib/single-story-sweep/protection-ctx.js';
|
|
@@ -770,28 +769,13 @@ export async function runSingleStoryInit({
|
|
|
770
769
|
remoteProbe: { remoteUrl: remote.remoteUrl, detail: remote.detail },
|
|
771
770
|
};
|
|
772
771
|
|
|
773
|
-
//
|
|
774
|
-
//
|
|
775
|
-
//
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
storyId,
|
|
781
|
-
'story-init',
|
|
782
|
-
renderSingleStoryInitComment(result),
|
|
783
|
-
);
|
|
784
|
-
progress(
|
|
785
|
-
'COMMENT',
|
|
786
|
-
`📝 Upserted story-init structured comment on #${storyId}.`,
|
|
787
|
-
);
|
|
788
|
-
} catch (err) {
|
|
789
|
-
Logger.error(
|
|
790
|
-
`[single-story-init] ⚠️ Failed to upsert story-init structured comment: ${err?.message ?? err}`,
|
|
791
|
-
);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
772
|
+
// Story #5343 — init posts no `story-init` comment. It was one GitHub write
|
|
773
|
+
// per Story restating what its own envelope already carries; the envelope is
|
|
774
|
+
// on stdout and on disk (below), `deliver-recover.js` classifies state from
|
|
775
|
+
// labels, the PR probe and disk artifacts, and `run-scoped-config.js` reads
|
|
776
|
+
// the base-branch pin off that same envelope. The `agent::executing` flip
|
|
777
|
+
// above is what makes the claim visible during the install window.
|
|
778
|
+
//
|
|
795
779
|
// Story #4685 — route the full result to a temp log and emit a single-line
|
|
796
780
|
// summary carrying the fields the orchestrating agent acts on (workCwd,
|
|
797
781
|
// remoteVerified). The `## Spec` names this the hot-path stdout to quiet.
|
|
@@ -820,40 +804,6 @@ export async function runSingleStoryInit({
|
|
|
820
804
|
return { success: true, result };
|
|
821
805
|
}
|
|
822
806
|
|
|
823
|
-
export function renderSingleStoryInitComment(result) {
|
|
824
|
-
const payload = {
|
|
825
|
-
storyId: result.storyId,
|
|
826
|
-
epicId: null,
|
|
827
|
-
standalone: true,
|
|
828
|
-
storyBranch: result.storyBranch,
|
|
829
|
-
baseBranch: result.baseBranch,
|
|
830
|
-
runScopedConfig: result.runScopedConfig,
|
|
831
|
-
worktreeEnabled: result.worktreeEnabled,
|
|
832
|
-
workCwd: result.workCwd,
|
|
833
|
-
worktreeCreated: result.worktreeCreated,
|
|
834
|
-
dependenciesInstalled: result.dependenciesInstalled,
|
|
835
|
-
installStatus: result.installStatus,
|
|
836
|
-
remoteVerified: result.remoteVerified,
|
|
837
|
-
remoteProbe: result.remoteProbe,
|
|
838
|
-
};
|
|
839
|
-
return [
|
|
840
|
-
'## Story init (standalone)',
|
|
841
|
-
'',
|
|
842
|
-
`- **standalone:** \`true\``,
|
|
843
|
-
`- **storyBranch:** \`${result.storyBranch}\``,
|
|
844
|
-
`- **baseBranch:** \`${result.baseBranch}\``,
|
|
845
|
-
`- **workCwd:** \`${result.workCwd}\``,
|
|
846
|
-
`- **worktreeEnabled:** \`${result.worktreeEnabled}\``,
|
|
847
|
-
`- **remoteVerified:** \`${result.remoteVerified}\``,
|
|
848
|
-
`- **dependenciesInstalled:** \`${result.dependenciesInstalled}\``,
|
|
849
|
-
'',
|
|
850
|
-
'```json',
|
|
851
|
-
JSON.stringify(payload, null, 2),
|
|
852
|
-
'```',
|
|
853
|
-
'',
|
|
854
|
-
].join('\n');
|
|
855
|
-
}
|
|
856
|
-
|
|
857
807
|
runAsCli(import.meta.url, runSingleStoryInit, {
|
|
858
808
|
source: 'single-story-init',
|
|
859
809
|
usage: {
|
|
@@ -107,6 +107,18 @@
|
|
|
107
107
|
* deterministic config source (`delivery.deliverRunner.concurrencyCap`) and
|
|
108
108
|
* one scheduling kernel with every `/mandrel-deliver` multi-Story invocation.
|
|
109
109
|
*
|
|
110
|
+
* **The cap is clamped to 1 when worktree isolation resolves off** (Story
|
|
111
|
+
* #5357). Concurrent dispatch is safe only because each Story gets its own
|
|
112
|
+
* checkout; with isolation off every Story resolves to the same `workCwd`, so
|
|
113
|
+
* two workers contend for `HEAD` and one takes the branch from under the
|
|
114
|
+
* other — the #5339/#5340 web run, where both wave-1 workers were initialised
|
|
115
|
+
* into the same tree. The clamp reads `resolveWorktreeEnabled`'s result, so all
|
|
116
|
+
* three routes to off (`CLAUDE_CODE_REMOTE=true`, `AP_WORKTREE_ENABLED=false`,
|
|
117
|
+
* `delivery.worktreeIsolation.enabled: false`) are covered by construction. It
|
|
118
|
+
* is a safety floor rather than a preference, so it is the one case where an
|
|
119
|
+
* explicit `--concurrency` does NOT win — and `capPrecedence` names
|
|
120
|
+
* `worktree-clamp` as the winning source and carries the requested value.
|
|
121
|
+
*
|
|
110
122
|
* Exit codes: 0 ok · 1 input error · 2 dependency cycle (`cycleError`) ·
|
|
111
123
|
* 3 wedged (`wedged`) — ready is empty, nothing is in flight, and undone
|
|
112
124
|
* Stories are waiting on blockers that are not done · 4 blocked (`blocked`) —
|
|
@@ -124,7 +136,13 @@ import { readFileSync } from 'node:fs';
|
|
|
124
136
|
import { parseArgs } from 'node:util';
|
|
125
137
|
|
|
126
138
|
import { runAsCli } from './lib/cli-utils.js';
|
|
127
|
-
import {
|
|
139
|
+
import {
|
|
140
|
+
getPaths,
|
|
141
|
+
getRunners,
|
|
142
|
+
getWorktreeIsolation,
|
|
143
|
+
resolveConfig,
|
|
144
|
+
resolveWorktreeEnabled,
|
|
145
|
+
} from './lib/config-resolver.js';
|
|
128
146
|
import { detectCycle } from './lib/Graph.js';
|
|
129
147
|
import { Logger } from './lib/Logger.js';
|
|
130
148
|
import { AGENT_LABELS } from './lib/label-constants.js';
|
|
@@ -206,6 +224,13 @@ Options:
|
|
|
206
224
|
WINS over the configured value, and the envelope's
|
|
207
225
|
capPrecedence records that it did — including when the
|
|
208
226
|
request exceeds the configured cap.
|
|
227
|
+
ONE EXCEPTION: when worktree isolation resolves off
|
|
228
|
+
(CLAUDE_CODE_REMOTE=true, AP_WORKTREE_ENABLED=false, or
|
|
229
|
+
delivery.worktreeIsolation.enabled: false), the cap is
|
|
230
|
+
clamped to 1 and the clamp outranks this flag — every
|
|
231
|
+
Story would otherwise run in the same checkout and two
|
|
232
|
+
workers would contend for HEAD. capPrecedence reports
|
|
233
|
+
source "worktree-clamp" with the requested value.
|
|
209
234
|
--done <csv> Comma-separated Story IDs already completed this run.
|
|
210
235
|
Their dependents become eligible; they are never
|
|
211
236
|
re-dispatched. Defaults to empty.
|
|
@@ -638,6 +663,15 @@ export function parseInFlight(raw) {
|
|
|
638
663
|
return { value: num, error: null };
|
|
639
664
|
}
|
|
640
665
|
|
|
666
|
+
/**
|
|
667
|
+
* The per-beat cap a run is clamped to when worktree isolation resolves off
|
|
668
|
+
* (Story #5357). `delivery.deliverRunner.concurrencyCap` is a throughput
|
|
669
|
+
* preference; this is a safety floor, so it outranks even an explicit
|
|
670
|
+
* `--concurrency`. Why the two are coupled, and why the clamp is never silent:
|
|
671
|
+
* ADR `20260917-5357` in `docs/decisions.md`.
|
|
672
|
+
*/
|
|
673
|
+
export const WORKTREE_DISABLED_CONCURRENCY_CAP = 1;
|
|
674
|
+
|
|
641
675
|
/**
|
|
642
676
|
* Validate a raw `--concurrency` value into a positive integer.
|
|
643
677
|
*
|
|
@@ -675,7 +709,9 @@ export function parseConcurrencyOverride(raw) {
|
|
|
675
709
|
* @param {object} [opts.config] Pre-resolved config (injected by tests so
|
|
676
710
|
* they never depend on a real `.agentrc`).
|
|
677
711
|
* @param {number} [opts.override] Validated positive integer from
|
|
678
|
-
* `--concurrency`; wins over config
|
|
712
|
+
* `--concurrency`; wins over config, unless
|
|
713
|
+
* the worktree-isolation floor clamps it.
|
|
714
|
+
* @param {NodeJS.ProcessEnv} [opts.env] Environment (test injection).
|
|
679
715
|
* @returns {number} The resolved positive-integer concurrency cap.
|
|
680
716
|
*/
|
|
681
717
|
export function resolveConcurrencyCap(opts = {}) {
|
|
@@ -702,44 +738,115 @@ export function resolveConcurrencyCap(opts = {}) {
|
|
|
702
738
|
* safety limit, and refusing a deliberate operator escalation would trade a
|
|
703
739
|
* silent override for a silent stall.
|
|
704
740
|
*
|
|
741
|
+
* **The one exception (Story #5357).** When worktree isolation resolves off,
|
|
742
|
+
* the cap is clamped to {@link WORKTREE_DISABLED_CONCURRENCY_CAP} and the
|
|
743
|
+
* clamp outranks the flag. That is not a reversal of the paragraph above: the
|
|
744
|
+
* configured cap is a preference and a flag may outrank a preference, but a
|
|
745
|
+
* shared checkout is not a preference at all. Dispatching two workers into one
|
|
746
|
+
* working tree corrupts the run whatever number either source asked for, so
|
|
747
|
+
* the floor wins — loudly, per {@link resolveWorktreeClamp}.
|
|
748
|
+
*
|
|
705
749
|
* @param {object} [opts]
|
|
706
750
|
* @param {string} [opts.cwd] Repo root for config resolution.
|
|
707
751
|
* @param {object} [opts.config] Pre-resolved config (test injection).
|
|
708
752
|
* @param {number} [opts.override] Validated positive integer from
|
|
709
753
|
* `--concurrency`.
|
|
754
|
+
* @param {NodeJS.ProcessEnv} [opts.env] Environment the worktree-isolation
|
|
755
|
+
* rules read (test injection).
|
|
710
756
|
* @returns {{
|
|
711
757
|
* cap: number,
|
|
712
|
-
* source: 'flag'|'config',
|
|
758
|
+
* source: 'flag'|'config'|'worktree-clamp',
|
|
713
759
|
* configuredCap: number,
|
|
714
760
|
* requestedCap: number|null,
|
|
715
761
|
* exceedsConfigured: boolean,
|
|
716
762
|
* note: string,
|
|
717
763
|
* }}
|
|
718
764
|
*/
|
|
719
|
-
export function resolveCapPrecedence({ cwd, config, override } = {}) {
|
|
765
|
+
export function resolveCapPrecedence({ cwd, config, override, env } = {}) {
|
|
720
766
|
const resolved = config ?? resolveConfig({ cwd });
|
|
721
767
|
const { deliverRunner } = getRunners(resolved);
|
|
722
768
|
const configuredCap = deliverRunner.concurrencyCap;
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
769
|
+
const requested =
|
|
770
|
+
override == null
|
|
771
|
+
? {
|
|
772
|
+
cap: configuredCap,
|
|
773
|
+
source: 'config',
|
|
774
|
+
configuredCap,
|
|
775
|
+
requestedCap: null,
|
|
776
|
+
exceedsConfigured: false,
|
|
777
|
+
note: `cap ${configuredCap} from delivery.deliverRunner.concurrencyCap (no --concurrency given)`,
|
|
778
|
+
}
|
|
779
|
+
: {
|
|
780
|
+
cap: override,
|
|
781
|
+
source: 'flag',
|
|
782
|
+
configuredCap,
|
|
783
|
+
requestedCap: override,
|
|
784
|
+
exceedsConfigured: override > configuredCap,
|
|
785
|
+
note:
|
|
786
|
+
override > configuredCap
|
|
787
|
+
? `cap ${override} from --concurrency, which OVERRIDES and EXCEEDS the configured delivery.deliverRunner.concurrencyCap ${configuredCap} — this run is deliberately above the project default`
|
|
788
|
+
: `cap ${override} from --concurrency, which overrides the configured delivery.deliverRunner.concurrencyCap ${configuredCap}`,
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
return resolveWorktreeClamp({ requested, config: resolved, env });
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Apply the worktree-isolation safety floor to an already-resolved precedence
|
|
796
|
+
* record (Story #5357).
|
|
797
|
+
*
|
|
798
|
+
* **Keyed off the resolved boolean, never one env var.** `CLAUDE_CODE_REMOTE`
|
|
799
|
+
* is only one of three routes to isolation-off — `AP_WORKTREE_ENABLED=false`
|
|
800
|
+
* and `delivery.worktreeIsolation.enabled: false` are the others and are
|
|
801
|
+
* exactly as unsafe — so this reads {@link resolveWorktreeEnabled}'s result and
|
|
802
|
+
* all three are covered by construction rather than by enumeration.
|
|
803
|
+
*
|
|
804
|
+
* **The config is normalised through `getWorktreeIsolation` first**, and that
|
|
805
|
+
* is load-bearing. `resolveWorktreeEnabled` reads
|
|
806
|
+
* `Boolean(config.delivery.worktreeIsolation.enabled)` raw, so a config that
|
|
807
|
+
* simply omits the block — every test-injected partial, and any consumer who
|
|
808
|
+
* never wrote one — reads `Boolean(undefined) === false` and would be clamped
|
|
809
|
+
* as though an operator had disabled isolation. `getWorktreeIsolation` applies
|
|
810
|
+
* the framework default (`enabled: true`) the same way `resolveConfig` does for
|
|
811
|
+
* a real `.agentrc`, so the clamp fires on a deliberate off, never on an
|
|
812
|
+
* absent key.
|
|
813
|
+
*
|
|
814
|
+
* The record is emitted whenever isolation is off, **including when the
|
|
815
|
+
* requested cap was already 1**: the point is that the run's sequentiality is
|
|
816
|
+
* always attributable to the reason that forced it, not left to coincide with
|
|
817
|
+
* a preference that happened to agree.
|
|
818
|
+
*
|
|
819
|
+
* @param {object} args
|
|
820
|
+
* @param {{cap: number, source: string, configuredCap: number, requestedCap: number|null, exceedsConfigured: boolean, note: string}} args.requested
|
|
821
|
+
* The precedence record the flag-vs-config rules produced.
|
|
822
|
+
* @param {object|null} [args.config] Resolved config.
|
|
823
|
+
* @param {NodeJS.ProcessEnv} [args.env] Environment (test injection).
|
|
824
|
+
* @returns {typeof args.requested} The record unchanged when isolation is on,
|
|
825
|
+
* or the clamped record naming `worktree-clamp` as the winning source.
|
|
826
|
+
*/
|
|
827
|
+
export function resolveWorktreeClamp({ requested, config, env } = {}) {
|
|
828
|
+
const worktreeEnabled = resolveWorktreeEnabled(
|
|
829
|
+
{
|
|
830
|
+
config: { delivery: { worktreeIsolation: getWorktreeIsolation(config) } },
|
|
831
|
+
},
|
|
832
|
+
env ?? process.env,
|
|
833
|
+
);
|
|
834
|
+
if (worktreeEnabled) return requested;
|
|
835
|
+
|
|
836
|
+
const from =
|
|
837
|
+
requested.source === 'flag'
|
|
838
|
+
? '--concurrency'
|
|
839
|
+
: 'delivery.deliverRunner.concurrencyCap';
|
|
734
840
|
return {
|
|
735
|
-
cap:
|
|
736
|
-
source: '
|
|
737
|
-
configuredCap,
|
|
738
|
-
requestedCap:
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
841
|
+
cap: WORKTREE_DISABLED_CONCURRENCY_CAP,
|
|
842
|
+
source: 'worktree-clamp',
|
|
843
|
+
configuredCap: requested.configuredCap,
|
|
844
|
+
requestedCap: requested.cap,
|
|
845
|
+
// The run is at 1, so it is not above the project default whatever was
|
|
846
|
+
// asked for — the escalation never took effect and must not be reported
|
|
847
|
+
// as though it had.
|
|
848
|
+
exceedsConfigured: false,
|
|
849
|
+
note: `cap clamped to ${WORKTREE_DISABLED_CONCURRENCY_CAP} because worktree isolation resolved OFF — requested ${requested.cap} from ${from}. Concurrent dispatch shares one checkout without per-Story worktrees, so two workers would contend for HEAD; this floor outranks an explicit --concurrency.`,
|
|
743
850
|
};
|
|
744
851
|
}
|
|
745
852
|
|
|
@@ -1000,6 +1107,8 @@ export function detectWedge({ nodes, doneIds, ready, inFlight }) {
|
|
|
1000
1107
|
* @param {string|number} [args.inFlight] Raw --in-flight count.
|
|
1001
1108
|
* @param {string} [args.cwd] Repo root for config resolution.
|
|
1002
1109
|
* @param {object} [args.config] Pre-resolved config (test injection).
|
|
1110
|
+
* @param {NodeJS.ProcessEnv} [args.env] Environment the worktree-isolation
|
|
1111
|
+
* clamp reads (test injection).
|
|
1003
1112
|
* @returns {{
|
|
1004
1113
|
* envelope: {kind: string, ready: number[], totalStories: number, concurrencyCap: number, inFlight: number, cycleError: string|null},
|
|
1005
1114
|
* exitCode: number
|
|
@@ -1013,6 +1122,7 @@ export function runStoriesWaveTick({
|
|
|
1013
1122
|
inFlight,
|
|
1014
1123
|
cwd,
|
|
1015
1124
|
config,
|
|
1125
|
+
env,
|
|
1016
1126
|
} = {}) {
|
|
1017
1127
|
// Validate the --concurrency override before resolving config so an invalid
|
|
1018
1128
|
// value fails fast with exit code 1 regardless of DAG validity.
|
|
@@ -1033,7 +1143,7 @@ export function runStoriesWaveTick({
|
|
|
1033
1143
|
return inputErrorResult(doneError, null, inFlightValue);
|
|
1034
1144
|
}
|
|
1035
1145
|
|
|
1036
|
-
const capPrecedence = resolveCapPrecedence({ cwd, config, override });
|
|
1146
|
+
const capPrecedence = resolveCapPrecedence({ cwd, config, override, env });
|
|
1037
1147
|
const concurrencyCap = capPrecedence.cap;
|
|
1038
1148
|
|
|
1039
1149
|
let rawJson;
|
|
@@ -1099,6 +1209,10 @@ export function runStoriesWaveTick({
|
|
|
1099
1209
|
* the run-end signal for `plan-run-epilogue.js`.
|
|
1100
1210
|
* - `blocked` — ids carrying `agent::blocked` (Story #4601). Non-empty means
|
|
1101
1211
|
* the loop must END, not poll: see `BLOCKED_EXIT_CODE`.
|
|
1212
|
+
* - `stalledDispatch` — `--dispatched` ids live state still reports as
|
|
1213
|
+
* `agent::ready`. They stay withheld; the field exists so a caller keeping
|
|
1214
|
+
* an append-only dispatched list can see an id that is pinned rather than
|
|
1215
|
+
* merely initializing (Story #5363).
|
|
1102
1216
|
*
|
|
1103
1217
|
* @param {object} args
|
|
1104
1218
|
* @param {string} args.stories Raw `--stories` CSV of Story ids.
|
|
@@ -1107,9 +1221,14 @@ export function runStoriesWaveTick({
|
|
|
1107
1221
|
* has spawned but may not yet have observed labelled.
|
|
1108
1222
|
* @param {string} [args.cwd] Repo root for config resolution.
|
|
1109
1223
|
* @param {object} [args.config] Pre-resolved config (test injection).
|
|
1224
|
+
* @param {NodeJS.ProcessEnv} [args.env] Environment the worktree-isolation
|
|
1225
|
+
* clamp reads (test injection).
|
|
1110
1226
|
* @param {Function} [args.probe] Probe seam (test injection).
|
|
1111
1227
|
* @param {Function} [args.context] Provider-context seam (test injection).
|
|
1112
|
-
* @returns {Promise<{ envelope: object, exitCode: number }>}
|
|
1228
|
+
* @returns {Promise<{ envelope: object, exitCode: number, records: object[] }>}
|
|
1229
|
+
* `records` are the probed nodes (id, dependsOn, files, body, labels) — the
|
|
1230
|
+
* bodies this beat already fetched, handed to the caller beside the envelope
|
|
1231
|
+
* rather than serialized into it.
|
|
1113
1232
|
*/
|
|
1114
1233
|
export async function runProbedStoriesWaveTick({
|
|
1115
1234
|
stories,
|
|
@@ -1117,6 +1236,7 @@ export async function runProbedStoriesWaveTick({
|
|
|
1117
1236
|
dispatched,
|
|
1118
1237
|
cwd,
|
|
1119
1238
|
config,
|
|
1239
|
+
env,
|
|
1120
1240
|
probe = probeLiveState,
|
|
1121
1241
|
context = createProbeContext,
|
|
1122
1242
|
} = {}) {
|
|
@@ -1141,7 +1261,7 @@ export async function runProbedStoriesWaveTick({
|
|
|
1141
1261
|
return inputErrorResult(dispatchedError);
|
|
1142
1262
|
}
|
|
1143
1263
|
|
|
1144
|
-
const capPrecedence = resolveCapPrecedence({ cwd, config, override });
|
|
1264
|
+
const capPrecedence = resolveCapPrecedence({ cwd, config, override, env });
|
|
1145
1265
|
const concurrencyCap = capPrecedence.cap;
|
|
1146
1266
|
|
|
1147
1267
|
let probed;
|
|
@@ -1171,6 +1291,7 @@ export async function runProbedStoriesWaveTick({
|
|
|
1171
1291
|
doneIds,
|
|
1172
1292
|
inFlight,
|
|
1173
1293
|
blockedIds = [],
|
|
1294
|
+
stalledDispatch = [],
|
|
1174
1295
|
foreignHeld = [],
|
|
1175
1296
|
inFlightRecords = [],
|
|
1176
1297
|
} = probed;
|
|
@@ -1198,12 +1319,25 @@ export async function runProbedStoriesWaveTick({
|
|
|
1198
1319
|
epilogueDue,
|
|
1199
1320
|
blocked: blockedIds,
|
|
1200
1321
|
blockedReason: blockedReasonFor(blockedIds),
|
|
1322
|
+
// Ids the caller listed in `--dispatched` that live state still reports
|
|
1323
|
+
// as `agent::ready`. Withheld as in flight (that is the flag's whole
|
|
1324
|
+
// job) and named here, because the same reading covers a healthy init
|
|
1325
|
+
// window and a spawn that died before init — and only the caller, who
|
|
1326
|
+
// owns the dispatched list, can tell those apart (Story #5363).
|
|
1327
|
+
stalledDispatch,
|
|
1201
1328
|
// Stories another operator's lease holds — withheld from dispatch this
|
|
1202
1329
|
// beat (folded into in-flight) and surfaced so the run can report
|
|
1203
1330
|
// "#<id> held by @<holder>" instead of dispatching into an init refusal.
|
|
1204
1331
|
foreignHeld,
|
|
1205
1332
|
foreignHeldReason: foreignHeldReasonFor(foreignHeld),
|
|
1206
1333
|
},
|
|
1334
|
+
// The probed nodes, beside the envelope rather than inside it. The
|
|
1335
|
+
// envelope is a list of ids by design — a Story body is several KB and has
|
|
1336
|
+
// no business on stdout — but the probe has already fetched every body,
|
|
1337
|
+
// and the one caller that needs them (`deliver-run.js`, building each
|
|
1338
|
+
// ready Story's dispatch prompt from its declared `changes[]`) would
|
|
1339
|
+
// otherwise re-fetch the whole set to read what this beat already holds.
|
|
1340
|
+
records: nodes,
|
|
1207
1341
|
// A blocked Story outranks the scheduler's own verdict — including a
|
|
1208
1342
|
// wedge, whose named blockers are moot while a human owes a decision.
|
|
1209
1343
|
// A cycle (2) does not yield: a self-referential DAG is a planning error
|
|
@@ -19,7 +19,6 @@ file is the roster, the per-kind command table, and the procedure.
|
|
|
19
19
|
| `arch-cycles.json` | `npm run check:arch` |
|
|
20
20
|
| `cyclomatic.json` | `npm run check:cyclomatic` |
|
|
21
21
|
| `context-budget.json` | `npm run check:context-budget` |
|
|
22
|
-
| `workflow-citations.json` | `npm run check:workflow-citations` |
|
|
23
22
|
| `agents-loc.csv` | `npm run baseline:agents-loc` |
|
|
24
23
|
|
|
25
24
|
A refresh is **not** a regression entry — it tells the ratchet that the new
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-09-
|
|
2
|
+
"generatedAt": "2026-09-17T13:37:08.250Z",
|
|
3
3
|
"generator": "generate-skills-index.js@1",
|
|
4
4
|
"skills": [
|
|
5
5
|
{
|
|
@@ -52,16 +52,6 @@
|
|
|
52
52
|
"allowedTools": null,
|
|
53
53
|
"vendor": null
|
|
54
54
|
},
|
|
55
|
-
{
|
|
56
|
-
"name": "scope-triage",
|
|
57
|
-
"tier": "core",
|
|
58
|
-
"category": "core",
|
|
59
|
-
"path": ".agents/skills/core/scope-triage/SKILL.md",
|
|
60
|
-
"description": "Optional split-advisory for `/mandrel-plan`. Under v2 there is no epic|story routing verdict — `/mandrel-plan` always authors Stories. Use this skill only when judging whether a draft should stay one Story or legitimately split (near-zero overlap or an architectural seam).",
|
|
61
|
-
"policyCapsuleBullets": 5,
|
|
62
|
-
"allowedTools": null,
|
|
63
|
-
"vendor": null
|
|
64
|
-
},
|
|
65
55
|
{
|
|
66
56
|
"name": "security-and-hardening",
|
|
67
57
|
"tier": "core",
|
|
@@ -97,7 +87,7 @@
|
|
|
97
87
|
"tier": "stack",
|
|
98
88
|
"category": "qa",
|
|
99
89
|
"path": ".agents/skills/stack/qa/playwright/SKILL.md",
|
|
100
|
-
"description": "Robust E2E browser testing with Playwright. Use when writing browser-driven tests — leverage auto-waiting (no `waitForTimeout`), prefer user-visible locators (`getByRole`, `getByText`, `getByLabel`) over CSS/XPath, reuse `storageState` for auth, and enable trace-on-first-retry for CI debugging.",
|
|
90
|
+
"description": "Robust E2E browser testing with Playwright. Use when writing browser-driven tests — leverage auto-waiting (no `waitForTimeout`), prefer user-visible locators (`getByRole`, `getByText`, `getByLabel`) over CSS/XPath, reuse `storageState` for auth, and enable trace-on-first-retry for CI debugging. Also carries the `data-testid` contract a UI change states in its acceptance criteria.",
|
|
101
91
|
"policyCapsuleBullets": 8,
|
|
102
92
|
"allowedTools": null,
|
|
103
93
|
"vendor": "playwright"
|
|
@@ -5,6 +5,8 @@ description:
|
|
|
5
5
|
tests — leverage auto-waiting (no `waitForTimeout`), prefer user-visible
|
|
6
6
|
locators (`getByRole`, `getByText`, `getByLabel`) over CSS/XPath, reuse
|
|
7
7
|
`storageState` for auth, and enable trace-on-first-retry for CI debugging.
|
|
8
|
+
Also carries the `data-testid` contract a UI change states in its acceptance
|
|
9
|
+
criteria.
|
|
8
10
|
vendor: playwright
|
|
9
11
|
---
|
|
10
12
|
|
|
@@ -21,6 +23,30 @@ vendor: playwright
|
|
|
21
23
|
- Use a unique data set per test run, or tear down state explicitly, to prevent cross-test contamination.
|
|
22
24
|
- Never let Playwright own the lifetime of a dev server it did not start: boot the server out-of-band, point the suite at the running origin, and set `reuseExistingServer` so `webServer` only probes readiness.
|
|
23
25
|
|
|
26
|
+
## The testid contract
|
|
27
|
+
|
|
28
|
+
A `data-testid` is a selector two artifacts share: the component that renders
|
|
29
|
+
it and the spec that queries it. Renaming one without the other is a silent
|
|
30
|
+
break — the suite still compiles, and the failure arrives as a missing element
|
|
31
|
+
at run time, in CI, attributed to whichever change happened to land next.
|
|
32
|
+
|
|
33
|
+
So a change touching UI (`*.tsx`, `*.astro`, `*.svelte`, `*.vue`, a components
|
|
34
|
+
folder) states which side of that contract it is on, in its own acceptance
|
|
35
|
+
criteria, as one of:
|
|
36
|
+
|
|
37
|
+
- **`data-testid invariance: <the testids that MUST be preserved>`** — the
|
|
38
|
+
change reshapes markup and renames nothing, and the list says what the
|
|
39
|
+
reshape may not touch.
|
|
40
|
+
- **`data-testid changes: <old> -> <new>, with the matching
|
|
41
|
+
tests/e2e/*.spec.ts selector updated`** — the rename is deliberate, and the
|
|
42
|
+
spec file that queries it is edited in the same change (or in one ordered
|
|
43
|
+
ahead of it, when the work is split).
|
|
44
|
+
|
|
45
|
+
Renaming a testid without the matching spec edit is forbidden either way. When
|
|
46
|
+
a change deliberately renames nothing, say so in its negative-scope prose too:
|
|
47
|
+
a preserved set stated only once reads as an omission the next author is free
|
|
48
|
+
to revise.
|
|
49
|
+
|
|
24
50
|
## Running a `webServer`-backed suite outside CI
|
|
25
51
|
|
|
26
52
|
Playwright's `webServer` block **watches the process it spawned**. That
|
|
@@ -152,15 +152,17 @@ node .agents/scripts/audit-to-stories.js --emit-plan-seed \
|
|
|
152
152
|
|
|
153
153
|
The seed renders the canonical one-pager sections — Problem Statement,
|
|
154
154
|
Recommended Direction, Key Assumptions (with links to every source
|
|
155
|
-
report), MVP Scope (the
|
|
156
|
-
authoring step has concrete anchors),
|
|
157
|
-
|
|
158
|
-
**
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
155
|
+
report), MVP Scope (**the findings, flat**), Key Files (so `/mandrel-plan`'s
|
|
156
|
+
authoring step has concrete anchors), Not Doing.
|
|
157
|
+
|
|
158
|
+
**The seed states findings, not a partition.** MVP Scope used
|
|
159
|
+
to render one numbered bullet per group beneath a `## Grouping` container
|
|
160
|
+
directive — a plan the seed had already decided, at the grouping grain, before
|
|
161
|
+
`/mandrel-plan` read a word of it. N now reaches the planner **undecided**: it
|
|
162
|
+
applies its own cohesion judgment, and container grouping is `/mandrel-plan`'s
|
|
163
|
+
Gate #3 call at persist, where N is known. The grouping still drives the
|
|
164
|
+
**standalone-Stories** path (Phase 5b), which needs one issue payload per
|
|
165
|
+
group.
|
|
164
166
|
|
|
165
167
|
Chain into the existing planning entrypoint:
|
|
166
168
|
|
|
@@ -172,9 +174,10 @@ Chain into the existing planning entrypoint:
|
|
|
172
174
|
then runs its author → persist path, as documented in its workflow.
|
|
173
175
|
|
|
174
176
|
**Dedup provenance is carried mechanically — do not hand-copy it.** The seed's
|
|
175
|
-
MVP Scope
|
|
177
|
+
MVP Scope section carries each group's `audit-fingerprints` and
|
|
176
178
|
`audit-semantic-keys` footers as HTML comments (invisible in the rendered
|
|
177
|
-
one-pager
|
|
179
|
+
one-pager, and per-group even though the visible list is flat — they are the
|
|
180
|
+
identity the next sweep matches on). `plan-persist` harvests them out of the seed on the
|
|
178
181
|
`plan-context.json` envelope and appends them to **every** Story body it
|
|
179
182
|
persists, via `carryProvenanceFooters`
|
|
180
183
|
([`lib/findings/route-finding.js`](../scripts/lib/findings/route-finding.js)).
|