session-orchestrator 4.1.0 → 5.0.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/skills/session-plan/SKILL.md +1 -1
- package/.agents/skills/session-start/SKILL.md +1 -1
- package/.agents/skills/ux-grill/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -2
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/session-plan/SKILL.md +1 -1
- package/.codex-plugin/skills/session-start/SKILL.md +1 -1
- package/.codex-plugin/skills/ux-grill/SKILL.md +21 -0
- package/.codex-plugin/skills/ux-grill/agents/openai.yaml +5 -0
- package/.cursor/commands/ux-grill.md +14 -0
- package/.cursor/skills/session-plan/SKILL.md +1 -1
- package/.cursor/skills/session-start/SKILL.md +1 -1
- package/.cursor/skills/ux-grill/SKILL.md +13 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +128 -1
- package/README.md +98 -86
- package/agents/dialectic-deriver.md +11 -0
- package/agents/ux-evaluator.md +1 -1
- package/commands/close.md +3 -3
- package/commands/go.md +2 -0
- package/commands/memory-cleanup.md +4 -3
- package/commands/persona-panel.md +1 -1
- package/commands/session.md +3 -2
- package/commands/ux-grill.md +51 -0
- package/docs/README.md +4 -4
- package/docs/USER-GUIDE.md +117 -50
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +55 -1
- package/docs/ci-setup.md +1 -1
- package/docs/codex-setup.md +9 -0
- package/docs/components.md +9 -9
- package/docs/cursor-setup.md +1 -0
- package/docs/events-schema.md +13 -6
- package/docs/github-mirror-protection.md +61 -20
- package/docs/instruction-delivery.md +1 -1
- package/docs/memory-proposal-flow.md +3 -3
- package/docs/migration-v4.md +2 -2
- package/docs/migration-v5.md +62 -0
- package/docs/owner-config-schema.md +74 -90
- package/docs/persona-panel.md +4 -4
- package/docs/pi-setup.md +1 -0
- package/docs/rule-authoring.md +13 -6
- package/docs/scope-collision-guard.md +16 -0
- package/docs/session-config-reference.md +55 -22
- package/docs/session-config-template.md +9 -5
- package/docs/vault-docs-architecture.md +4 -2
- package/hooks/_lib/hook-import-set.json +70 -3
- package/hooks/_lib/lock-bootstrap.mjs +84 -1
- package/hooks/_lib/vcs-create-matcher.mjs +401 -16
- package/hooks/enforce-scope.mjs +201 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks.json +7 -2
- package/hooks/on-session-start.mjs +171 -49
- package/hooks/post-bash-issue-budget-refund.mjs +375 -0
- package/hooks/pre-auq-clarity.mjs +70 -18
- package/hooks/pre-bash-issue-budget.mjs +170 -26
- package/hooks/subagent-telemetry.mjs +106 -20
- package/package.json +5 -4
- package/pi/prompts/ux-grill.md +12 -0
- package/scripts/baseline-archetypes.mjs +28 -0
- package/scripts/ci/assert-vitest-green.mjs +4 -2
- package/scripts/dialectic-deriver.mjs +32 -8
- package/scripts/emit-session.mjs +72 -1
- package/scripts/lib/agent-status.mjs +441 -9
- package/scripts/lib/auq/schema.mjs +10 -3
- package/scripts/lib/auto-dialectic.mjs +0 -68
- package/scripts/lib/baseline-archetypes.mjs +439 -0
- package/scripts/lib/build-live-signals.mjs +5 -6
- package/scripts/lib/ci-status-banner.mjs +29 -6
- package/scripts/lib/claude-md-budget-lint.mjs +52 -2
- package/scripts/lib/config/issue-budget.mjs +68 -8
- package/scripts/lib/config/private-config-dir.mjs +3 -2
- package/scripts/lib/config/remote-hosts.mjs +2 -2
- package/scripts/lib/config-schema.mjs +79 -0
- package/scripts/lib/config.mjs +12 -1
- package/scripts/lib/eval/engine.mjs +7 -1
- package/scripts/lib/file-lock.mjs +151 -8
- package/scripts/lib/git-porcelain.mjs +113 -0
- package/scripts/lib/instruction-budget-guard.mjs +415 -47
- package/scripts/lib/io.mjs +29 -4
- package/scripts/lib/issue-budget-reconcile.mjs +392 -0
- package/scripts/lib/issue-budget.mjs +412 -9
- package/scripts/lib/learnings/evolve-telemetry.mjs +1 -2
- package/scripts/lib/learnings/sizing-subject.mjs +44 -0
- package/scripts/lib/locks/staging-fence-lock.mjs +19 -38
- package/scripts/lib/locks/state-md-lock.mjs +19 -41
- package/scripts/lib/maintenance-due-banner.mjs +450 -0
- package/scripts/lib/owner-config.example.yaml +29 -46
- package/scripts/lib/owner-yaml.mjs +14 -13
- package/scripts/lib/peer-cards/merger.mjs +143 -0
- package/scripts/lib/pre-dispatch-check.mjs +20 -14
- package/scripts/lib/project-hygiene.mjs +81 -30
- package/scripts/lib/quality-gate.mjs +27 -71
- package/scripts/lib/reconcile/engine.mjs +19 -1
- package/scripts/lib/reconcile/writer.mjs +278 -11
- package/scripts/lib/resource-probe/evaluate.mjs +19 -21
- package/scripts/lib/rules-sync.mjs +34 -4
- package/scripts/lib/scope-echo.mjs +346 -0
- package/scripts/lib/session-close-backfill.mjs +182 -40
- package/scripts/lib/session-end/phase-skip.mjs +85 -86
- package/scripts/lib/session-end/tail-runner.mjs +178 -0
- package/scripts/lib/session-lock.mjs +62 -2
- package/scripts/lib/session-record-repair.mjs +91 -0
- package/scripts/lib/session-schema/constants.mjs +6 -0
- package/scripts/lib/session-schema/filters.mjs +26 -1
- package/scripts/lib/session-schema/validator.mjs +20 -0
- package/scripts/lib/session-shape.mjs +558 -0
- package/scripts/lib/session-start-probes.mjs +429 -56
- package/scripts/lib/session-token-rollup.mjs +95 -10
- package/scripts/lib/state-md/frontmatter-mutators.mjs +22 -34
- package/scripts/lib/state-md.mjs +1 -0
- package/scripts/lib/subagents-schema.mjs +77 -9
- package/scripts/lib/telemetry/pricing.mjs +197 -0
- package/scripts/lib/telemetry/sync.mjs +50 -1
- package/scripts/lib/test-runner/artifact-paths.mjs +30 -5
- package/scripts/lib/test-runner/issue-reconcile.mjs +45 -8
- package/scripts/lib/tmux-layout/layouts.mjs +62 -4
- package/scripts/lib/ux-grill/collect.mjs +1163 -0
- package/scripts/lib/ux-grill/compare.mjs +285 -0
- package/scripts/lib/ux-grill/manifest.mjs +618 -0
- package/scripts/lib/ux-grill/measures.mjs +431 -0
- package/scripts/lib/ux-grill/paths.mjs +224 -0
- package/scripts/lib/ux-grill/pencil-coverage.mjs +284 -0
- package/scripts/lib/ux-grill/reconcile.mjs +344 -0
- package/scripts/lib/ux-grill/run-record.mjs +316 -0
- package/scripts/lib/ux-grill/schema.mjs +321 -0
- package/scripts/lib/validate/check-skill-script-paths.mjs +33 -10
- package/scripts/lib/validate/check-untracked-test-deps.mjs +33 -19
- package/scripts/lib/validate/check-unwired-features.mjs +56 -27
- package/scripts/lib/vault-mirror/process.mjs +2 -1
- package/scripts/lib/vault-status/board-lock.mjs +18 -0
- package/scripts/lib/vault-status/board-writer.mjs +8 -0
- package/scripts/lib/vault-status/narrative-mirror.mjs +4 -4
- package/scripts/lib/wave-resource-gate.mjs +23 -27
- package/scripts/lib/wave-sizing.mjs +10 -3
- package/scripts/materialize-wave-scope.mjs +68 -14
- package/scripts/mcp-server.sh +16 -1
- package/scripts/print-applicable-rules.mjs +7 -6
- package/scripts/print-learnings-index.mjs +3 -2
- package/scripts/release.mjs +7 -2
- package/scripts/session-shape.mjs +266 -0
- package/skills/_shared/config-reading.md +15 -9
- package/skills/_shared/private-capability-context.md +89 -0
- package/skills/bootstrap/SKILL.md +60 -209
- package/skills/bootstrap/_shared-template.md +99 -14
- package/skills/bootstrap/deep-template.md +36 -26
- package/skills/bootstrap/fast-template.md +44 -8
- package/skills/bootstrap/intensity-heuristic.md +10 -4
- package/skills/bootstrap/private-contract.md +119 -0
- package/skills/bootstrap/public-fallback.md +30 -18
- package/skills/bootstrap/references/bootstrap-ecosystem-health-flow.md +48 -0
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +37 -0
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +108 -0
- package/skills/bootstrap/references/bootstrap-rules-fetch-bridge.md +64 -0
- package/skills/bootstrap/standard-template.md +39 -24
- package/skills/claude-md-drift-check/SKILL.md +9 -2
- package/skills/claude-md-drift-check/checker.mjs +213 -21
- package/skills/discovery/SKILL.md +6 -173
- package/skills/discovery/probes/vault-staleness.mjs +35 -5
- package/skills/discovery/probes-docs.md +8 -4
- package/skills/discovery/probes-supply-chain.md +4 -2
- package/skills/discovery/probes-ui.md +8 -4
- package/skills/discovery/probes-vault.md +12 -4
- package/skills/discovery/references/discovery-interactive-triage.md +139 -0
- package/skills/discovery/references/discovery-triage-state.md +54 -0
- package/skills/docs-orchestrator/audience-mapping.md +1 -1
- package/skills/eval/rubric-v1.md +13 -0
- package/skills/evolve/SKILL.md +2 -458
- package/skills/evolve/references/evolve-analyze-mode.md +360 -0
- package/skills/evolve/references/evolve-dialectic-mode.md +139 -0
- package/skills/gitlab-ops/SKILL.md +3 -3
- package/skills/grill/SKILL.md +1 -1
- package/skills/memory-cleanup/SKILL.md +2 -2
- package/skills/plan/mode-new.md +9 -0
- package/skills/plan/mode-retro.md +4 -3
- package/skills/reconcile/SKILL.md +11 -1
- package/skills/session-end/SKILL.md +3 -2
- package/skills/session-end/drift-operations.md +20 -5
- package/skills/session-end/metrics-collection.md +1 -0
- package/skills/session-end/phase-3-2-docs-verification.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +27 -67
- package/skills/session-end/phase-3-7a-recommendations.md +2 -2
- package/skills/session-end/references/phase-2-quality-gate.md +3 -3
- package/skills/session-end/references/phase-3-documentation-updates.md +8 -6
- package/skills/session-end/references/phase-5-issue-cleanup.md +32 -1
- package/skills/session-end/session-metrics-write.md +33 -12
- package/skills/session-plan/SKILL.md +46 -180
- package/skills/session-plan/references/session-plan-task-classification.md +152 -0
- package/skills/session-plan/wave-template.md +8 -15
- package/skills/session-start/SKILL.md +41 -7
- package/skills/session-start/phase-2-5-docs-planning.md +1 -1
- package/skills/session-start/phase-8-5-express-path.md +12 -9
- package/skills/session-start/references/operations-contract.md +114 -0
- package/skills/session-start/references/phase-1-5-session-continuity.md +2 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +42 -24
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +3 -1
- package/skills/session-start/soul.md +2 -2
- package/skills/test-runner/SKILL.md +1 -1
- package/skills/test-runner/rubric-v1.md +2 -2
- package/skills/tmux-layout/SKILL.md +3 -1
- package/skills/ux-grill/SKILL.md +211 -0
- package/skills/ux-grill/rubric-v2.md +201 -0
- package/skills/ux-grill/soul.md +76 -0
- package/skills/wave-executor/SKILL.md +32 -127
- package/skills/wave-executor/circuit-breaker.md +3 -1
- package/skills/wave-executor/references/wave-executor-quality-gate.md +61 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +86 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +12 -2
- package/skills/wave-executor/references/wave-loop-review.md +19 -6
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +6 -2
- package/templates/_shared/ux-manifest.template.md +149 -0
- package/templates/nextjs-minimal/package.json +1 -1
- package/templates/node-minimal/package.json +1 -1
- package/scripts/lib/multi-provider-build/providers.mjs +0 -64
- package/scripts/lib/multi-provider-build/templating.mjs +0 -130
- package/scripts/lib/owner-config/coerce.mjs +0 -29
- package/scripts/lib/owner-config/constants.mjs +0 -21
- package/scripts/lib/owner-config/defaults.mjs +0 -50
- package/scripts/lib/owner-config/error.mjs +0 -19
- package/scripts/lib/owner-config/index.mjs +0 -13
- package/scripts/lib/owner-config/merge.mjs +0 -52
- package/scripts/lib/owner-config/validate.mjs +0 -259
- package/scripts/lib/owner-config-loader.mjs +0 -170
- package/scripts/lib/owner-config.mjs +0 -28
- package/scripts/lib/soul-resolve.mjs +0 -130
- package/scripts/lib/vault-mirror/render.mjs +0 -8
- package/templates/_shared/journey-manifest.md +0 -114
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
* Check: every `scripts/**.mjs` path cited in `skills/`, `commands/`,
|
|
4
4
|
* `agents/` and `docs/` either EXISTS or is annotated as deliberately absent
|
|
5
5
|
* (#1176). Extended (#1187) to also cite `scripts/**.sh` and `hooks/**.sh` —
|
|
6
|
-
* see "## Mode
|
|
6
|
+
* see "## Mode" below for why that
|
|
7
7
|
* half is advisory, not blocking. `docs/` joined `SCAN_DIRS` in #1208, after
|
|
8
8
|
* the 22 dead paths it carried at the time (9 `.mjs`, all ADR/reference
|
|
9
9
|
* prose) were annotated — see that section below for the census and why
|
|
10
10
|
* widening the scan root had to wait for the annotation pass, not precede it.
|
|
11
|
+
* #1241 adds blocking repo-rooted `.md` paths quoted as complete inline-code
|
|
12
|
+
* spans, with the same annotations and fence handling. Scan roots stay fixed.
|
|
11
13
|
*
|
|
12
14
|
* ## Why
|
|
13
15
|
*
|
|
@@ -50,7 +52,7 @@
|
|
|
50
52
|
* looking misplaced. A malformed marker (unknown class, or `planned` without a
|
|
51
53
|
* `#<iid>`) is itself a finding for the same reason — it must never fail silent.
|
|
52
54
|
*
|
|
53
|
-
* ## Mode: BLOCKING for `.mjs`, ADVISORY for `.sh` (#1187)
|
|
55
|
+
* ## Mode: BLOCKING for `.mjs` / `.md`, ADVISORY for `.sh` (#1187)
|
|
54
56
|
*
|
|
55
57
|
* Unlike `check-doc-cli-commands.mjs`, the oracle here is the repository's own
|
|
56
58
|
* filesystem, not a locally installed third-party binary — there is no version
|
|
@@ -115,6 +117,22 @@ export const SCAN_DIRS = Object.freeze(['skills', 'commands', 'agents', 'docs'])
|
|
|
115
117
|
*/
|
|
116
118
|
const CITATION_RE = /scripts\/[a-zA-Z0-9_/-]*\.(?:mjs|sh)|hooks\/[a-zA-Z0-9_/-]*\.sh/g;
|
|
117
119
|
|
|
120
|
+
/**
|
|
121
|
+
* A complete inline-code span, including spans delimited by multiple backticks.
|
|
122
|
+
* Consume the outer span before judging its content, so a quoted command that
|
|
123
|
+
* contains backticks cannot donate a nested path-looking substring.
|
|
124
|
+
*/
|
|
125
|
+
const INLINE_CODE_RE = /(?<!`)(`+)(.+?)(?<!`)\1(?!`)/g;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* #1241's bounded grammar: a literal repo-rooted .md path, optionally followed
|
|
129
|
+
* by :line, :line:end / :line-end, and #anchor. Only the file is checked.
|
|
130
|
+
* URLs, commands, absolute paths, skill-relative references/, variables and
|
|
131
|
+
* globs are outside this grammar. Matching is line-local; revisit if the
|
|
132
|
+
* instruction corpus adopts multiline path spans or another target root.
|
|
133
|
+
*/
|
|
134
|
+
const MARKDOWN_CITATION_RE = /^((?:docs|skills|commands|agents|templates|rules|scripts|hooks|tests|\.(?:claude|codex|cursor|pi|orchestrator|gitlab|github))\/[a-zA-Z0-9_./-]+\.md)(?::\d+(?:[:-]\d+)?)?(?:#[^\s`]+)?$/;
|
|
135
|
+
|
|
118
136
|
/**
|
|
119
137
|
* Filename fragments that mark a citation as an ILLUSTRATIVE placeholder —
|
|
120
138
|
* `scripts/example.sh`, `hooks/my-hook.sh`, `scripts/<name>.sh` — rather than
|
|
@@ -211,6 +229,10 @@ export function extractCitations(lines) {
|
|
|
211
229
|
for (const hit of raw.matchAll(CITATION_RE)) {
|
|
212
230
|
citations.push({ line: lineNumber, path: hit[0] });
|
|
213
231
|
}
|
|
232
|
+
for (const span of raw.matchAll(INLINE_CODE_RE)) {
|
|
233
|
+
const hit = span[2].match(MARKDOWN_CITATION_RE);
|
|
234
|
+
if (hit) citations.push({ line: lineNumber, path: hit[1] });
|
|
235
|
+
}
|
|
214
236
|
};
|
|
215
237
|
|
|
216
238
|
// A blockquoted fence is still a fence — the shared tracker strips the `>`
|
|
@@ -234,12 +256,12 @@ export function extractCitations(lines) {
|
|
|
234
256
|
|
|
235
257
|
/**
|
|
236
258
|
* Census the documentation corpus for dead `scripts/**.mjs`/`.sh` and
|
|
237
|
-
* `hooks/**.sh` citations.
|
|
259
|
+
* `hooks/**.sh` citations, plus repo-rooted `.md` inline-code paths.
|
|
238
260
|
*
|
|
239
261
|
* @param {{pluginRoot: string, dirs?: string[], strictSh?: boolean}} options
|
|
240
262
|
* `strictSh` (default `false`) promotes a dead `.sh` citation from
|
|
241
263
|
* `severity: 'warn'` to `severity: 'fail'` — see the module docblock
|
|
242
|
-
* "Mode
|
|
264
|
+
* "Mode" for why the default stays
|
|
243
265
|
* advisory in this release.
|
|
244
266
|
* @returns {{ok: boolean, summary: object, findings: {kind: string, file: string, line: number, path: string, annotation: string | null, message: string, severity: 'fail' | 'warn'}[], toolError: boolean}}
|
|
245
267
|
*/
|
|
@@ -344,10 +366,11 @@ export function scanSkillScriptPaths({ pluginRoot, dirs = SCAN_DIRS, strictSh =
|
|
|
344
366
|
summary.existing += 1;
|
|
345
367
|
continue;
|
|
346
368
|
}
|
|
347
|
-
//
|
|
369
|
+
// A script placeholder name needs no marker — see
|
|
348
370
|
// `isPlaceholderCitation`'s docblock for the closed fragment list and
|
|
349
|
-
// its named ceiling.
|
|
350
|
-
|
|
371
|
+
// its named ceiling. Markdown examples require explicit annotations:
|
|
372
|
+
// a real missing docs/foobar.md must not disappear because of its name.
|
|
373
|
+
if (path.extname(citation.path) !== '.md' && isPlaceholderCitation(citation.path)) {
|
|
351
374
|
summary.placeholders += 1;
|
|
352
375
|
continue;
|
|
353
376
|
}
|
|
@@ -366,7 +389,7 @@ export function scanSkillScriptPaths({ pluginRoot, dirs = SCAN_DIRS, strictSh =
|
|
|
366
389
|
}
|
|
367
390
|
if (marker && !marker.ok) continue; // already reported as bad-annotation
|
|
368
391
|
|
|
369
|
-
// `.mjs`
|
|
392
|
+
// `.mjs` and `.md` are blocking.
|
|
370
393
|
// `.sh` is advisory (`warn`) unless the caller opted into `strictSh`.
|
|
371
394
|
const isSh = path.extname(citation.path) === '.sh';
|
|
372
395
|
const severity = isSh && !strictSh ? 'warn' : 'fail';
|
|
@@ -405,7 +428,7 @@ export function scanSkillScriptPaths({ pluginRoot, dirs = SCAN_DIRS, strictSh =
|
|
|
405
428
|
* @returns {number} 0 = clean, 1 = findings, 2 = tool error
|
|
406
429
|
*/
|
|
407
430
|
export function runCheckSkillScriptPaths(pluginRoot, { strictSh = false } = {}) {
|
|
408
|
-
console.log('--- Check:
|
|
431
|
+
console.log('--- Check: script and Markdown paths cited in skills/commands/agents/docs exist ---');
|
|
409
432
|
const inspection = scanSkillScriptPaths({ pluginRoot, strictSh });
|
|
410
433
|
|
|
411
434
|
for (const item of inspection.findings) {
|
|
@@ -427,7 +450,7 @@ export function runCheckSkillScriptPaths(pluginRoot, { strictSh = false } = {})
|
|
|
427
450
|
const blockingCount = inspection.findings.filter((f) => f.severity !== 'warn').length;
|
|
428
451
|
if (inspection.ok) {
|
|
429
452
|
console.log(
|
|
430
|
-
` PASS: ${s.citations}
|
|
453
|
+
` PASS: ${s.citations} file citation(s) in ${s.filesScanned} doc file(s) — ` +
|
|
431
454
|
`${s.existing} exist, ${s.annotated} annotated as deliberately absent, ` +
|
|
432
455
|
`${s.placeholders} placeholder(s)` +
|
|
433
456
|
(s.warnings > 0 ? `, ${s.warnings} advisory .sh warning(s) (see --strict-sh)` : ''),
|
|
@@ -629,28 +629,42 @@ function importClosure(repoRoot, entry, readSource) {
|
|
|
629
629
|
*/
|
|
630
630
|
function checkIgnoreBatch(repoRoot, specs, out) {
|
|
631
631
|
if (specs.length === 0) return;
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
632
|
+
let pending = specs;
|
|
633
|
+
for (;;) {
|
|
634
|
+
const ci = spawnSync('git', ['check-ignore', '--stdin'], {
|
|
635
|
+
cwd: repoRoot, input: pending.join('\n'), encoding: 'utf8',
|
|
636
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
637
|
+
});
|
|
638
|
+
// 0 = at least one ignored (listed on stdout); 1 = none ignored; else fatal.
|
|
639
|
+
if (ci.status === 0) {
|
|
640
|
+
for (const line of (ci.stdout || '').split('\n')) {
|
|
641
|
+
const p = line.trim();
|
|
642
|
+
if (p) out.add(p);
|
|
643
|
+
}
|
|
644
|
+
return;
|
|
641
645
|
}
|
|
646
|
+
if (ci.status === 1) return;
|
|
647
|
+
// Git identifies this refused candidate precisely. Retrying only the rest
|
|
648
|
+
// preserves the old dropped-candidate semantics without bisecting healthy
|
|
649
|
+
// paths (78 subprocesses for five symlink descendants in the tracked tree).
|
|
650
|
+
const rejected = /^fatal: pathspec '([^'\n]+)' is beyond a symbolic link\r?\n?$/.exec(ci.stderr || '')?.[1];
|
|
651
|
+
if (rejected && pending.includes(rejected)) {
|
|
652
|
+
process.stderr.write(` WARN: git check-ignore rejected "${rejected}" — candidate dropped (${(ci.stderr || '').trim()})\n`);
|
|
653
|
+
pending = pending.filter((spec) => spec !== rejected);
|
|
654
|
+
if (pending.length === 0) return;
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
if (pending.length === 1) {
|
|
658
|
+
process.stderr.write(
|
|
659
|
+
` WARN: git check-ignore rejected "${pending[0]}" — candidate dropped (${(ci.stderr || '').trim().split('\n')[0]})\n`,
|
|
660
|
+
);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const mid = pending.length >> 1;
|
|
664
|
+
checkIgnoreBatch(repoRoot, pending.slice(0, mid), out);
|
|
665
|
+
checkIgnoreBatch(repoRoot, pending.slice(mid), out);
|
|
642
666
|
return;
|
|
643
667
|
}
|
|
644
|
-
if (ci.status === 1) return;
|
|
645
|
-
if (specs.length === 1) {
|
|
646
|
-
process.stderr.write(
|
|
647
|
-
` WARN: git check-ignore rejected "${specs[0]}" — candidate dropped (${(ci.stderr || '').trim().split('\n')[0]})\n`,
|
|
648
|
-
);
|
|
649
|
-
return;
|
|
650
|
-
}
|
|
651
|
-
const mid = specs.length >> 1;
|
|
652
|
-
checkIgnoreBatch(repoRoot, specs.slice(0, mid), out);
|
|
653
|
-
checkIgnoreBatch(repoRoot, specs.slice(mid), out);
|
|
654
668
|
}
|
|
655
669
|
|
|
656
670
|
/**
|
|
@@ -114,8 +114,9 @@
|
|
|
114
114
|
* `CHANGELOG.md` is excluded from the prose corpus for the same reason: it is an
|
|
115
115
|
* append-only record of what a PAST release shipped, so it names the symbols of
|
|
116
116
|
* code that may since have died. Counting it silenced a true positive
|
|
117
|
-
* (`soul-resolve.mjs
|
|
118
|
-
*
|
|
117
|
+
* (`soul-resolve.mjs` — since DELETED as dead: its only live claim was in
|
|
118
|
+
* `.claude/rules/owner-persona.md`, while its symbols appeared in a 2026-06
|
|
119
|
+
* changelog entry).
|
|
119
120
|
*
|
|
120
121
|
* ## S4 `unreachable-library-module` — the question the machine can answer
|
|
121
122
|
*
|
|
@@ -777,11 +778,11 @@ function mentionedModuleTokens(lines) {
|
|
|
777
778
|
* ## Cluster roots — one defect, one line
|
|
778
779
|
*
|
|
779
780
|
* Only the ROOT of each unreachable cluster is reported: a module no OTHER
|
|
780
|
-
* unreachable module references.
|
|
781
|
-
*
|
|
782
|
-
*
|
|
783
|
-
* disappear together. Measured on the live tree: 71 unreachable modules
|
|
784
|
-
* to 50 roots. This is category separation in the sense of
|
|
781
|
+
* unreachable module references. A drag-cluster looks like this: an unimported
|
|
782
|
+
* top-level module whose own subtree of interior files goes down with it —
|
|
783
|
+
* reporting the interior files too would multiply one deletion into N findings
|
|
784
|
+
* that all disappear together. Measured on the live tree: 71 unreachable modules
|
|
785
|
+
* collapse to 50 roots. This is category separation in the sense of
|
|
785
786
|
* `.claude/rules/development.md` § Guard & Threshold Design — a structural split,
|
|
786
787
|
* never a raised threshold.
|
|
787
788
|
*
|
|
@@ -794,9 +795,18 @@ function mentionedModuleTokens(lines) {
|
|
|
794
795
|
* switched off. Revisit if a real module-resolver (import-specifier resolution
|
|
795
796
|
* relative to the importing file) becomes cheap, or if a collided basename is
|
|
796
797
|
* ever confirmed to mask a true positive. The `coordinator-invoked-module`
|
|
797
|
-
* DOWNGRADE is exempt
|
|
798
|
-
* `dirname/base` suffix, because
|
|
799
|
-
* reportable class and would otherwise
|
|
798
|
+
* DOWNGRADE is exempt, and since #1293 so is the CLUSTER-ROOT filter: in both
|
|
799
|
+
* a colliding basename must be named with its `dirname/base` suffix, because
|
|
800
|
+
* those matches move a module OUT of the reportable class and would otherwise
|
|
801
|
+
* hide a true unreachable sibling. Measured cost of leaving the root filter
|
|
802
|
+
* on bare basenames: `locks/index.mjs` and `worktree/index.mjs` were both
|
|
803
|
+
* suppressed by a third unreachable module that merely mentioned bare
|
|
804
|
+
* `index.mjs`, and only resurfaced when that module was deleted for an
|
|
805
|
+
* unrelated reason. Remaining ceiling: two ambiguous roots whose mentioning
|
|
806
|
+
* module ALSO carries the qualified form (e.g. an unreachable module that
|
|
807
|
+
* literally writes `locks/index.mjs`) are still suppressed — correct when it
|
|
808
|
+
* is a real reference, a mask when it is prose. Revisit if a qualified
|
|
809
|
+
* mention is ever confirmed to hide a root.
|
|
800
810
|
* - **Reachable ≠ executed.** A module imported by a hook that never takes that
|
|
801
811
|
* branch reads as wired here. Proving execution needs coverage data, not a graph.
|
|
802
812
|
* - **Reachable from SOME entrypoint is not reachable from the PROMISED one.**
|
|
@@ -839,6 +849,10 @@ export function collectUnreachableLibraryModules(pluginRoot) {
|
|
|
839
849
|
// module an operator flagged. Measured 2026-08-28 on the first S4
|
|
840
850
|
// allowlist entry: 52 → 51 unreachable modules plus one bogus stale line.
|
|
841
851
|
mentions: relative === SELF_REL ? new Set() : mentionedModuleTokens(lines),
|
|
852
|
+
// Raw text, kept for the QUALIFIED (`dirname/base`) re-check in the
|
|
853
|
+
// root filter below: `mentionedModuleTokens` strips the directory, so
|
|
854
|
+
// a colliding basename can only be disambiguated against the body.
|
|
855
|
+
rawBody: relative === SELF_REL ? '' : body,
|
|
842
856
|
};
|
|
843
857
|
});
|
|
844
858
|
|
|
@@ -876,10 +890,38 @@ export function collectUnreachableLibraryModules(pluginRoot) {
|
|
|
876
890
|
(module) => !reachable.has(module.relative) && !module.entrypoint && module.exports.length > 0,
|
|
877
891
|
);
|
|
878
892
|
const unreachableSet = new Set(unreachable.map((module) => module.relative));
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
)
|
|
893
|
+
// Basename census, shared by the root filter below and the downgrade half
|
|
894
|
+
// further down. A bare basename is only a valid module reference when it is
|
|
895
|
+
// UNIQUE in the corpus: `writer.mjs` names both `peer-cards/writer.mjs` and
|
|
896
|
+
// `reconcile/writer.mjs` (measured 2026-09-07), so a doc naming ONE of them
|
|
897
|
+
// would otherwise downgrade BOTH out of the reportable class — a true
|
|
898
|
+
// unreachable silently moved into the advisory half. For a colliding basename
|
|
899
|
+
// the reference must therefore carry at least the `dirname/base` suffix
|
|
900
|
+
// (`reconcile/writer.mjs`); unique basenames keep the cheaper bare match.
|
|
901
|
+
// Direction matters in both consumers: this can only ever ADD findings back to
|
|
902
|
+
// the reportable class, never remove one.
|
|
903
|
+
/** @type {Map<string, number>} */
|
|
904
|
+
const basenameCount = new Map();
|
|
905
|
+
for (const module of modules) basenameCount.set(module.base, (basenameCount.get(module.base) ?? 0) + 1);
|
|
906
|
+
|
|
907
|
+
const roots = unreachable.filter((module) => {
|
|
908
|
+
// A bare-basename mention only suppresses when the basename is UNIQUE
|
|
909
|
+
// (see the census above). When it collides, the mentioning module must name
|
|
910
|
+
// the `dirname/base` form in its body — otherwise ONE unreachable module
|
|
911
|
+
// mentioning bare `index.mjs` masks EVERY differently-pathed `index.mjs`
|
|
912
|
+
// root at once (#1293: `locks/index.mjs` + `worktree/index.mjs` were masked
|
|
913
|
+
// by a third unreachable module until that module was deleted for an
|
|
914
|
+
// unrelated reason).
|
|
915
|
+
const ambiguous = (basenameCount.get(module.base) ?? 0) > 1;
|
|
916
|
+
const qualified = module.relative.split(path.sep).slice(-2).join('/');
|
|
917
|
+
const qualifiedRe = ambiguous ? tokenMatcher(qualified) : null;
|
|
918
|
+
return !unreachable.some(
|
|
919
|
+
(other) =>
|
|
920
|
+
other.relative !== module.relative &&
|
|
921
|
+
other.mentions.has(module.base) &&
|
|
922
|
+
(qualifiedRe === null || qualifiedRe.test(other.rawBody)),
|
|
923
|
+
);
|
|
924
|
+
});
|
|
883
925
|
|
|
884
926
|
// Category split (see § Category split in the doc block above): an INSTRUCTION
|
|
885
927
|
// document that names both the module AND one of its exported symbols is an
|
|
@@ -893,19 +935,6 @@ export function collectUnreachableLibraryModules(pluginRoot) {
|
|
|
893
935
|
.sort()
|
|
894
936
|
.map((file) => ({ relative: path.relative(pluginRoot, file), body: readFileSync(file, 'utf8') }));
|
|
895
937
|
|
|
896
|
-
// Basename census for the downgrade half. A bare basename is only a valid
|
|
897
|
-
// module reference when it is UNIQUE in the corpus: `writer.mjs` names both
|
|
898
|
-
// `peer-cards/writer.mjs` and `reconcile/writer.mjs` (measured 2026-09-07),
|
|
899
|
-
// so a doc naming ONE of them would otherwise downgrade BOTH out of the
|
|
900
|
-
// reportable class — a true unreachable silently moved into the advisory
|
|
901
|
-
// half. For a colliding basename the doc must therefore carry at least the
|
|
902
|
-
// `dirname/base` suffix (`reconcile/writer.mjs`); unique basenames keep the
|
|
903
|
-
// cheaper bare match. Direction matters: this can only ever ADD findings back
|
|
904
|
-
// to the reportable class, never remove one.
|
|
905
|
-
/** @type {Map<string, number>} */
|
|
906
|
-
const basenameCount = new Map();
|
|
907
|
-
for (const module of modules) basenameCount.set(module.base, (basenameCount.get(module.base) ?? 0) + 1);
|
|
908
|
-
|
|
909
938
|
let coordinatorInvoked = 0;
|
|
910
939
|
const findings = roots.map((module) => {
|
|
911
940
|
// Docs write POSIX separators regardless of host; `path.relative` does not.
|
|
@@ -879,7 +879,8 @@ export async function processSession(rawEntry, _lineNum, ctx) {
|
|
|
879
879
|
// This is deliberately REDUNDANT with the renderer's status mapping (#909,
|
|
880
880
|
// render-sessions.mjs) — see that module's header. The filter removes one
|
|
881
881
|
// status from the vault; the mapping keeps every OTHER status honest, and
|
|
882
|
-
// guards the generators' other entry point (
|
|
882
|
+
// guards the generators' other entry point (this module imports
|
|
883
|
+
// render-learnings.mjs and render-sessions.mjs directly — no barrel).
|
|
883
884
|
if (!isRealSession(entry)) {
|
|
884
885
|
return emitEntryAction(_lineNum, ctx, {
|
|
885
886
|
action: 'skipped-abandoned',
|
|
@@ -60,6 +60,15 @@ const DEFAULT_POLL_MS = 50;
|
|
|
60
60
|
* REVISIT when a board sweep is measured above 30 s (half the TTL — the point
|
|
61
61
|
* at which a slow host crosses it), or when an `onLockOutcome` carrying
|
|
62
62
|
* `staleOverride` is observed in the events ledger on a host that had no crash.
|
|
63
|
+
*
|
|
64
|
+
* What expiry does NOT do: protect the section it bounds — past 60 s the next
|
|
65
|
+
* writer takes over while this one may still run (see file-lock.mjs § Lease
|
|
66
|
+
* semantics). What IS guaranteed since #1285: the late writer's release can no
|
|
67
|
+
* longer delete the successor's lock, because release runs under the same
|
|
68
|
+
* `.acquire` guard as takeover. A `not-owner` release here therefore means THIS
|
|
69
|
+
* writer's lease expired mid-section — the lost-update case above, never a
|
|
70
|
+
* benign miss. Since #1336 that release result reaches callers through
|
|
71
|
+
* `onReleaseOutcome` (see withBoardLock).
|
|
63
72
|
*/
|
|
64
73
|
const DEFAULT_STALE_MS = 60_000;
|
|
65
74
|
|
|
@@ -105,6 +114,11 @@ export function boardLockPathFor(vaultDir) {
|
|
|
105
114
|
* is present only when this acquire OVERRODE an aged lock, and carries
|
|
106
115
|
* `file-lock.mjs`'s own reason token — the observable behind the
|
|
107
116
|
* DEFAULT_STALE_MS revisit trigger.
|
|
117
|
+
* @param {(release: { ok: boolean, reason?: string }) => void} [opts.onReleaseOutcome]
|
|
118
|
+
* — diagnostic sink for the release result, called at most once, AFTER `fn`
|
|
119
|
+
* (only on the locked path). `{ ok: false, reason: 'not-owner' }` means this
|
|
120
|
+
* writer's lease expired mid-section; `'busy'` means the release gave up on
|
|
121
|
+
* the `.acquire` guard and left the lock in place.
|
|
108
122
|
* @param {(lockPath: string, fn: Function, opts: object) => Promise<object>} [opts.lockImpl]
|
|
109
123
|
* — test seam; defaults to {@link withFileLock}. Must honour the same
|
|
110
124
|
* `{ ok: true, value } | { ok: false, reason }` contract.
|
|
@@ -123,6 +137,7 @@ export async function withBoardLock(vaultDir, fn, opts = {}) {
|
|
|
123
137
|
staleMs = DEFAULT_STALE_MS,
|
|
124
138
|
holder: holderOpt,
|
|
125
139
|
onLockOutcome,
|
|
140
|
+
onReleaseOutcome,
|
|
126
141
|
lockImpl = withFileLock,
|
|
127
142
|
warn = (msg) => process.stderr.write(msg),
|
|
128
143
|
} = opts;
|
|
@@ -167,6 +182,9 @@ export async function withBoardLock(vaultDir, fn, opts = {}) {
|
|
|
167
182
|
indent: 2,
|
|
168
183
|
tmpPrefix: '.board.lock',
|
|
169
184
|
warn: warnAndWatch,
|
|
185
|
+
// Separate sink, not a second onLockOutcome call — that one stays
|
|
186
|
+
// "exactly once, before fn".
|
|
187
|
+
...(typeof onReleaseOutcome === 'function' ? { onRelease: onReleaseOutcome } : {}),
|
|
170
188
|
},
|
|
171
189
|
);
|
|
172
190
|
|
|
@@ -1095,6 +1095,14 @@ async function mirrorBoardInner({ repoRoot, repos, explicitStatus, now = new Dat
|
|
|
1095
1095
|
waited_ms: Date.now() - acquireStartedAt,
|
|
1096
1096
|
};
|
|
1097
1097
|
},
|
|
1098
|
+
// #1336: the release runs in withFileLock's finally, i.e. before
|
|
1099
|
+
// withBoardLock returns — so this lands on the SAME board_written event,
|
|
1100
|
+
// which mirrorBoard emits only after this function returns.
|
|
1101
|
+
onReleaseOutcome: (r) => {
|
|
1102
|
+
if (lockOutcome && r && r.ok === false && typeof r.reason === 'string') {
|
|
1103
|
+
lockOutcome.release = r.reason;
|
|
1104
|
+
}
|
|
1105
|
+
},
|
|
1098
1106
|
});
|
|
1099
1107
|
|
|
1100
1108
|
return lockOutcome === undefined ? inner : { ...inner, lock: lockOutcome };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* narrative-mirror.mjs — Durable per-repo narrative mirror (Epic #673 Phase 1, #675).
|
|
3
3
|
*
|
|
4
|
-
* At session-end, extract from a repo's
|
|
4
|
+
* At session-end, extract from a repo's active `STATE.md` the DURABLE narrative —
|
|
5
5
|
* `## Wave History`, `## Deviations`, `## What Not To Retry`, plus the mission-status
|
|
6
6
|
* rollup — and idempotently mirror it into a generator-owned per-repo vault file, so a
|
|
7
7
|
* reviewer or stand-in can read PER REPO what was done, what failed, and what not to
|
|
@@ -31,7 +31,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
31
31
|
import path from 'node:path';
|
|
32
32
|
|
|
33
33
|
import { emitEvent, sessionAttribution } from '../events.mjs';
|
|
34
|
-
import { parseStateMd, parseMissionStatus } from '../state-md.mjs';
|
|
34
|
+
import { parseStateMd, parseMissionStatus, resolveStateMdPath } from '../state-md.mjs';
|
|
35
35
|
import {
|
|
36
36
|
parseFrontmatter,
|
|
37
37
|
toDate,
|
|
@@ -328,7 +328,7 @@ export function renderNarrative(opts) {
|
|
|
328
328
|
lines.push(`# ${repo ?? 'unknown'} — Session Narrative`);
|
|
329
329
|
lines.push('');
|
|
330
330
|
lines.push(
|
|
331
|
-
'> Durable per-repo narrative mirrored from
|
|
331
|
+
'> Durable per-repo narrative mirrored from the repo\'s active `STATE.md` (Epic #673). ' +
|
|
332
332
|
'What was done, what failed, and what not to retry — readable without opening the repo.',
|
|
333
333
|
);
|
|
334
334
|
lines.push('');
|
|
@@ -909,7 +909,7 @@ async function runNarrativeMirror(opts) {
|
|
|
909
909
|
}
|
|
910
910
|
|
|
911
911
|
// Read STATE.md (best-effort; absent STATE.md → nothing to mirror).
|
|
912
|
-
const stateMdPath =
|
|
912
|
+
const stateMdPath = resolveStateMdPath(repoRoot);
|
|
913
913
|
let stateContents;
|
|
914
914
|
try {
|
|
915
915
|
stateContents = await readFile(stateMdPath, 'utf8');
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { probe, evaluate } from './resource-probe.mjs';
|
|
11
|
+
import { resolveAgentCap } from './session-shape.mjs';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Wave roles that may run on a declared remote host, mapped to the
|
|
@@ -194,7 +195,10 @@ async function applyOffloadDecision(result, opts) {
|
|
|
194
195
|
// ceiling, which is a property of the REPO and holds wherever the wave runs.
|
|
195
196
|
// Restoring plannedAgents unconditionally here would let a heavy repo exceed
|
|
196
197
|
// its own cap by way of a remote host.
|
|
197
|
-
const cap =
|
|
198
|
+
const cap =
|
|
199
|
+
config?.['heavy-repo'] === true
|
|
200
|
+
? resolveAgentCap(config['agents-per-wave'], MODE_BLIND_SESSION_TYPE)
|
|
201
|
+
: null;
|
|
198
202
|
const agents = cap === null ? plannedAgents : Math.min(plannedAgents, cap);
|
|
199
203
|
|
|
200
204
|
return {
|
|
@@ -210,42 +214,34 @@ async function applyOffloadDecision(result, opts) {
|
|
|
210
214
|
}
|
|
211
215
|
|
|
212
216
|
/**
|
|
213
|
-
*
|
|
214
|
-
* `null` when no cap should apply.
|
|
217
|
+
* The session type this module resolves `agents-per-wave` FOR: none.
|
|
215
218
|
*
|
|
216
219
|
* `_coerceInteger()` (scripts/lib/config/coercers.mjs) parses the documented
|
|
217
220
|
* HR-003 parenthetical-override syntax — `agents-per-wave: 4 (deep: 18)` —
|
|
218
|
-
* into an OBJECT `{ default: 4, deep: 18 }`, not a plain number
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
* HR-004 exactly where it matters most (a heavy repo that also runs deep
|
|
222
|
-
* sessions).
|
|
221
|
+
* into an OBJECT `{ default: 4, deep: 18 }`, not a plain number, so the cap
|
|
222
|
+
* must be resolved rather than type-guarded (a bare `typeof cap !== 'number'`
|
|
223
|
+
* check no-ops HR-004 for every repo using the override syntax).
|
|
223
224
|
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
* `<default> (mode: <higher-ceiling>)`, i.e. `default` is the MORE
|
|
229
|
-
*
|
|
230
|
-
*
|
|
225
|
+
* This gate has NO session mode in scope — `waveRole` is a wave role (e.g.
|
|
226
|
+
* "Impl-Core"), never a session mode (e.g. "deep") — so it deliberately asks
|
|
227
|
+
* `resolveAgentCap` for no type and takes the `.default` fallback. That is the
|
|
228
|
+
* conservative reading: the HR-003 convention writes the override as
|
|
229
|
+
* `<default> (mode: <higher-ceiling>)`, i.e. `default` is the MORE restrictive
|
|
230
|
+
* of the pair. Under-applying a looser mode-specific ceiling is safe; passing a
|
|
231
|
+
* guessed `'deep'` here would silently RAISE a heavy repo's ceiling to the deep
|
|
232
|
+
* override, which is the bug this named constant exists to make un-writable.
|
|
231
233
|
*
|
|
232
|
-
* @
|
|
233
|
-
* @returns {number|null}
|
|
234
|
+
* @type {undefined}
|
|
234
235
|
*/
|
|
235
|
-
|
|
236
|
-
if (typeof cap === 'number') return Number.isFinite(cap) ? cap : null;
|
|
237
|
-
if (cap !== null && typeof cap === 'object' && !Array.isArray(cap)) {
|
|
238
|
-
const def = cap.default;
|
|
239
|
-
return typeof def === 'number' && Number.isFinite(def) ? def : null;
|
|
240
|
-
}
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
236
|
+
const MODE_BLIND_SESSION_TYPE = undefined;
|
|
243
237
|
|
|
244
238
|
/**
|
|
245
239
|
* HR-003/HR-004 heavy-repo preflight ceiling (baseline #60). A STATIC cap
|
|
246
240
|
* independent of the live resource-probe verdict: when `config['heavy-repo']`
|
|
247
241
|
* is `true`, `agents` is clamped to at most `config['agents-per-wave']`
|
|
248
|
-
* (resolved via
|
|
242
|
+
* (resolved via `resolveAgentCap` from `./session-shape.mjs` — the ONE exported
|
|
243
|
+
* copy of that resolver — to handle the object-override shape; see
|
|
244
|
+
* {@link MODE_BLIND_SESSION_TYPE} for why no session type is passed).
|
|
249
245
|
* More-restrictive-wins — this only ever LOWERS `agents`, never raises it
|
|
250
246
|
* above what the resource-driven rules already decided (e.g. a
|
|
251
247
|
* coordinator-direct 0 stays 0).
|
|
@@ -257,7 +253,7 @@ function resolveApwCap(cap) {
|
|
|
257
253
|
function applyHeavyRepoCap(result, opts) {
|
|
258
254
|
const { config } = opts;
|
|
259
255
|
if (!config || config['heavy-repo'] !== true) return result;
|
|
260
|
-
const cap =
|
|
256
|
+
const cap = resolveAgentCap(config['agents-per-wave'], MODE_BLIND_SESSION_TYPE);
|
|
261
257
|
if (cap === null) return result;
|
|
262
258
|
if (result.agents <= cap) return result; // already within the ceiling — never raise
|
|
263
259
|
return {
|
|
@@ -3,10 +3,17 @@
|
|
|
3
3
|
* Learning: coordinator-over-worktree-on-shared-files caused back-to-back regressions.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
//
|
|
6
|
+
// SSOT for the session-type enum. It used to be RE-DECLARED here while this very comment named
|
|
7
|
+
// constants.mjs as the source of truth — two literals that agreed only by luck (and already
|
|
8
|
+
// differed in member order). The direction of the dependency is forced: constants.mjs declares
|
|
9
|
+
// itself a leaf ("no imports from siblings or parent") and mentions wave-sizing.mjs only in prose,
|
|
10
|
+
// so this module imports, never the reverse; no cycle exists in either direction.
|
|
11
|
+
//
|
|
12
|
+
// 'unknown' is a member because scripts/lib/session-close-backfill.mjs writes it for records whose
|
|
7
13
|
// type it could not measure (4.0.0). It is NOT a mode anyone selects — isolation resolves as for
|
|
8
|
-
// 'deep' (the conservative end).
|
|
9
|
-
|
|
14
|
+
// 'deep' (the conservative end).
|
|
15
|
+
import { VALID_SESSION_TYPES } from './session-schema/constants.mjs';
|
|
16
|
+
|
|
10
17
|
const VALID_COLLISION_RISKS = ['low', 'medium', 'high'];
|
|
11
18
|
const VALID_CONFIG_ISOLATIONS = ['auto', 'worktree', 'none'];
|
|
12
19
|
const VALID_ENFORCEMENTS = ['strict', 'warn', 'off'];
|