mandrel 2.24.0 → 2.26.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/agents/acceptance-critic.md +19 -21
- package/.agents/agents/auditor.md +16 -19
- package/.agents/agents/story-worker.md +41 -54
- package/.agents/audit-checklists/architecture.md +5 -1
- package/.agents/audit-checklists/baselines.md +21 -0
- package/.agents/audit-checklists/quality.md +3 -0
- package/.agents/docs/agentrc-reference.json +2 -1
- package/.agents/docs/configuration.md +2 -1
- package/.agents/docs/quality-gates.md +80 -18
- package/.agents/docs/workflows.md +4 -2
- package/.agents/instructions.md +1 -1
- package/.agents/rules/ci-remediation.md +68 -3
- package/.agents/schemas/agentrc.schema.json +6 -1
- package/.agents/schemas/audit-rules.json +15 -0
- package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +242 -0
- package/.agents/schemas/baselines/baseline-envelope.schema.json +8 -0
- package/.agents/schemas/baselines/crap.schema.json +13 -1
- package/.agents/schemas/crap-report.schema.json +37 -0
- package/.agents/schemas/model-attribution.schema.json +4 -0
- package/.agents/scripts/acceptance-eval.js +124 -15
- package/.agents/scripts/audit-baselines.js +136 -0
- package/.agents/scripts/audit-labels-bootstrap.js +16 -95
- package/.agents/scripts/audit-to-stories.js +33 -3
- package/.agents/scripts/check-arch-cycles.js +12 -93
- package/.agents/scripts/check-baseline-drift.js +16 -3
- package/.agents/scripts/check-baselines.js +19 -3
- package/.agents/scripts/check-context-budget.js +95 -14
- package/.agents/scripts/check-cyclomatic.js +214 -0
- package/.agents/scripts/check-schema-references.js +392 -0
- package/.agents/scripts/check-test-temp-hygiene.js +121 -1
- package/.agents/scripts/check-workflow-timeouts.js +291 -0
- package/.agents/scripts/cleanup-repo-test-temp.js +7 -5
- package/.agents/scripts/deliver-light.js +3 -4
- package/.agents/scripts/deliver-recover.js +13 -0
- package/.agents/scripts/diagnose-friction.js +85 -19
- package/.agents/scripts/lib/audit-baselines/engine.js +177 -0
- package/.agents/scripts/lib/audit-baselines/gate-surface.js +63 -0
- package/.agents/scripts/lib/audit-baselines/headroom.js +72 -0
- package/.agents/scripts/lib/audit-baselines/hotspots.js +69 -0
- package/.agents/scripts/lib/audit-baselines/kinds.js +313 -0
- package/.agents/scripts/lib/audit-baselines/outliers.js +100 -0
- package/.agents/scripts/lib/audit-baselines/read.js +87 -0
- package/.agents/scripts/lib/audit-baselines/staleness.js +123 -0
- package/.agents/scripts/lib/audit-baselines/surface-entry.js +106 -0
- package/.agents/scripts/lib/audit-baselines/trend.js +125 -0
- package/.agents/scripts/lib/audit-baselines/weights.js +193 -0
- package/.agents/scripts/lib/audit-suite/index.js +0 -5
- package/.agents/scripts/lib/audit-suite/selector.js +9 -62
- package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +177 -0
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -0
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +35 -3
- package/.agents/scripts/lib/audit-to-stories/group-findings.js +20 -11
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +23 -9
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +9 -2
- package/.agents/scripts/lib/baseline-loader.js +0 -0
- package/.agents/scripts/lib/baseline-schema-registry.js +13 -1
- package/.agents/scripts/lib/baselines/diff-scope-cli.js +22 -160
- package/.agents/scripts/lib/baselines/duplication-scanner.js +27 -0
- package/.agents/scripts/lib/baselines/git-base.js +0 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +499 -57
- package/.agents/scripts/lib/baselines/preview-gates.js +58 -15
- package/.agents/scripts/lib/baselines/reader.js +10 -0
- package/.agents/scripts/lib/baselines/refresh-service.js +69 -11
- package/.agents/scripts/lib/baselines/scope.js +39 -90
- package/.agents/scripts/lib/baselines/writer.js +16 -7
- package/.agents/scripts/lib/changed-files.js +8 -1
- package/.agents/scripts/lib/cli-args.js +115 -1
- package/.agents/scripts/lib/close-validation/runner.js +70 -25
- package/.agents/scripts/lib/config-settings-schema-delivery.js +5 -0
- package/.agents/scripts/lib/crap-engine.js +124 -27
- package/.agents/scripts/lib/crap-method-identity.js +153 -0
- package/.agents/scripts/lib/crap-utils.js +86 -13
- package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
- package/.agents/scripts/lib/env-loader.js +46 -16
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +0 -2
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +0 -2
- package/.agents/scripts/lib/findings/route-finding.js +123 -17
- package/.agents/scripts/lib/findings/severity.js +80 -2
- package/.agents/scripts/lib/git/cached-fetch.js +0 -0
- package/.agents/scripts/lib/git-utils.js +136 -80
- package/.agents/scripts/lib/import-graph.js +156 -0
- package/.agents/scripts/lib/label-constants.js +17 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +17 -2
- package/.agents/scripts/lib/observability/source-classifier.js +175 -2
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +110 -17
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +17 -12
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +36 -6
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +5 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/floors.js +12 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +548 -0
- package/.agents/scripts/lib/orchestration/deliver-recover.js +2 -2
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +10 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +39 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +71 -20
- package/.agents/scripts/lib/orchestration/light-backstop.js +20 -3
- package/.agents/scripts/lib/orchestration/light-escalation.js +82 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +103 -5
- package/.agents/scripts/lib/orchestration/plan-context.js +119 -66
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +31 -5
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +216 -109
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +61 -13
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +79 -22
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +51 -20
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +70 -74
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +231 -0
- package/.agents/scripts/lib/orchestration/resolve-stories.js +18 -17
- package/.agents/scripts/lib/orchestration/retro-proposals.js +0 -0
- package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -0
- package/.agents/scripts/lib/orchestration/run-scoped-config.js +276 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +41 -10
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +125 -37
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +6 -6
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +42 -38
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +129 -21
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +267 -125
- package/.agents/scripts/lib/orchestration/spec-budget.js +16 -5
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +182 -95
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +22 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +5 -11
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +4 -4
- package/.agents/scripts/lib/reserved-test-ids.js +77 -0
- package/.agents/scripts/lib/story-adjacency.js +3 -3
- package/.agents/scripts/lib/story-plan.js +137 -42
- package/.agents/scripts/lib/templates/decomposer-prompts.js +4 -4
- package/.agents/scripts/lib/test-runner-contract.js +134 -0
- package/.agents/scripts/lib/test-tiers.js +11 -2
- package/.agents/scripts/lib/ticket-body-sections.js +1 -1
- package/.agents/scripts/lib/util/concurrent-map.js +17 -0
- package/.agents/scripts/lib/util/parse-id-list.js +103 -0
- package/.agents/scripts/lib/util/poll-loop.js +18 -2
- package/.agents/scripts/lib/wave-runner/live-probe.js +29 -13
- package/.agents/scripts/lib/wave-runner/ready-set.js +254 -43
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +10 -13
- package/.agents/scripts/lib/workers/crap-worker.js +8 -13
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +4 -10
- package/.agents/scripts/lib/workers/maintainability-worker.js +4 -10
- package/.agents/scripts/lib/workers/serve-worker-messages.js +35 -0
- package/.agents/scripts/lib/worktree/git-hooks.js +206 -0
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +6 -0
- package/.agents/scripts/lib/worktree-manager.js +14 -0
- package/.agents/scripts/plan-run-epilogue.js +17 -5
- package/.agents/scripts/pr-watch-with-update.js +568 -213
- package/.agents/scripts/providers/github/tickets.js +33 -10
- package/.agents/scripts/provision-git-hooks.js +85 -0
- package/.agents/scripts/quality-preview.js +137 -28
- package/.agents/scripts/resolve-stories.js +4 -1
- package/.agents/scripts/run-coverage.js +86 -35
- package/.agents/scripts/run-lint.js +20 -0
- package/.agents/scripts/run-tests.js +36 -36
- package/.agents/scripts/single-story-close.js +34 -2
- package/.agents/scripts/single-story-confirm-merge.js +22 -6
- package/.agents/scripts/single-story-init.js +7 -0
- package/.agents/scripts/stories-wave-tick.js +308 -47
- package/.agents/scripts/story-plan.js +65 -9
- package/.agents/scripts/update-coverage-baseline.js +34 -4
- package/.agents/scripts/update-crap-baseline.js +42 -4
- package/.agents/scripts/update-duplication-baseline.js +209 -83
- package/.agents/scripts/update-maintainability-baseline.js +31 -4
- package/.agents/scripts/validate-docs-freshness.js +1 -0
- package/.agents/skills/core/diagnose-friction/SKILL.md +4 -1
- package/.agents/skills/core/gates-and-baselines/SKILL.md +17 -11
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/templates/single-story-body.md +16 -8
- package/.agents/workflows/audit-architecture.md +69 -13
- package/.agents/workflows/audit-baselines.md +289 -0
- package/.agents/workflows/audit-navigability.md +5 -4
- package/.agents/workflows/audit-quality.md +26 -0
- package/.agents/workflows/audit-to-stories.md +30 -4
- package/.agents/workflows/deliver.md +92 -97
- package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
- package/.agents/workflows/helpers/audit-lens-core.md +25 -4
- package/.agents/workflows/helpers/code-quality-guardrails.md +9 -2
- package/.agents/workflows/helpers/deliver-digest.md +41 -21
- package/.agents/workflows/helpers/deliver-reference.md +95 -4
- package/.agents/workflows/helpers/deliver-story-reference.md +174 -52
- package/.agents/workflows/helpers/deliver-story.md +104 -119
- package/.agents/workflows/helpers/plan-reference.md +49 -5
- package/.agents/workflows/memory-consolidate.md +116 -0
- package/.agents/workflows/plan.md +62 -75
- package/README.md +13 -6
- package/docs/CHANGELOG.md +93 -0
- package/lib/cli/update.js +14 -11
- package/lib/cli/version-check.js +9 -1
- package/package.json +9 -4
- package/.agents/schemas/friction-event.schema.json +0 -56
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +0 -707
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
*
|
|
19
19
|
* - **dangling-citation** — a sentence referencing a document section
|
|
20
20
|
* (`§`, "design note", "review doc") with no repo-relative path and no
|
|
21
|
-
* `#<digits>` issue anchor in the same sentence.
|
|
21
|
+
* `#<digits>` issue anchor in the same sentence. An anchor written
|
|
22
|
+
* inside a code span counts — the conventional markdown form (Story
|
|
23
|
+
* #4906).
|
|
22
24
|
* - **open-question** — interrogative-to-operator phrasing ("Flag if",
|
|
23
25
|
* "TBD", "confirm with the operator", a trailing `?`) in Goal/Spec
|
|
24
26
|
* prose outside code spans. Bodies record decisions; unresolved
|
|
@@ -73,28 +75,55 @@ const OPEN_QUESTION_MARKERS = [
|
|
|
73
75
|
*/
|
|
74
76
|
|
|
75
77
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
+
* Private-use sentinel standing in for one extracted inline code span.
|
|
79
|
+
* It carries no citation marker, no anchor and no sentence boundary, so it
|
|
80
|
+
* is inert for every marker heuristic while recording where the span sat.
|
|
81
|
+
*/
|
|
82
|
+
const CODE_SLOT_PATTERN = /\uE000(\d+)\uE001/g;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Replace fenced code blocks with a space and each inline code span with a
|
|
86
|
+
* positional slot, so code content (shell snippets, grep patterns, JSON)
|
|
87
|
+
* never trips a prose heuristic yet stays recoverable for the anchor check.
|
|
78
88
|
*
|
|
79
89
|
* @param {string} text
|
|
80
|
-
* @returns {string}
|
|
90
|
+
* @returns {{ slotted: string, spans: string[] }}
|
|
81
91
|
*/
|
|
82
|
-
function
|
|
83
|
-
|
|
92
|
+
function slotCodeSpans(text) {
|
|
93
|
+
const spans = [];
|
|
94
|
+
const slotted = text
|
|
95
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
96
|
+
.replace(/`[^`\n]*`/g, (span) => {
|
|
97
|
+
spans.push(span.slice(1, -1));
|
|
98
|
+
return `\uE000${spans.length - 1}\uE001`;
|
|
99
|
+
});
|
|
100
|
+
return { slotted, spans };
|
|
84
101
|
}
|
|
85
102
|
|
|
86
103
|
/**
|
|
87
104
|
* Split prose into sentence-ish units. Newlines are boundaries too, so a
|
|
88
|
-
* bullet list yields one unit per bullet.
|
|
105
|
+
* bullet list yields one unit per bullet. Each unit carries two views of
|
|
106
|
+
* the same sentence: `prose`, with code content removed, which every
|
|
107
|
+
* marker heuristic reads; and `anchorText`, with inline code content
|
|
108
|
+
* restored, so a citation anchored inside a code span is still visible to
|
|
109
|
+
* the anchor check. Restoring only ever adds anchors — markers are matched
|
|
110
|
+
* against `prose` alone, exactly as before.
|
|
89
111
|
*
|
|
90
112
|
* @param {string} text
|
|
91
|
-
* @returns {string
|
|
113
|
+
* @returns {Array<{ prose: string, anchorText: string }>}
|
|
92
114
|
*/
|
|
93
115
|
function splitSentences(text) {
|
|
94
|
-
|
|
116
|
+
const { slotted, spans } = slotCodeSpans(text);
|
|
117
|
+
return slotted
|
|
95
118
|
.split(/(?<=[.!?])\s+|\n+/)
|
|
96
|
-
.map((
|
|
97
|
-
|
|
119
|
+
.map((unit) => ({
|
|
120
|
+
prose: unit.replace(CODE_SLOT_PATTERN, ' ').trim(),
|
|
121
|
+
anchorText: unit.replace(
|
|
122
|
+
CODE_SLOT_PATTERN,
|
|
123
|
+
(_slot, index) => ` ${spans[Number(index)]} `,
|
|
124
|
+
),
|
|
125
|
+
}))
|
|
126
|
+
.filter((unit) => unit.prose.length > 0);
|
|
98
127
|
}
|
|
99
128
|
|
|
100
129
|
/**
|
|
@@ -112,18 +141,21 @@ function excerpt(text) {
|
|
|
112
141
|
|
|
113
142
|
/**
|
|
114
143
|
* dangling-citation: a citation-marker sentence with no locating anchor.
|
|
144
|
+
* The marker is matched against the sentence's code-stripped prose; the
|
|
145
|
+
* anchor against its code-restored text, so a path or issue reference
|
|
146
|
+
* written in a code span — the conventional markdown form — counts.
|
|
115
147
|
*
|
|
116
|
-
* @param {string} prose -
|
|
148
|
+
* @param {string} prose - Raw body prose.
|
|
117
149
|
* @param {string} slug
|
|
118
150
|
* @returns {TextHygieneFinding[]}
|
|
119
151
|
*/
|
|
120
152
|
function findDanglingCitations(prose, slug) {
|
|
121
153
|
const findings = [];
|
|
122
|
-
for (const sentence of splitSentences(prose)) {
|
|
154
|
+
for (const { prose: sentence, anchorText } of splitSentences(prose)) {
|
|
123
155
|
const cites = CITATION_MARKERS.some((m) => m.test(sentence));
|
|
124
156
|
if (!cites) continue;
|
|
125
157
|
const anchored =
|
|
126
|
-
ISSUE_ANCHOR.test(
|
|
158
|
+
ISSUE_ANCHOR.test(anchorText) || REPO_PATH_ANCHOR.test(anchorText);
|
|
127
159
|
if (anchored) continue;
|
|
128
160
|
findings.push({
|
|
129
161
|
kind: 'dangling-citation',
|
|
@@ -142,13 +174,13 @@ function findDanglingCitations(prose, slug) {
|
|
|
142
174
|
* open-question: operator-directed phrasing (or a trailing `?`) in prose a
|
|
143
175
|
* non-interactive sub-agent executes.
|
|
144
176
|
*
|
|
145
|
-
* @param {string} prose -
|
|
177
|
+
* @param {string} prose - Raw Goal/Spec prose.
|
|
146
178
|
* @param {string} slug
|
|
147
179
|
* @returns {TextHygieneFinding[]}
|
|
148
180
|
*/
|
|
149
181
|
function findOpenQuestions(prose, slug) {
|
|
150
182
|
const findings = [];
|
|
151
|
-
for (const sentence of splitSentences(prose)) {
|
|
183
|
+
for (const { prose: sentence } of splitSentences(prose)) {
|
|
152
184
|
const marked =
|
|
153
185
|
OPEN_QUESTION_MARKERS.some((m) => m.test(sentence)) ||
|
|
154
186
|
sentence.endsWith('?');
|
|
@@ -217,12 +249,11 @@ export function evaluateTextHygiene({ draftStories = null } = {}) {
|
|
|
217
249
|
}
|
|
218
250
|
const goal = typeof body.goal === 'string' ? body.goal : '';
|
|
219
251
|
const spec = typeof body.spec === 'string' ? body.spec : '';
|
|
220
|
-
const bodyProse =
|
|
221
|
-
typeof story.body === 'string' ? story.body : [goal, spec].join('\n')
|
|
222
|
-
);
|
|
252
|
+
const bodyProse =
|
|
253
|
+
typeof story.body === 'string' ? story.body : [goal, spec].join('\n');
|
|
223
254
|
findings.push(
|
|
224
255
|
...findDanglingCitations(bodyProse, slug),
|
|
225
|
-
...findOpenQuestions(
|
|
256
|
+
...findOpenQuestions([goal, spec].join('\n'), slug),
|
|
226
257
|
...findSlicingMass(body, slug),
|
|
227
258
|
);
|
|
228
259
|
}
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
* the provider call needed to load the Epic.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import * as os from 'node:os';
|
|
11
10
|
import path from 'node:path';
|
|
12
11
|
import {
|
|
13
12
|
resolveFeatureRoots,
|
|
@@ -15,39 +14,15 @@ import {
|
|
|
15
14
|
} from '../../bdd-runner-detect.js';
|
|
16
15
|
import { scanBddScenarios } from '../../bdd-scenario-scanner.js';
|
|
17
16
|
import { getPaths, PROJECT_ROOT } from '../../config-resolver.js';
|
|
18
|
-
import { scanMemoryFreshness } from '../../feedback-loop/memory-freshness.js';
|
|
19
17
|
import { fetchPriorFeedback } from '../../feedback-loop/prior-feedback-fetcher.js';
|
|
20
18
|
import { Logger } from '../../Logger.js';
|
|
21
19
|
import { hasTicketSection } from '../../ticket-body-sections.js';
|
|
20
|
+
import {
|
|
21
|
+
concurrentMap,
|
|
22
|
+
FANOUT_CONCURRENCY,
|
|
23
|
+
} from '../../util/concurrent-map.js';
|
|
22
24
|
import { ensureDocsDigest } from '../docs-digest.js';
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Resolve the per-project memory directory used by the memory-freshness
|
|
26
|
-
* pre-flight (Story #2557 / Epic #2547).
|
|
27
|
-
*
|
|
28
|
-
* Resolution order:
|
|
29
|
-
* 1. `MANDREL_MEMORY_DIR` environment variable (test seam and operator
|
|
30
|
-
* override).
|
|
31
|
-
* 2. `~/.claude/projects/<repo>/memory/` — the standard Claude Code
|
|
32
|
-
* memory substrate path, scoped by the configured GitHub repo so each
|
|
33
|
-
* consumer project gets its own memory pool.
|
|
34
|
-
* 3. `null` when neither is resolvable. The scanner tolerates a missing
|
|
35
|
-
* `memoryDir` and surfaces a single `errors[]` entry.
|
|
36
|
-
*
|
|
37
|
-
* @param {{ github?: { owner?: string, repo?: string }|null }} opts
|
|
38
|
-
* @returns {string|null}
|
|
39
|
-
*/
|
|
40
|
-
function resolveMemoryDir({ github } = {}) {
|
|
41
|
-
if (
|
|
42
|
-
typeof process.env.MANDREL_MEMORY_DIR === 'string' &&
|
|
43
|
-
process.env.MANDREL_MEMORY_DIR.length > 0
|
|
44
|
-
) {
|
|
45
|
-
return process.env.MANDREL_MEMORY_DIR;
|
|
46
|
-
}
|
|
47
|
-
const repo = github?.repo;
|
|
48
|
-
if (typeof repo !== 'string' || repo.length === 0) return null;
|
|
49
|
-
return path.join(os.homedir(), '.claude', 'projects', repo, 'memory');
|
|
50
|
-
}
|
|
25
|
+
import { buildMemoryPoolAdvisory } from './memory-pool-advisory.js';
|
|
51
26
|
|
|
52
27
|
/**
|
|
53
28
|
* Build the digest-first `docsContext` envelope field (Story #4433 — hard
|
|
@@ -95,6 +70,24 @@ async function buildPlanningDocsContext({ seedIssueId, settings, cwd }) {
|
|
|
95
70
|
return { mode: 'digest', digestPath: relPath };
|
|
96
71
|
}
|
|
97
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Story #2637 — index existing BDD scenarios so the Acceptance Engineer step
|
|
75
|
+
* can annotate planned ACs with matches from the project's `.feature` files.
|
|
76
|
+
* Empty array when the project has not adopted BDD; the scanner is
|
|
77
|
+
* best-effort and never throws on filesystem errors.
|
|
78
|
+
*
|
|
79
|
+
* @returns {Array<object>}
|
|
80
|
+
*/
|
|
81
|
+
function scanBddScenariosBestEffort() {
|
|
82
|
+
try {
|
|
83
|
+
const featureRoots = resolveFeatureRoots({ cwd: PROJECT_ROOT });
|
|
84
|
+
return scanBddScenarios({ featureRoots });
|
|
85
|
+
} catch (err) {
|
|
86
|
+
Logger.warn(`[plan-context] BDD scenario scan skipped: ${err.message}`);
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
98
91
|
/**
|
|
99
92
|
* Build the authoring context the host LLM (or the
|
|
100
93
|
* `/plan` author step) needs to write the Tech Spec.
|
|
@@ -130,51 +123,54 @@ export async function buildAuthoringContext(
|
|
|
130
123
|
|
|
131
124
|
const { cwd = PROJECT_ROOT } = opts;
|
|
132
125
|
|
|
133
|
-
const docsContext = await buildPlanningDocsContext({
|
|
134
|
-
seedIssueId: epic.id,
|
|
135
|
-
settings,
|
|
136
|
-
cwd,
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// Story #2094 Task #2103 — verify the project's BDD runner pending-tag
|
|
140
|
-
// support so the acceptance-spec body can record either the verified tag
|
|
141
|
-
// (features-first ordering) or "fallback: dependencies-first ordering"
|
|
142
|
-
// when no supported runner is present.
|
|
143
|
-
const bddRunner = await verifyBddRunnerPendingTag({ cwd: PROJECT_ROOT });
|
|
144
|
-
|
|
145
|
-
// Story #2637 — index existing BDD scenarios so the Acceptance Engineer
|
|
146
|
-
// step can annotate planned ACs with matches from the project's
|
|
147
|
-
// `.feature` files. Empty array when the project has not adopted BDD;
|
|
148
|
-
// the scanner is best-effort and never throws on filesystem errors.
|
|
149
|
-
let bddScenarios = [];
|
|
150
|
-
try {
|
|
151
|
-
const featureRoots = resolveFeatureRoots({ cwd: PROJECT_ROOT });
|
|
152
|
-
bddScenarios = scanBddScenarios({ featureRoots });
|
|
153
|
-
} catch (err) {
|
|
154
|
-
Logger.warn(`[plan-context] BDD scenario scan skipped: ${err.message}`);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Story #2557 — memory-freshness pre-flight runs BEFORE the prior-feedback
|
|
158
|
-
// fetch so the planner sees a deduplicated, currently-actionable memory
|
|
159
|
-
// store. The scanner is best-effort: missing memory dir or gh-CLI failures
|
|
160
|
-
// land in `memoryFreshness.errors[]` and never throw.
|
|
161
126
|
const githubCfg = opts.github ?? null;
|
|
162
|
-
const memoryDir = resolveMemoryDir({ github: githubCfg });
|
|
163
|
-
const memoryFreshness = await scanMemoryFreshness({
|
|
164
|
-
memoryDir,
|
|
165
|
-
owner: githubCfg?.owner,
|
|
166
|
-
repo: githubCfg?.repo,
|
|
167
|
-
projectRoot: PROJECT_ROOT,
|
|
168
|
-
});
|
|
169
127
|
|
|
170
|
-
// Story #
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
128
|
+
// Story #4952 — these five gathers share no data, so they run under bounded
|
|
129
|
+
// concurrency instead of five sequential awaits on the interactive `/plan`
|
|
130
|
+
// path. `concurrentMap` preserves input order, so the destructuring is
|
|
131
|
+
// positional and the produced context is identical to the serial build:
|
|
132
|
+
//
|
|
133
|
+
// 1. the digest-first `docsContext` pointer (Story #4433);
|
|
134
|
+
// 2. Story #2094 Task #2103 — the project's BDD runner pending-tag
|
|
135
|
+
// support, so the acceptance-spec body records either the verified tag
|
|
136
|
+
// (features-first ordering) or "fallback: dependencies-first ordering"
|
|
137
|
+
// when no supported runner is present;
|
|
138
|
+
// 3. the best-effort `.feature` scenario index;
|
|
139
|
+
// 4. Story #4919 — the memory-pool advisory that replaced the retired
|
|
140
|
+
// memory-freshness pre-flight (#2557 / #4414) in the same slot. The
|
|
141
|
+
// scanner marked an entry stale when a cited issue was closed, but the
|
|
142
|
+
// memory corpus is delivery retrospectives whose subject IS a delivered
|
|
143
|
+
// Story — and its directory (`~/.claude/projects/<repo>/memory/`) never
|
|
144
|
+
// resolved, because harness project dirs are cwd-slugs. This renders no
|
|
145
|
+
// per-entry verdict at all: it stats and counts, and the `/plan` spine
|
|
146
|
+
// surfaces `recommend` at Gate #1. Filesystem-only and total;
|
|
147
|
+
// 5. Story #2554 — open meta feedback issues, so retro signals are routed
|
|
148
|
+
// into durable substrates rather than lost in chat. Best-effort:
|
|
149
|
+
// missing owner/repo or gh-CLI failures land in `errors[]`, never throw.
|
|
150
|
+
const [
|
|
151
|
+
docsContext,
|
|
152
|
+
bddRunner,
|
|
153
|
+
bddScenarios,
|
|
154
|
+
memoryPoolAdvisory,
|
|
155
|
+
priorFeedback,
|
|
156
|
+
] = await concurrentMap(
|
|
157
|
+
[
|
|
158
|
+
() => buildPlanningDocsContext({ seedIssueId: epic.id, settings, cwd }),
|
|
159
|
+
() => verifyBddRunnerPendingTag({ cwd: PROJECT_ROOT }),
|
|
160
|
+
() => scanBddScenariosBestEffort(),
|
|
161
|
+
() => buildMemoryPoolAdvisory({ cwd: PROJECT_ROOT }),
|
|
162
|
+
() =>
|
|
163
|
+
fetchPriorFeedback({
|
|
164
|
+
owner: githubCfg?.owner,
|
|
165
|
+
repo: githubCfg?.repo,
|
|
166
|
+
}),
|
|
167
|
+
],
|
|
168
|
+
(gather) => gather(),
|
|
169
|
+
// The independent context gathers (Story #4952): a handful of local
|
|
170
|
+
// probes plus one `gh` read, so this rides the shared fan-out bound
|
|
171
|
+
// rather than declaring its own.
|
|
172
|
+
{ concurrency: FANOUT_CONCURRENCY },
|
|
173
|
+
);
|
|
178
174
|
|
|
179
175
|
// Story #4811 — the codebase snapshot (#2634), its authoring grounding
|
|
180
176
|
// (#4139 F10) and the spec-freshness helpers behind it are retired. The
|
|
@@ -211,7 +207,7 @@ export async function buildAuthoringContext(
|
|
|
211
207
|
docsContext,
|
|
212
208
|
bddRunner,
|
|
213
209
|
bddScenarios,
|
|
214
|
-
|
|
210
|
+
memoryPoolAdvisory,
|
|
215
211
|
priorFeedback,
|
|
216
212
|
};
|
|
217
213
|
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-pool-advisory.js — the `/plan` Phase 0 memory-hygiene advisory.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the retired memory-freshness pre-flight (Story #2557 / #4414) in
|
|
5
|
+
* the same slot, fixing both of that design's defects:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Correct pool resolution.** The retired `resolveMemoryDir` built
|
|
8
|
+
* `~/.claude/projects/<github.repo>/memory/`, but harness project
|
|
9
|
+
* directories are **cwd-slugs** — the absolute cwd with every `/` and `.`
|
|
10
|
+
* replaced by `-` — so the old path never resolved in any consumer and
|
|
11
|
+
* the scan was a silent no-op everywhere.
|
|
12
|
+
* 2. **A named consumer.** The retired scanner emitted a per-entry staleness
|
|
13
|
+
* verdict nothing read. This emits one advisory the `/plan` spine
|
|
14
|
+
* surfaces at Gate #1, recommending `/memory-consolidate`.
|
|
15
|
+
*
|
|
16
|
+
* It also drops the semantic that made the old scanner unfixable: it renders
|
|
17
|
+
* **no per-entry verdict at all**. A memory citing a closed issue is a
|
|
18
|
+
* delivery retrospective whose subject is that issue — not a stale entry — and
|
|
19
|
+
* only the attended `/memory-consolidate` pass, reading content, can tell the
|
|
20
|
+
* difference. This module counts and stats; it never judges an entry.
|
|
21
|
+
*
|
|
22
|
+
* Detection is filesystem-only — no child processes, no `gh` probes, no
|
|
23
|
+
* network. Every failure path fails soft to "no pool, no recommendation": the
|
|
24
|
+
* advisory can degrade the nudge, never a plan.
|
|
25
|
+
*
|
|
26
|
+
* Test seams: `cwd`, `env`, `fsImpl` (node:fs-compatible `statSync` /
|
|
27
|
+
* `readdirSync` / `readFileSync`), `now`, and the two thresholds.
|
|
28
|
+
*
|
|
29
|
+
* `buildMemoryPoolAdvisory` is the **only** export: the helpers below have no
|
|
30
|
+
* caller outside this module, and exporting one solely for a test would add a
|
|
31
|
+
* row to the `dead-exports-production` ratchet (the `buildUiSurfaceSignal`
|
|
32
|
+
* precedent). Tests reach every branch through the seams above — do not
|
|
33
|
+
* "fix" the missing exports.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import * as defaultFs from 'node:fs';
|
|
37
|
+
import * as os from 'node:os';
|
|
38
|
+
import * as path from 'node:path';
|
|
39
|
+
|
|
40
|
+
/** Recommend a consolidation pass once the stamp is this old. */
|
|
41
|
+
const STALE_AFTER_DAYS = 30;
|
|
42
|
+
|
|
43
|
+
/** Recommend a consolidation pass once the pool holds more entries than this. */
|
|
44
|
+
const ENTRY_COUNT_CEILING = 100;
|
|
45
|
+
|
|
46
|
+
/** Stamp file written by `/memory-consolidate` after its operator gate. */
|
|
47
|
+
const STAMP_FILENAME = '.consolidation-stamp.json';
|
|
48
|
+
|
|
49
|
+
/** The index file is not itself a memory entry. */
|
|
50
|
+
const INDEX_FILENAME = 'MEMORY.md';
|
|
51
|
+
|
|
52
|
+
const MS_PER_DAY = 86_400_000;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Slugify an absolute path the way the harness names its per-project
|
|
56
|
+
* directories: every `/` and `.` becomes `-`. Verified against real
|
|
57
|
+
* directories in `~/.claude/projects/` — a plain checkout and a worktree both
|
|
58
|
+
* round-trip exactly.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} absPath
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
function slugifyProjectPath(absPath) {
|
|
64
|
+
return String(absPath ?? '').replace(/[/.]/g, '-');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the memory pool directory for a working directory.
|
|
69
|
+
*
|
|
70
|
+
* `MANDREL_MEMORY_DIR` wins outright (operator override and test seam);
|
|
71
|
+
* otherwise `~/.claude/projects/<cwd-slug>/memory/`.
|
|
72
|
+
*
|
|
73
|
+
* @param {{ cwd?: string, env?: Record<string,string|undefined>, homedir?: string }} [opts]
|
|
74
|
+
* @returns {string|null} absolute pool path, or `null` when unresolvable
|
|
75
|
+
*/
|
|
76
|
+
function resolveMemoryPoolDir({ cwd, env = process.env, homedir } = {}) {
|
|
77
|
+
const override = env?.MANDREL_MEMORY_DIR;
|
|
78
|
+
if (typeof override === 'string' && override.length > 0) return override;
|
|
79
|
+
|
|
80
|
+
const base = typeof cwd === 'string' && cwd.length > 0 ? cwd : null;
|
|
81
|
+
if (!base) return null;
|
|
82
|
+
|
|
83
|
+
const home =
|
|
84
|
+
typeof homedir === 'string' && homedir.length > 0 ? homedir : os.homedir();
|
|
85
|
+
if (!home) return null;
|
|
86
|
+
|
|
87
|
+
return path.join(
|
|
88
|
+
home,
|
|
89
|
+
'.claude',
|
|
90
|
+
'projects',
|
|
91
|
+
slugifyProjectPath(base),
|
|
92
|
+
'memory',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Read the consolidation stamp, returning its ISO timestamp or `null`.
|
|
98
|
+
* A missing, unreadable, unparseable, or malformed stamp is indistinguishable
|
|
99
|
+
* from "never consolidated" — all four mean the same thing to the advisory.
|
|
100
|
+
*
|
|
101
|
+
* @returns {string|null}
|
|
102
|
+
*/
|
|
103
|
+
function readStamp({ poolDir, fsImpl }) {
|
|
104
|
+
try {
|
|
105
|
+
const raw = fsImpl.readFileSync(path.join(poolDir, STAMP_FILENAME), 'utf8');
|
|
106
|
+
const parsed = JSON.parse(raw);
|
|
107
|
+
const value = parsed?.lastConsolidatedAt;
|
|
108
|
+
if (typeof value !== 'string' || value.length === 0) return null;
|
|
109
|
+
return Number.isNaN(Date.parse(value)) ? null : value;
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Count memory entries — `.md` files other than the index.
|
|
117
|
+
*
|
|
118
|
+
* @returns {number|null} `null` when the directory cannot be listed
|
|
119
|
+
*/
|
|
120
|
+
function countEntries({ poolDir, fsImpl }) {
|
|
121
|
+
try {
|
|
122
|
+
return fsImpl
|
|
123
|
+
.readdirSync(poolDir)
|
|
124
|
+
.filter((name) => name.endsWith('.md') && name !== INDEX_FILENAME).length;
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Build the `memoryPoolAdvisory` envelope field.
|
|
132
|
+
*
|
|
133
|
+
* Advisory only — it carries **no routing authority**, mirroring
|
|
134
|
+
* `deliverLightSuggestion`. The `/plan` spine surfaces `recommend` at Gate #1;
|
|
135
|
+
* nothing auto-runs, and nothing here mutates the operator's memory store.
|
|
136
|
+
*
|
|
137
|
+
* @param {object} [opts]
|
|
138
|
+
* @param {string} [opts.cwd] — defaults to `process.cwd()`
|
|
139
|
+
* @param {Record<string,string|undefined>} [opts.env]
|
|
140
|
+
* @param {object} [opts.fsImpl] — node:fs-compatible seam
|
|
141
|
+
* @param {string} [opts.homedir]
|
|
142
|
+
* @param {Date|string|number} [opts.now]
|
|
143
|
+
* @param {number} [opts.staleAfterDays]
|
|
144
|
+
* @param {number} [opts.entryCountCeiling]
|
|
145
|
+
* @returns {{ present: boolean, entryCount: number, lastConsolidatedAt: string|null,
|
|
146
|
+
* recommend: boolean, reasons: string[] }}
|
|
147
|
+
*/
|
|
148
|
+
export function buildMemoryPoolAdvisory({
|
|
149
|
+
cwd = process.cwd(),
|
|
150
|
+
env = process.env,
|
|
151
|
+
fsImpl = defaultFs,
|
|
152
|
+
homedir,
|
|
153
|
+
now = new Date(),
|
|
154
|
+
staleAfterDays = STALE_AFTER_DAYS,
|
|
155
|
+
entryCountCeiling = ENTRY_COUNT_CEILING,
|
|
156
|
+
} = {}) {
|
|
157
|
+
const absent = (reason) => ({
|
|
158
|
+
present: false,
|
|
159
|
+
entryCount: 0,
|
|
160
|
+
lastConsolidatedAt: null,
|
|
161
|
+
recommend: false,
|
|
162
|
+
reasons: [reason],
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const poolDir = resolveMemoryPoolDir({ cwd, env, homedir });
|
|
166
|
+
if (!poolDir) {
|
|
167
|
+
return absent(
|
|
168
|
+
'no memory pool could be resolved for this working directory',
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let isDir = false;
|
|
173
|
+
try {
|
|
174
|
+
isDir = fsImpl.statSync(poolDir).isDirectory();
|
|
175
|
+
} catch {
|
|
176
|
+
isDir = false;
|
|
177
|
+
}
|
|
178
|
+
if (!isDir) {
|
|
179
|
+
return absent(`no memory pool at ${poolDir} — nothing to consolidate`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const entryCount = countEntries({ poolDir, fsImpl });
|
|
183
|
+
if (entryCount === null) {
|
|
184
|
+
return absent(`memory pool at ${poolDir} could not be listed`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const lastConsolidatedAt = readStamp({ poolDir, fsImpl });
|
|
188
|
+
const reasons = [];
|
|
189
|
+
|
|
190
|
+
// An empty pool has nothing to consolidate, whatever the stamp says.
|
|
191
|
+
if (entryCount === 0) {
|
|
192
|
+
return {
|
|
193
|
+
present: true,
|
|
194
|
+
entryCount: 0,
|
|
195
|
+
lastConsolidatedAt,
|
|
196
|
+
recommend: false,
|
|
197
|
+
reasons: ['memory pool is empty — nothing to consolidate'],
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (lastConsolidatedAt === null) {
|
|
202
|
+
reasons.push(
|
|
203
|
+
'no consolidation stamp — this pool has never been consolidated',
|
|
204
|
+
);
|
|
205
|
+
} else {
|
|
206
|
+
const ageDays =
|
|
207
|
+
(new Date(now).getTime() - Date.parse(lastConsolidatedAt)) / MS_PER_DAY;
|
|
208
|
+
if (ageDays > staleAfterDays) {
|
|
209
|
+
reasons.push(
|
|
210
|
+
`last consolidated ${Math.floor(ageDays)} days ago (over the ${staleAfterDays}-day threshold)`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (entryCount > entryCountCeiling) {
|
|
216
|
+
reasons.push(
|
|
217
|
+
`${entryCount} entries (over the ${entryCountCeiling}-entry threshold)`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
present: true,
|
|
223
|
+
entryCount,
|
|
224
|
+
lastConsolidatedAt,
|
|
225
|
+
recommend: reasons.length > 0,
|
|
226
|
+
reasons:
|
|
227
|
+
reasons.length > 0
|
|
228
|
+
? reasons
|
|
229
|
+
: ['memory pool is within both freshness thresholds'],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
extractChangePaths,
|
|
35
35
|
parse as parseStoryBody,
|
|
36
36
|
} from '../story-body/story-body.js';
|
|
37
|
+
import { expandIdList } from '../util/parse-id-list.js';
|
|
37
38
|
import { resolveStoryDispatchMode } from './complexity-gate.js';
|
|
38
39
|
|
|
39
40
|
/** Labels/state that mean a blocker no longer gates its dependents. */
|
|
@@ -355,29 +356,29 @@ export function buildStoriesEnvelope({
|
|
|
355
356
|
}
|
|
356
357
|
|
|
357
358
|
/**
|
|
358
|
-
* Parse and validate the `--ids` list.
|
|
359
|
+
* Parse and validate the `--ids` list, expanding any `A-B` dash range.
|
|
360
|
+
*
|
|
361
|
+
* A contiguous span is how an operator names a plan run — `/deliver 4922 -
|
|
362
|
+
* 4926` — so the range is expanded here rather than transcribed by the host.
|
|
363
|
+
* `stories-wave-tick.js --stories` reads through this same function, which is
|
|
364
|
+
* what keeps the sequencing set identical to the resolved one.
|
|
359
365
|
*
|
|
360
366
|
* @param {string|undefined} raw
|
|
367
|
+
* @param {string} [flag] Flag name, for the error message.
|
|
361
368
|
* @returns {number[]}
|
|
362
369
|
*/
|
|
363
|
-
export function parseIds(raw) {
|
|
364
|
-
const ids =
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
throw new Error(
|
|
372
|
-
`[resolve-stories] --ids must be a comma-separated list of positive issue numbers (got "${s}").`,
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
return n;
|
|
376
|
-
});
|
|
370
|
+
export function parseIds(raw, flag = '--ids') {
|
|
371
|
+
const { ids, error } = expandIdList(raw, {
|
|
372
|
+
flag,
|
|
373
|
+
prefix: '[resolve-stories] ',
|
|
374
|
+
});
|
|
375
|
+
if (error) {
|
|
376
|
+
throw new Error(error);
|
|
377
|
+
}
|
|
377
378
|
if (ids.length === 0) {
|
|
378
379
|
throw new Error(
|
|
379
|
-
|
|
380
|
+
`[resolve-stories] ${flag} is required: node resolve-stories.js --ids 101,102 (or a range: --ids 101-104)`,
|
|
380
381
|
);
|
|
381
382
|
}
|
|
382
|
-
return
|
|
383
|
+
return ids;
|
|
383
384
|
}
|
|
Binary file
|
|
@@ -494,6 +494,18 @@ async function executeAuditRoster({
|
|
|
494
494
|
? selectedAudits.map((lens) => `- \`${lens}\``)
|
|
495
495
|
: ['- _(none — docs-only or no matching change-set lenses)_']),
|
|
496
496
|
'',
|
|
497
|
+
// Story #4949 — the roster used to name the lenses and say nothing about
|
|
498
|
+
// how to dispatch them, which made a serial walk (and a nested
|
|
499
|
+
// coordinator) fully compliant with it. The lenses are read-only and share
|
|
500
|
+
// no write paths, so they are the textbook independent fan-out; naming the
|
|
501
|
+
// shape here is what turns that from an option into the instruction.
|
|
502
|
+
'**Dispatch shape (MUST): flat, parallel, one turn.** Spawn one ' +
|
|
503
|
+
'`auditor` sub-agent per lens listed above and issue every one of those ' +
|
|
504
|
+
'spawns in a SINGLE turn — no nested fan-out, no serial walk. A ' +
|
|
505
|
+
'coordinator sub-agent that re-dispatches the lenses is the failure ' +
|
|
506
|
+
'this line exists to prevent: a grandchild routes its findings to the ' +
|
|
507
|
+
'wrong parent or loses them outright.',
|
|
508
|
+
'',
|
|
497
509
|
'```json',
|
|
498
510
|
JSON.stringify(
|
|
499
511
|
{
|