session-orchestrator 5.2.0 → 5.3.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/architecture/SKILL.md +3 -1
- package/.agents/skills/autopilot/SKILL.md +5 -1
- package/.agents/skills/autopilot/agents/openai.yaml +5 -0
- package/.agents/skills/bootstrap/SKILL.md +5 -1
- package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
- package/.agents/skills/brainstorm/SKILL.md +5 -1
- package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
- package/.agents/skills/close/SKILL.md +5 -1
- package/.agents/skills/close/agents/openai.yaml +5 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
- package/.agents/skills/debug/SKILL.md +5 -1
- package/.agents/skills/debug/agents/openai.yaml +5 -0
- package/.agents/skills/discovery/SKILL.md +5 -1
- package/.agents/skills/discovery/agents/openai.yaml +5 -0
- package/.agents/skills/dispatcher/SKILL.md +5 -1
- package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ecosystem-health/SKILL.md +3 -1
- package/.agents/skills/eli5/SKILL.md +5 -1
- package/.agents/skills/eli5/agents/openai.yaml +5 -0
- package/.agents/skills/eval/SKILL.md +6 -2
- package/.agents/skills/eval/agents/openai.yaml +5 -0
- package/.agents/skills/evolve/SKILL.md +6 -2
- package/.agents/skills/evolve/agents/openai.yaml +5 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
- package/.agents/skills/gitlab-ops/SKILL.md +3 -1
- package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
- package/.agents/skills/go/SKILL.md +5 -1
- package/.agents/skills/go/agents/openai.yaml +5 -0
- package/.agents/skills/grill/SKILL.md +5 -1
- package/.agents/skills/grill/agents/openai.yaml +5 -0
- package/.agents/skills/harness-audit/SKILL.md +5 -1
- package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
- package/.agents/skills/hook-development/SKILL.md +3 -1
- package/.agents/skills/mcp-builder/SKILL.md +3 -1
- package/.agents/skills/memory-cleanup/SKILL.md +5 -1
- package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
- package/.agents/skills/mode-selector/SKILL.md +3 -1
- package/.agents/skills/npm-publish/SKILL.md +4 -2
- package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
- package/.agents/skills/persona-panel/SKILL.md +5 -1
- package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
- package/.agents/skills/plan/SKILL.md +5 -1
- package/.agents/skills/plan/agents/openai.yaml +5 -0
- package/.agents/skills/playwright-driver/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +5 -1
- package/.agents/skills/portfolio/agents/openai.yaml +5 -0
- package/.agents/skills/quality-gates/SKILL.md +3 -1
- package/.agents/skills/reconcile/SKILL.md +5 -1
- package/.agents/skills/reconcile/agents/openai.yaml +5 -0
- package/.agents/skills/release/SKILL.md +5 -1
- package/.agents/skills/release/agents/openai.yaml +5 -0
- package/.agents/skills/remote-offload/SKILL.md +3 -1
- package/.agents/skills/repo-audit/SKILL.md +5 -1
- package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
- package/.agents/skills/session/SKILL.md +21 -0
- package/.agents/skills/session/agents/openai.yaml +5 -0
- package/.agents/skills/session-end/SKILL.md +3 -1
- package/.agents/skills/session-plan/SKILL.md +3 -1
- package/.agents/skills/session-start/SKILL.md +3 -1
- package/.agents/skills/spinout/SKILL.md +5 -1
- package/.agents/skills/spinout/agents/openai.yaml +5 -0
- package/.agents/skills/sunset-review/SKILL.md +5 -1
- package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
- package/.agents/skills/templates-ack/SKILL.md +21 -0
- package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
- package/.agents/skills/test/SKILL.md +5 -1
- package/.agents/skills/test/agents/openai.yaml +5 -0
- package/.agents/skills/test-runner/SKILL.md +3 -1
- package/.agents/skills/tmux-layout/SKILL.md +3 -1
- package/.agents/skills/using-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ux-grill/SKILL.md +5 -1
- package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
- package/.agents/skills/vault-mirror/SKILL.md +3 -1
- package/.agents/skills/vault-sync/SKILL.md +3 -1
- package/.agents/skills/wave-executor/SKILL.md +3 -1
- package/.agents/skills/write-executable-plan/SKILL.md +3 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +4 -4
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
- package/.codex-plugin/skills/eval/SKILL.md +1 -1
- package/.codex-plugin/skills/evolve/SKILL.md +1 -1
- package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
- package/.codex-plugin/skills/session/SKILL.md +1 -1
- package/.cursor/commands/eval.md +1 -1
- package/.cursor/commands/session.md +1 -1
- package/.cursor/rules/000-session-orchestrator.mdc +0 -2
- package/.cursor/rules/050-plan.mdc +1 -1
- package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/npm-publish/SKILL.md +1 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +12 -3
- package/AGENTS.md +3 -2
- package/CHANGELOG.md +136 -0
- package/README.md +9 -9
- package/SECURITY.md +12 -0
- package/agents/dialectic-deriver.md +13 -10
- package/agents/eval-judge.md +67 -45
- package/agents/skill-applied-judge.md +34 -19
- package/commands/session.md +7 -3
- package/docs/baseline.md +12 -6
- package/docs/codex-setup.md +14 -2
- package/docs/components.md +7 -5
- package/docs/events-schema.md +56 -9
- package/docs/rule-authoring.md +58 -6
- package/docs/session-config-reference.md +100 -7
- package/docs/session-config-template.md +31 -2
- package/docs/telemetry.md +2 -0
- package/hooks/_lib/hook-import-set.json +85 -8
- package/hooks/_lib/subagent-transcript.mjs +582 -31
- package/hooks/config-protection.mjs +11 -3
- package/hooks/cwd-change-restore.mjs +11 -3
- package/hooks/enforce-commands.mjs +70 -23
- package/hooks/enforce-scope.mjs +143 -33
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/loop-guard.mjs +11 -3
- package/hooks/on-session-end.mjs +58 -23
- package/hooks/on-session-start.mjs +48 -11
- package/hooks/on-stop.mjs +168 -22
- package/hooks/operator-steer.mjs +11 -3
- package/hooks/post-bash-issue-budget-refund.mjs +18 -8
- package/hooks/post-bash-write-verify.mjs +3 -2
- package/hooks/post-edit-import-probe.mjs +17 -9
- package/hooks/post-edit-validate.mjs +13 -5
- package/hooks/post-subagent-discovery-validator.mjs +98 -13
- package/hooks/post-tool-batch-wave-signal.mjs +200 -38
- package/hooks/post-tool-failure-corrective-context.mjs +11 -5
- package/hooks/post-tooluse-frontend-slop.mjs +10 -4
- package/hooks/pre-auq-clarity.mjs +15 -2
- package/hooks/pre-bash-destructive-guard.mjs +80 -9
- package/hooks/pre-bash-issue-budget.mjs +16 -11
- package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
- package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
- package/hooks/pre-bash-staging-fence.mjs +335 -31
- package/hooks/pre-bash-templates-first.mjs +19 -14
- package/hooks/pre-task-scope-disjoint.mjs +233 -2
- package/hooks/subagent-telemetry.mjs +15 -19
- package/hooks/wave-scope-commit-guard.mjs +197 -100
- package/monitors/monitors.json +1 -1
- package/output-styles/wave-summary.md +1 -1
- package/package.json +1 -1
- package/pi/prompts/eval.md +1 -1
- package/pi/prompts/session.md +1 -1
- package/rules/README.md +1 -1
- package/rules/opt-in-domain/prompt-caching.md +1 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +3 -3
- package/rules/opt-in-stack/frontend.md +1 -1
- package/rules/opt-in-stack/security-web.md +3 -3
- package/rules/opt-in-stack/swift.md +1 -1
- package/scripts/autopilot.mjs +23 -2
- package/scripts/backfill-abandoned-sessions.mjs +117 -15
- package/scripts/check-sessions-integrity.mjs +300 -0
- package/scripts/dialectic-deriver.mjs +50 -13
- package/scripts/emit-session.mjs +75 -29
- package/scripts/eval-session.mjs +65 -3
- package/scripts/generate-agents-skills.mjs +102 -29
- package/scripts/generate-cursor-adapter.mjs +61 -16
- package/scripts/lib/agent-status.mjs +2 -31
- package/scripts/lib/auq/clarity.mjs +10 -2
- package/scripts/lib/auq/parse.mjs +12 -31
- package/scripts/lib/auq/schema.mjs +56 -41
- package/scripts/lib/auto-dialectic.mjs +304 -15
- package/scripts/lib/autopilot/flags.mjs +12 -1
- package/scripts/lib/autopilot/kill-switches.mjs +6 -3
- package/scripts/lib/autopilot/loop.mjs +14 -1
- package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
- package/scripts/lib/ci-status-banner.mjs +376 -16
- package/scripts/lib/command-blocker.mjs +275 -28
- package/scripts/lib/config/dialectic.mjs +12 -3
- package/scripts/lib/config/gate.mjs +74 -0
- package/scripts/lib/config/reaper.mjs +162 -0
- package/scripts/lib/config.mjs +14 -0
- package/scripts/lib/convergence-monitor.mjs +74 -11
- package/scripts/lib/ecosystem-health.mjs +11 -0
- package/scripts/lib/eval/engine.mjs +421 -53
- package/scripts/lib/eval/judge.mjs +463 -40
- package/scripts/lib/eval/schema.mjs +10 -1
- package/scripts/lib/events-rotation.mjs +221 -25
- package/scripts/lib/events-schema.mjs +114 -0
- package/scripts/lib/events.mjs +524 -5
- package/scripts/lib/frontmatter-guard.mjs +21 -10
- package/scripts/lib/gates/gate-baseline.mjs +27 -2
- package/scripts/lib/gates/gate-full.mjs +28 -3
- package/scripts/lib/gates/gate-helpers.mjs +243 -21
- package/scripts/lib/gates/gate-incremental.mjs +28 -3
- package/scripts/lib/gates/gate-per-file.mjs +27 -2
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
- package/scripts/lib/instruction-budget-guard.mjs +146 -4
- package/scripts/lib/io.mjs +42 -8
- package/scripts/lib/issue-close-strip-labels.mjs +207 -49
- package/scripts/lib/js-mask.mjs +197 -0
- package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
- package/scripts/lib/maintenance-due-banner.mjs +53 -88
- package/scripts/lib/orphan-reaper.mjs +1588 -0
- package/scripts/lib/peer-cards/merger.mjs +48 -10
- package/scripts/lib/peer-cards/reader.mjs +78 -2
- package/scripts/lib/process-group.mjs +899 -0
- package/scripts/lib/quality-gate.mjs +107 -28
- package/scripts/lib/reconcile/backlog.mjs +368 -0
- package/scripts/lib/reconcile/engine.mjs +55 -188
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
- package/scripts/lib/reconcile/sanitize.mjs +69 -3
- package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
- package/scripts/lib/resource-probe/parsers.mjs +31 -0
- package/scripts/lib/rule-loader.mjs +41 -12
- package/scripts/lib/scope-echo.mjs +39 -2
- package/scripts/lib/scope-gate.mjs +605 -1
- package/scripts/lib/session-close-backfill.mjs +33 -6
- package/scripts/lib/session-id.mjs +9 -20
- package/scripts/lib/session-invocation.mjs +20 -0
- package/scripts/lib/session-schema/constants.mjs +30 -2
- package/scripts/lib/session-schema/normalizer.mjs +56 -4
- package/scripts/lib/session-schema.mjs +8 -3
- package/scripts/lib/session-start-probes.mjs +95 -10
- package/scripts/lib/sessions-canonical.mjs +23 -0
- package/scripts/lib/sessions-integrity-banner.mjs +7 -1
- package/scripts/lib/sessions-staleness-banner.mjs +193 -51
- package/scripts/lib/skill-evidence-window.mjs +891 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
- package/scripts/lib/skill-evolution/engine.mjs +18 -9
- package/scripts/lib/skill-judge.mjs +45 -3
- package/scripts/lib/tail-window.mjs +56 -0
- package/scripts/lib/telemetry/schema.mjs +30 -0
- package/scripts/lib/telemetry/sync.mjs +61 -6
- package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
- package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
- package/scripts/lib/user-invocable-skills.mjs +23 -3
- package/scripts/lib/ux-grill/reconcile.mjs +48 -22
- package/scripts/lib/validate/check-agents-skills.mjs +26 -15
- package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
- package/scripts/lib/validate/check-entry-guard.mjs +13 -50
- package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
- package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
- package/scripts/lib/validate/check-rules.mjs +7 -5
- package/scripts/lib/validate/check-skill-links.mjs +9 -1
- package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
- package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
- package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
- package/scripts/lib/validate/check-unwired-features.mjs +130 -27
- package/scripts/lib/validate/check-validator-registration.mjs +34 -10
- package/scripts/lib/validate/confidential-names.mjs +10 -0
- package/scripts/lib/validate-vendored-rules.mjs +4 -3
- package/scripts/lib/vault-mirror/namespace.mjs +46 -8
- package/scripts/lib/vault-mirror/process.mjs +10 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
- package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
- package/scripts/lib/vault-yaml.mjs +118 -0
- package/scripts/lib/worktree/lifecycle.mjs +153 -1
- package/scripts/release-session-lock.mjs +305 -0
- package/scripts/release.mjs +30 -5
- package/scripts/resolve-session-invocation.mjs +59 -0
- package/scripts/run-quality-gate.mjs +156 -17
- package/scripts/sweep-expired-rules.mjs +14 -3
- package/scripts/validate-plugin.mjs +12 -0
- package/scripts/validate-wave-scope.mjs +32 -105
- package/scripts/vault-mirror.mjs +9 -1
- package/skills/_shared/platform-tools.md +23 -11
- package/skills/autopilot/SKILL.md +22 -7
- package/skills/claude-md-drift-check/SKILL.md +1 -1
- package/skills/convergence-monitoring/README.md +8 -1
- package/skills/convergence-monitoring/SIGNALS.md +50 -6
- package/skills/convergence-monitoring/SKILL.md +15 -6
- package/skills/eval/SKILL.md +39 -24
- package/skills/eval/rubric-v1.md +1 -0
- package/skills/eval/rubric-v2.md +457 -0
- package/skills/evolve/SKILL.md +1 -1
- package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
- package/skills/gitlab-ops/SKILL.md +3 -2
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +11 -0
- package/skills/session-end/SKILL.md +13 -16
- package/skills/session-end/discovery-scan.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +55 -9
- package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
- package/skills/session-end/session-metrics-write.md +10 -0
- package/skills/session-plan/SKILL.md +17 -5
- package/skills/session-plan/references/session-plan-task-classification.md +2 -2
- package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -1
- package/skills/ux-grill/SKILL.md +1 -1
- package/skills/wave-executor/SKILL.md +8 -4
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +5 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +2 -1
- package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +0 -5
- package/.codex-plugin/skills/npm-publish/agents/openai.yaml +0 -5
- package/.cursor/commands/convergence-monitoring.md +0 -13
- package/.cursor/commands/npm-publish.md +0 -13
- package/pi/prompts/convergence-monitoring.md +0 -11
- package/pi/prompts/npm-publish.md +0 -11
|
@@ -53,6 +53,30 @@
|
|
|
53
53
|
* a rule that only cites live instances loses its explanation the moment it works.
|
|
54
54
|
* If you need a CURRENT S2 hit, run the checker; do not assume this one.
|
|
55
55
|
*
|
|
56
|
+
* S6 `parser-only-config-key` — the key IS read, but the ONLY file that
|
|
57
|
+
* reads it is its own block parser
|
|
58
|
+
* `scripts/lib/config/<root>.mjs`.
|
|
59
|
+
*
|
|
60
|
+
* S6 is the mirror image of S2, and it exists because S1 counts the PARSER as a
|
|
61
|
+
* consumer: `CONSUMER_DIRS` is `scripts/` + `hooks/`, and every block parser
|
|
62
|
+
* lives under `scripts/lib/config/`. A key its parser resolves and nothing else
|
|
63
|
+
* reads is `compact-nudge` wearing a value object. Measured 2026-09-22 on this
|
|
64
|
+
* tree: `reaper.max-hook-latency-ms` and `reaper.false-alarm-window` both passed
|
|
65
|
+
* S1 on `scripts/lib/config/reaper.mjs` alone while `scripts/lib/orphan-reaper.mjs`
|
|
66
|
+
* used same-named HARDCODED defaults and never saw the parsed value.
|
|
67
|
+
*
|
|
68
|
+
* Why the signal is this narrow — one read file, and it is the key's OWN parser.
|
|
69
|
+
* The blunt rule ("the parser layer never counts") was measured first and
|
|
70
|
+
* REJECTED: it flips 102 of 263 declared keys to unwired, because the ordinary
|
|
71
|
+
* shape here is a consumer that reads the camelCase FIELD (`cfg.driftCheck.mode`)
|
|
72
|
+
* while this census matches the kebab TOKEN. 102 of 263 is the broken instrument
|
|
73
|
+
* `.claude/rules/host-resources.md` HR-101 forbids. The narrow rule reports 32 —
|
|
74
|
+
* a backlog, printed as an aggregate like S4 and enumerated under `--list`.
|
|
75
|
+
*
|
|
76
|
+
* S6 carries no camelCase fallback ON PURPOSE: adding one re-hides both reaper
|
|
77
|
+
* keys, whose camelCase names exist in `orphan-reaper.mjs` as that module's own
|
|
78
|
+
* defaults. A name is not a read.
|
|
79
|
+
*
|
|
56
80
|
* S2 applies to TOP-LEVEL keys only — a nested key reaches code through its
|
|
57
81
|
* parent — and its premise is structural: every Session Config key has to pass
|
|
58
82
|
* through the parser layer to become a value. Measured 2026-08-08: 84 of 89
|
|
@@ -393,6 +417,29 @@ const PARSER_PATHS = Object.freeze([
|
|
|
393
417
|
path.join('scripts', 'parse-config.mjs'),
|
|
394
418
|
]);
|
|
395
419
|
|
|
420
|
+
/**
|
|
421
|
+
* The per-block parser directory: `scripts/lib/config/<block>.mjs` turns ONE
|
|
422
|
+
* Session Config block's YAML into a value object and does nothing else.
|
|
423
|
+
*
|
|
424
|
+
* Signal S6 below is about this directory specifically, which is why it is not
|
|
425
|
+
* folded into {@link PARSER_PATHS}.
|
|
426
|
+
*/
|
|
427
|
+
const BLOCK_PARSER_DIR = path.join('scripts', 'lib', 'config');
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* The key's OWN block parser, by convention `scripts/lib/config/<root>.mjs`.
|
|
431
|
+
*
|
|
432
|
+
* Deliberately an exact name match and not "anything under the parser dir": a
|
|
433
|
+
* sibling block's parser reading the key is still a second file, and S6 only
|
|
434
|
+
* fires when there is exactly ONE reader and it is the key's own.
|
|
435
|
+
*
|
|
436
|
+
* @param {string} root top-level key of the block
|
|
437
|
+
* @returns {string} plugin-root-relative path
|
|
438
|
+
*/
|
|
439
|
+
function ownBlockParserPath(root) {
|
|
440
|
+
return path.join(BLOCK_PARSER_DIR, `${root}.mjs`);
|
|
441
|
+
}
|
|
442
|
+
|
|
396
443
|
/**
|
|
397
444
|
* Declared-but-unread keys accepted on purpose. Key = full dotted path (S1/S2)
|
|
398
445
|
* or module path relative to the plugin root (S4), value = REASON naming the
|
|
@@ -420,7 +467,8 @@ const ALLOWLIST = Object.freeze({
|
|
|
420
467
|
|
|
421
468
|
/**
|
|
422
469
|
* @typedef {{
|
|
423
|
-
* kind: 'unwired-config-key' | 'parser-orphan-config-key' | '
|
|
470
|
+
* kind: 'unwired-config-key' | 'parser-orphan-config-key' | 'parser-only-config-key'
|
|
471
|
+
* | 'allowlist-missing-reason'
|
|
424
472
|
* | 'allowlist-stale' | 'orphaned-prose-module' | 'unreachable-library-module'
|
|
425
473
|
* | 'coordinator-invoked-module' | 'hand-keyed-learning-subject'
|
|
426
474
|
* | 'tool-error',
|
|
@@ -883,7 +931,9 @@ function mentionedModuleTokens(lines) {
|
|
|
883
931
|
* switched off. Revisit if a real module-resolver (import-specifier resolution
|
|
884
932
|
* relative to the importing file) becomes cheap, or if a collided basename is
|
|
885
933
|
* ever confirmed to mask a true positive. The `coordinator-invoked-module`
|
|
886
|
-
* DOWNGRADE is exempt, and since #1293 so is the CLUSTER-ROOT filter
|
|
934
|
+
* DOWNGRADE is exempt, and since #1293 so is the CLUSTER-ROOT filter (since
|
|
935
|
+
* #1298 its predicate also drives the "drags N" count, which had collapsed
|
|
936
|
+
* two dragged `index.mjs` into one): in both
|
|
887
937
|
* a colliding basename must be named with its `dirname/base` suffix, because
|
|
888
938
|
* those matches move a module OUT of the reportable class and would otherwise
|
|
889
939
|
* hide a true unreachable sibling. Measured cost of leaving the root filter
|
|
@@ -1014,24 +1064,41 @@ export function collectUnreachableLibraryModules(pluginRoot) {
|
|
|
1014
1064
|
const basenameCount = new Map();
|
|
1015
1065
|
for (const module of modules) basenameCount.set(module.base, (basenameCount.get(module.base) ?? 0) + 1);
|
|
1016
1066
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1067
|
+
// One `dirname/base` matcher per unreachable module whose basename collides,
|
|
1068
|
+
// built once: `references` below runs per (mentioner, target) pair.
|
|
1069
|
+
/** @type {Map<string, RegExp>} */
|
|
1070
|
+
const qualifiedMatchers = new Map();
|
|
1071
|
+
for (const module of unreachable) {
|
|
1072
|
+
if ((basenameCount.get(module.base) ?? 0) < 2) continue;
|
|
1073
|
+
qualifiedMatchers.set(module.relative, tokenMatcher(module.relative.split(path.sep).slice(-2).join('/')));
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Does `mentioner` reference the unreachable module `target`? A bare-basename
|
|
1077
|
+
* mention only counts when the basename is UNIQUE (see the census above).
|
|
1078
|
+
* When it collides, the mentioning module must name the `dirname/base` form
|
|
1079
|
+
* in its body — otherwise ONE unreachable module mentioning bare `index.mjs`
|
|
1080
|
+
* references EVERY differently-pathed `index.mjs` at once (#1293:
|
|
1081
|
+
* `locks/index.mjs` + `worktree/index.mjs` were masked as roots by a third
|
|
1082
|
+
* unreachable module until that module was deleted for an unrelated reason).
|
|
1083
|
+
*
|
|
1084
|
+
* The ONE predicate for both consumers — the cluster-root filter and the drag
|
|
1085
|
+
* count of the finding message — so the two cannot disagree about who
|
|
1086
|
+
* references whom. They did (#1298): the drag count compared bare basenames,
|
|
1087
|
+
* so a root importing `a/index.mjs` AND `b/index.mjs` read "drags 1".
|
|
1088
|
+
*
|
|
1089
|
+
* @param {(typeof modules)[number]} mentioner
|
|
1090
|
+
* @param {(typeof modules)[number]} target an `unreachable` module
|
|
1091
|
+
* @returns {boolean}
|
|
1092
|
+
*/
|
|
1093
|
+
const references = (mentioner, target) => {
|
|
1094
|
+
if (!mentioner.mentions.has(target.base)) return false;
|
|
1095
|
+
const qualifiedRe = qualifiedMatchers.get(target.relative);
|
|
1096
|
+
return qualifiedRe === undefined || qualifiedRe.test(mentioner.rawBody);
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
const roots = unreachable.filter(
|
|
1100
|
+
(module) => !unreachable.some((other) => other.relative !== module.relative && references(other, module)),
|
|
1101
|
+
);
|
|
1035
1102
|
|
|
1036
1103
|
// A pure `export *` barrel enters the population above so it can HEAD its own
|
|
1037
1104
|
// cluster, but reporting it needs one more condition. S3 already exempts this
|
|
@@ -1106,9 +1173,10 @@ export function collectUnreachableLibraryModules(pluginRoot) {
|
|
|
1106
1173
|
'architecture. Re-check only if that instruction is ever removed',
|
|
1107
1174
|
});
|
|
1108
1175
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
)
|
|
1176
|
+
// Counted per MODULE through the root filter's own predicate, never per
|
|
1177
|
+
// basename token: `mentions` is a Set of basenames, so two dragged
|
|
1178
|
+
// `index.mjs` files would collapse into one (#1298).
|
|
1179
|
+
const dragged = unreachable.filter((other) => other.relative !== module.relative && references(module, other));
|
|
1112
1180
|
const tail = dragged.length > 0 ? `, and drags ${dragged.length} further unreachable module(s)` : '';
|
|
1113
1181
|
// A pure barrel reports its re-export surface; naming "0 symbol(s)" there
|
|
1114
1182
|
// would read as a checker bug rather than as the barrel it is.
|
|
@@ -1259,6 +1327,7 @@ export function inspectUnwiredFeatures(pluginRoot) {
|
|
|
1259
1327
|
orphanedModules: 0,
|
|
1260
1328
|
unreachableModules: 0,
|
|
1261
1329
|
coordinatorInvokedModules: 0,
|
|
1330
|
+
parserOnly: 0,
|
|
1262
1331
|
handKeyedSubjects: 0,
|
|
1263
1332
|
judgedSubjects: 0,
|
|
1264
1333
|
},
|
|
@@ -1318,7 +1387,7 @@ export function inspectUnwiredFeatures(pluginRoot) {
|
|
|
1318
1387
|
|
|
1319
1388
|
for (const key of [...declared.keys.keys()].sort()) {
|
|
1320
1389
|
const meta = /** @type {DeclaredKey} */ (declared.keys.get(key));
|
|
1321
|
-
const { code, comment } = countReadSites(meta, corpus);
|
|
1390
|
+
const { code, comment, files: readFiles } = countReadSites(meta, corpus);
|
|
1322
1391
|
|
|
1323
1392
|
/** @type {Finding | null} */
|
|
1324
1393
|
let issue = null;
|
|
@@ -1331,6 +1400,21 @@ export function inspectUnwiredFeatures(pluginRoot) {
|
|
|
1331
1400
|
`declared in ${meta.sources.join(' + ')} but no .mjs under ${CONSUMER_DIRS.join('/ or ')}/ ` +
|
|
1332
1401
|
`reads it${commentNote} — wire it, delete it, or allowlist it with a reason`,
|
|
1333
1402
|
};
|
|
1403
|
+
} else if (readFiles.length === 1 && readFiles[0] === ownBlockParserPath(meta.root)) {
|
|
1404
|
+
// S6 — the ONLY file that reads this key is the parser that produced it.
|
|
1405
|
+
// A parser is not a consumer: it turns YAML into a field nobody then
|
|
1406
|
+
// reads, which is `compact-nudge` wearing a value object. Measured
|
|
1407
|
+
// 2026-09-22: `reaper.max-hook-latency-ms` and `reaper.false-alarm-window`
|
|
1408
|
+
// both passed S1 on the strength of `scripts/lib/config/reaper.mjs`
|
|
1409
|
+
// alone, while `orphan-reaper.mjs` used its OWN hardcoded defaults of the
|
|
1410
|
+
// same name and never received the parsed value.
|
|
1411
|
+
issue = {
|
|
1412
|
+
kind: 'parser-only-config-key',
|
|
1413
|
+
key,
|
|
1414
|
+
message:
|
|
1415
|
+
`read only by its own block parser (${readFiles[0]}) — a parser is not a consumer; ` +
|
|
1416
|
+
'nothing outside the config layer reads the parsed value',
|
|
1417
|
+
};
|
|
1334
1418
|
} else if (key === meta.root && !tokenMatcher(key).test(parserBody)) {
|
|
1335
1419
|
issue = {
|
|
1336
1420
|
kind: 'parser-orphan-config-key',
|
|
@@ -1356,7 +1440,8 @@ export function inspectUnwiredFeatures(pluginRoot) {
|
|
|
1356
1440
|
continue;
|
|
1357
1441
|
}
|
|
1358
1442
|
|
|
1359
|
-
result.summary.
|
|
1443
|
+
if (issue.kind === 'parser-only-config-key') result.summary.parserOnly += 1;
|
|
1444
|
+
else result.summary.unwired += 1;
|
|
1360
1445
|
findings.push(issue);
|
|
1361
1446
|
}
|
|
1362
1447
|
|
|
@@ -1426,6 +1511,7 @@ export function runCheckUnwiredFeatures(pluginRoot, { list = false } = {}) {
|
|
|
1426
1511
|
consumerFiles,
|
|
1427
1512
|
unwired,
|
|
1428
1513
|
allowlisted,
|
|
1514
|
+
parserOnly,
|
|
1429
1515
|
orphanedModules,
|
|
1430
1516
|
unreachableModules,
|
|
1431
1517
|
coordinatorInvokedModules,
|
|
@@ -1445,8 +1531,16 @@ export function runCheckUnwiredFeatures(pluginRoot, { list = false } = {}) {
|
|
|
1445
1531
|
// architecture — an instruction surface tells an LLM to call the module.
|
|
1446
1532
|
// Measured 2026-09-07: 46 of the 52 findings the single S4 class carried.
|
|
1447
1533
|
// Printing 46 WARN lines for the design is the broken instrument HR-101 forbids.
|
|
1448
|
-
|
|
1534
|
+
//
|
|
1535
|
+
// `parser-only-config-key` (S6) is deferred on the S4 terms too: 32 findings
|
|
1536
|
+
// on the live tree is a backlog to work down, not a per-run alarm.
|
|
1537
|
+
const DEFERRED = Object.freeze([
|
|
1538
|
+
'unreachable-library-module',
|
|
1539
|
+
'coordinator-invoked-module',
|
|
1540
|
+
'parser-only-config-key',
|
|
1541
|
+
]);
|
|
1449
1542
|
const s4 = inspection.findings.filter((item) => item.kind === 'unreachable-library-module');
|
|
1543
|
+
const s6 = inspection.findings.filter((item) => item.kind === 'parser-only-config-key');
|
|
1450
1544
|
for (const item of inspection.findings) {
|
|
1451
1545
|
if (!list && DEFERRED.includes(item.kind)) continue;
|
|
1452
1546
|
console.log(` WARN: [${item.kind}] ${item.key} — ${item.message}`);
|
|
@@ -1465,10 +1559,19 @@ export function runCheckUnwiredFeatures(pluginRoot, { list = false } = {}) {
|
|
|
1465
1559
|
);
|
|
1466
1560
|
}
|
|
1467
1561
|
|
|
1562
|
+
if (!list && s6.length > 0) {
|
|
1563
|
+
console.log(
|
|
1564
|
+
` WARN: [parser-only-config-key] ${s6.length} declared key(s) whose only reader is their own ` +
|
|
1565
|
+
`block parser — e.g. ${s6.slice(0, 3).map((item) => item.key).join(', ')}. ` +
|
|
1566
|
+
'Re-run with --list for the full census.',
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1468
1570
|
console.log(
|
|
1469
1571
|
` PASS: censused ${declaredKeys} declared key(s) from ${inspection.sourcesScanned.join(' + ') || '(no source)'} ` +
|
|
1470
1572
|
`against ${consumerFiles} consumer file(s) — ${unwired} unwired, ${allowlisted} allowlisted, ` +
|
|
1471
|
-
`${
|
|
1573
|
+
`${parserOnly} parser-only, ${orphanedModules} prose-orphaned module(s), ` +
|
|
1574
|
+
`${unreachableModules} unreachable module(s), ` +
|
|
1472
1575
|
`${coordinatorInvokedModules} coordinator-invoked module(s), ${handKeyedSubjects} hand-keyed ` +
|
|
1473
1576
|
`learning subject(s) of ${judgedSubjects} judged`,
|
|
1474
1577
|
);
|
|
@@ -51,15 +51,21 @@
|
|
|
51
51
|
* skill body) and nowhere in the three RUN surfaces above still reports
|
|
52
52
|
* UNREGISTERED — being documented is not being run. REVISIT if a fourth run
|
|
53
53
|
* surface (a new CI job file, a different git hook) is ever added: extend
|
|
54
|
-
* `RUN_SURFACES`, do not special-case it here.
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
54
|
+
* `RUN_SURFACES`, do not special-case it here.
|
|
55
|
+
*
|
|
56
|
+
* THE "NEVER A FALSE UNREGISTERED" CLAIM WAS FALSE (#1388, measured
|
|
57
|
+
* 2026-09-18 @ 20a4cbff). This header used to argue that the hand-rolled
|
|
58
|
+
* stripper could only ever MISS a comment, never manufacture a false
|
|
59
|
+
* UNREGISTERED. It had no regex-literal branch, so
|
|
60
|
+
* `const re = /\/*$/;` in `scripts/validate-plugin.mjs` opened a BLOCK
|
|
61
|
+
* COMMENT at the regex's `/*` and swallowed every `runCheck(...)` line below
|
|
62
|
+
* it — precisely a false UNREGISTERED, for every checker registered after
|
|
63
|
+
* that point. The `.mjs` surface is therefore lexed by the shared
|
|
64
|
+
* `maskSource` (`scripts/lib/js-mask.mjs`), which recognises regex literals;
|
|
65
|
+
* only the `#`-comment shell/YAML surfaces (`.husky/pre-commit`,
|
|
66
|
+
* `.gitlab-ci.yml`) still use the small local walker, whose flat
|
|
67
|
+
* quote-tracking ceiling (no `\"` escape handling, no `${...}` walk) is
|
|
68
|
+
* real but bounded to those two files.
|
|
63
69
|
*
|
|
64
70
|
* Usage: check-validator-registration.mjs <repo-root>
|
|
65
71
|
* Output: ` PASS: …` / ` FAIL: …` lines (two leading spaces), then
|
|
@@ -74,6 +80,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
74
80
|
import path from 'node:path';
|
|
75
81
|
import { enumerateRepoFiles } from './enumerate-repo-files.mjs';
|
|
76
82
|
import { isMainModule } from '../is-main-module.mjs';
|
|
83
|
+
import { maskSource } from '../js-mask.mjs';
|
|
77
84
|
|
|
78
85
|
/** Marker line inside a checker's own header — declares deliberate CLI-only status. */
|
|
79
86
|
export const STANDALONE_MARKER = /^\s*\/\/\s*registration:\s*standalone\b(?:\s+(.*))?$/m;
|
|
@@ -113,11 +120,28 @@ function commentStyleForSurface(rel) {
|
|
|
113
120
|
* truncating the line early. See the header NAMED CEILING for what this
|
|
114
121
|
* quote-tracking deliberately does not attempt.
|
|
115
122
|
*
|
|
123
|
+
* The JS style (`//` + `/* *\/`) is delegated to the shared lexer
|
|
124
|
+
* {@link maskSource} in `keepLiterals` mode — it is the only one of the three
|
|
125
|
+
* RUN_SURFACES that can contain a REGEX LITERAL, and a stripper without a
|
|
126
|
+
* regex branch reads `/\/*$/` as a block-comment opener and swallows every
|
|
127
|
+
* registration below it (#1388, see the header). `maskSource` blanks comment
|
|
128
|
+
* bytes to SPACES rather than deleting them, which is strictly safer for the
|
|
129
|
+
* substring match below: deletion could splice two fragments into a basename
|
|
130
|
+
* that was never written. The `#` style stays local — `maskSource` models
|
|
131
|
+
* JavaScript, not sh/YAML.
|
|
132
|
+
*
|
|
116
133
|
* @param {string} text
|
|
117
134
|
* @param {{lineComment: string, blockComment: boolean, quoteChars: string[]}} style
|
|
118
|
-
* @returns {string}
|
|
135
|
+
* @returns {string} TWO comment models behind one signature, and they differ in
|
|
136
|
+
* shape: the JS style (`//` + `/* *\/`, delegated to {@link maskSource})
|
|
137
|
+
* returns a string of the SAME LENGTH as `text`, with comment bytes blanked to
|
|
138
|
+
* spaces; the `#` style (sh/YAML, the local walker) returns a SHORTER string,
|
|
139
|
+
* with the comment text deleted to end-of-line (the newline itself is kept).
|
|
140
|
+
* Only the substring match below is common to both — do not rely on offsets
|
|
141
|
+
* surviving the `#` style.
|
|
119
142
|
*/
|
|
120
143
|
export function stripComments(text, { lineComment, blockComment, quoteChars }) {
|
|
144
|
+
if (lineComment === '//' && blockComment) return maskSource(text, { keepLiterals: true });
|
|
121
145
|
let out = '';
|
|
122
146
|
let i = 0;
|
|
123
147
|
let inQuote = null;
|
|
@@ -177,6 +177,16 @@ export function inspectConfidentialNames({ namesPath, deps = {} } = {}) {
|
|
|
177
177
|
// Unconfigured → no list, no noise. This is the normal case for public repos
|
|
178
178
|
// and for any host that has not opted into confidential-name scanning.
|
|
179
179
|
if (typeof namesPath !== 'string' || namesPath.trim() === '') {
|
|
180
|
+
// UNREACHABLE from the one production caller today (#1273 P1):
|
|
181
|
+
// `check-owner-leakage.mjs` resolves `namesPath` via `resolveHostPath()`
|
|
182
|
+
// and returns its own early `disabledReason` on an empty/non-string path
|
|
183
|
+
// BEFORE ever calling `inspectConfidentialNames` — so in production this
|
|
184
|
+
// branch never runs. It is reachable only for a direct caller (or a test)
|
|
185
|
+
// that skips that pre-filter. Kept regardless: `status` is part of the
|
|
186
|
+
// documented public contract every deep importer may switch on, and
|
|
187
|
+
// 'unconfigured' is the correct answer for exactly this input shape.
|
|
188
|
+
// REVISIT TRIGGER (BV-004): the production caller stops pre-filtering an
|
|
189
|
+
// empty/non-string `namesPath` before calling `inspectConfidentialNames`.
|
|
180
190
|
return { status: 'unconfigured', names: [] };
|
|
181
191
|
}
|
|
182
192
|
|
|
@@ -617,8 +617,9 @@ export function validateRuleContent({ content, relPath, targetRoot = null, requi
|
|
|
617
617
|
}
|
|
618
618
|
|
|
619
619
|
/**
|
|
620
|
-
* Validates every `*.md` rule file under `dir`, recursively (skips dotfiles
|
|
621
|
-
* and `
|
|
620
|
+
* Validates every `*.md` rule file under `dir`, recursively (skips dotfiles,
|
|
621
|
+
* `_index.md` and `README.md` — neither is a vendored rule: `rules-sync.mjs`
|
|
622
|
+
* vendors only the entries listed in `_index.md`, #1164).
|
|
622
623
|
*
|
|
623
624
|
* When `pluginRoot` is given, every scanned file is additionally passed through
|
|
624
625
|
* `scanVendoringLeaks()` and the findings are collected into the additive
|
|
@@ -642,7 +643,7 @@ export function validateRulesDir({ dir, targetRoot = null, requireProvenance = f
|
|
|
642
643
|
const absPath = join(absDir, entry.name);
|
|
643
644
|
if (entry.isDirectory()) {
|
|
644
645
|
files.push(...collectRuleFiles(absPath));
|
|
645
|
-
} else if (entry.isFile() && entry.name.endsWith('.md') && entry.name !== '_index.md') {
|
|
646
|
+
} else if (entry.isFile() && entry.name.endsWith('.md') && entry.name !== '_index.md' && entry.name !== 'README.md') {
|
|
646
647
|
files.push(relative(dir, absPath).replace(/\\/g, '/'));
|
|
647
648
|
}
|
|
648
649
|
}
|
|
@@ -5,10 +5,18 @@
|
|
|
5
5
|
* vault writes under `40-learnings/<repoNs>/` and `50-sessions/<repoNs>/`.
|
|
6
6
|
*
|
|
7
7
|
* Contract:
|
|
8
|
-
* resolveRepoNamespace({ vaultName?,
|
|
8
|
+
* resolveRepoNamespace({ vaultName?, repoRoot? }) → string
|
|
9
9
|
*
|
|
10
|
-
* - Pure + deterministic (given the same
|
|
11
|
-
* and the same host-local pseudonym
|
|
10
|
+
* - Pure + deterministic (given the same vaultName, the same `.vault.yaml`
|
|
11
|
+
* under `repoRoot`, the same git remote, and the same host-local pseudonym
|
|
12
|
+
* map).
|
|
13
|
+
* - `repoRoot` (#1389) selects the directory whose `.vault.yaml` is read;
|
|
14
|
+
* it defaults to `process.cwd()`. Named limit: it scopes ONLY that lookup —
|
|
15
|
+
* the {@link deriveRepo} fallback still resolves from the process cwd and
|
|
16
|
+
* is cached per process, so a `repoRoot` that differs from the cwd AND has
|
|
17
|
+
* no declared slug falls back to the cwd's git identity. Every production
|
|
18
|
+
* caller passes its own cwd today; revisit if a caller ever mirrors a repo
|
|
19
|
+
* other than the one it runs in.
|
|
12
20
|
* - Returns a lowercase kebab slug safe for use as a filesystem path segment.
|
|
13
21
|
* - Host-local pseudonym mapping (Epic #725 D5): consulted ONLY at the redaction
|
|
14
22
|
* site (only when a segment is owner-leaky). If such a repo (raw or sanitised)
|
|
@@ -47,6 +55,7 @@ import { isOwnerLeakySegment } from '../../lib/validate/check-owner-leakage.mjs'
|
|
|
47
55
|
import { loadPseudonymMap } from './pseudonym-map.mjs';
|
|
48
56
|
import { loadHostPaths, resolveHostPath } from '../config/host-paths.mjs';
|
|
49
57
|
import { isQueryFailure, resolvePreferredRemote } from '../vcs-repo-spec.mjs';
|
|
58
|
+
import { readVaultSlug } from '../vault-yaml.mjs';
|
|
50
59
|
|
|
51
60
|
// ── Lazy pseudonym-map path resolution (Epic #725 D5) ────────────────────────
|
|
52
61
|
// The map path comes from env SO_NAMESPACE_MAP > owner.yaml paths.namespace-map-path
|
|
@@ -237,19 +246,48 @@ function lookupPseudonym(base, seg) {
|
|
|
237
246
|
*
|
|
238
247
|
* @param {object} [opts]
|
|
239
248
|
* @param {string|null} [opts.vaultName] - Optional override for the repo identifier.
|
|
240
|
-
* When non-empty and non-whitespace, used in place of the git-derived
|
|
241
|
-
* When absent, the
|
|
249
|
+
* When non-empty and non-whitespace, used in place of the declared/git-derived
|
|
250
|
+
* repo name. When absent, the base is the repo's declared `.vault.yaml`
|
|
251
|
+
* `metadata.slug` (#1131), else the git origin via deriveRepo().
|
|
252
|
+
* @param {string|null} [opts.repoRoot] - Directory whose `.vault.yaml` supplies
|
|
253
|
+
* the declared slug (#1389). `undefined`/`null` → `process.cwd()`. Does NOT
|
|
254
|
+
* redirect the deriveRepo() fallback (see the module header's named limit).
|
|
242
255
|
* @returns {string} A single kebab-slug path segment, e.g. 'session-orchestrator'.
|
|
243
256
|
* Special returns:
|
|
244
257
|
* - 'unknown-repo' — slug derivation produced an empty string.
|
|
245
258
|
* - 'redacted-repo' — the raw or slugified value matched an owner-leakage pattern
|
|
246
259
|
* (CP1 personal home path / CP6 private slug / CP10 personal name in Projects path).
|
|
247
260
|
*/
|
|
248
|
-
export function resolveRepoNamespace({ vaultName = null } = {}) {
|
|
249
|
-
// Choose the base identifier: explicit override
|
|
261
|
+
export function resolveRepoNamespace({ vaultName = null, repoRoot = null } = {}) {
|
|
262
|
+
// Choose the base identifier. Precedence: explicit `vaultName` override >
|
|
263
|
+
// the repo's declared `.vault.yaml` `metadata.slug` (#1131) > git-derived.
|
|
264
|
+
//
|
|
265
|
+
// WHY the slug sits HERE and not in deriveRepo(): deriveRepo() is the RAW
|
|
266
|
+
// repo IDENTITY (`org/repo`) and has consumers that want exactly that and
|
|
267
|
+
// nothing else — `vault-relocation-rules.mjs`, `relocate-vault-corpus.mjs`,
|
|
268
|
+
// `named-vault-resolver.mjs` (which documents the `org/repo-name` format).
|
|
269
|
+
// #1131 concerns the vault FOLDER namespace, which is what THIS function
|
|
270
|
+
// decides, so the lookup belongs at this candidate-selection point.
|
|
271
|
+
//
|
|
272
|
+
// Note what this placement does NOT buy: the `source-repo:` frontmatter is
|
|
273
|
+
// this function's output, not deriveRepo()'s (#732; process.mjs deliberately
|
|
274
|
+
// shares ONE value between the write path and the rendered field), so a repo
|
|
275
|
+
// whose declared slug differs from its derived name gets a new `source-repo`
|
|
276
|
+
// either way. That is inherent to moving the folder, not a side effect of
|
|
277
|
+
// where the lookup lives.
|
|
278
|
+
//
|
|
279
|
+
// It sits BELOW `vaultName` (an explicit per-call/operator override must stay
|
|
280
|
+
// honourable) and ABOVE derivation (a declared registration beats a guess from
|
|
281
|
+
// the remote URL or the checkout directory: the remote answers "where does
|
|
282
|
+
// this code live?", the slug answers "which vault project IS this?", and a
|
|
283
|
+
// host-side rename legitimately diverges the two). It sits ABOVE the leak
|
|
284
|
+
// guard below deliberately — a declared slug is operator data flowing into a
|
|
285
|
+
// written path and is checked by CP1/CP6/CP10 and the pseudonym map exactly
|
|
286
|
+
// like any git-derived identifier.
|
|
287
|
+
const declaredSlug = readVaultSlug(repoRoot ?? process.cwd());
|
|
250
288
|
const base = (vaultName && typeof vaultName === 'string' && vaultName.trim())
|
|
251
289
|
? vaultName.trim()
|
|
252
|
-
: deriveRepo();
|
|
290
|
+
: (declaredSlug ?? deriveRepo());
|
|
253
291
|
|
|
254
292
|
// Sanitise: collapse to last path segment, lowercase, strip non-[a-z0-9-].
|
|
255
293
|
const seg = subjectToSlug(base);
|
|
@@ -666,8 +666,12 @@ export async function processLearning(rawEntry, _lineNum, ctx) {
|
|
|
666
666
|
});
|
|
667
667
|
}
|
|
668
668
|
|
|
669
|
-
// #660: namespace new writes under a per-repo subdirectory.
|
|
670
|
-
|
|
669
|
+
// #660: namespace new writes under a per-repo subdirectory. #1389: `ctx.repoRoot`
|
|
670
|
+
// is optional — absent, resolveRepoNamespace falls back to process.cwd().
|
|
671
|
+
const repoNs = resolveRepoNamespace({
|
|
672
|
+
vaultName: ctx?.vaultName ?? null,
|
|
673
|
+
repoRoot: ctx?.repoRoot ?? null,
|
|
674
|
+
});
|
|
671
675
|
// #725 D2: thread the resolved repo namespace into the learning frontmatter as
|
|
672
676
|
// `source-repo` for cross-repo attribution. repoNs is already sanitised +
|
|
673
677
|
// leak-guarded by resolveRepoNamespace, so it is safe to interpolate as-is. The
|
|
@@ -898,7 +902,10 @@ export async function processSession(rawEntry, _lineNum, ctx) {
|
|
|
898
902
|
// owner-leaky repo's real name reached the vault through the session-note
|
|
899
903
|
// frontmatter even though the directory AND the learning `source-repo` field
|
|
900
904
|
// were already pseudonym-mapped/redacted (#732 leak-guard bypass).
|
|
901
|
-
const repoNs = resolveRepoNamespace({
|
|
905
|
+
const repoNs = resolveRepoNamespace({
|
|
906
|
+
vaultName: ctx?.vaultName ?? null,
|
|
907
|
+
repoRoot: ctx?.repoRoot ?? null,
|
|
908
|
+
});
|
|
902
909
|
|
|
903
910
|
// Quality gate (PRD F1.2): skip sessions whose rendered narrative is too short.
|
|
904
911
|
// Measure on the rendered markdown body so the check is schema-agnostic across
|
|
@@ -476,14 +476,24 @@ export function generateSessionNote(entry, options = {}) {
|
|
|
476
476
|
// #1276: lifecycle-only records omit every older count alias. Started
|
|
477
477
|
// measures participation; completed/planned-only counts retain their
|
|
478
478
|
// labels so a plan or a completion count never claims dispatch coverage.
|
|
479
|
-
|
|
480
|
-
|
|
479
|
+
// The `agents_*` triple is the same lifecycle measurement under the
|
|
480
|
+
// de-underscored producer spelling: `metrics-collection.md` documents
|
|
481
|
+
// `agent_count_{planned,started,completed}`, but the live ledger carries
|
|
482
|
+
// one session (`main-2026-09-18-session-1`, 5 waves, measured 2026-09-18
|
|
483
|
+
// over `.orchestrator/metrics/sessions.jsonl`) whose waves emit only
|
|
484
|
+
// `agents_planned` / `agents_started` / `agents_completed` — every one of
|
|
485
|
+
// its 15 counts rendered `?` beside a real number, exactly the #1074 bug
|
|
486
|
+
// class. Each alias sits at the precedence of its documented twin, so no
|
|
487
|
+
// cell that already rendered a value can change.
|
|
488
|
+
const completedAgents = waveCount(w.agent_count_completed) ?? waveCount(w.agents_completed);
|
|
489
|
+
const plannedAgents = waveCount(w.agent_count_planned) ?? waveCount(w.agents_planned);
|
|
481
490
|
const agentsCell =
|
|
482
491
|
waveCount(w.agent_count) ??
|
|
483
492
|
waveCount(w.agents) ??
|
|
484
493
|
waveCount(w.agents_dispatched) ??
|
|
485
494
|
waveCount(w.dispatched) ??
|
|
486
495
|
waveCount(w.agent_count_started) ??
|
|
496
|
+
waveCount(w.agents_started) ??
|
|
487
497
|
(completedAgents === undefined ? undefined : `${completedAgents} completed`) ??
|
|
488
498
|
(plannedAgents === undefined ? MISSING_CELL : `${plannedAgents} planned`);
|
|
489
499
|
const filesCell = waveCount(w.files_changed) ?? waveCount(w.files) ?? MISSING_CELL;
|
|
@@ -43,6 +43,7 @@ import { readConfigFile, parseSessionConfig } from '../config.mjs';
|
|
|
43
43
|
import { validatePathInsideProject } from '../path-utils.mjs';
|
|
44
44
|
import { createSecretValueMasker } from '../secret-masker.mjs';
|
|
45
45
|
import { expandTilde } from '../common.mjs';
|
|
46
|
+
import { readVaultSlug } from '../vault-yaml.mjs';
|
|
46
47
|
|
|
47
48
|
/** Frontmatter sentinel that identifies generator-owned narrative files. */
|
|
48
49
|
export const GENERATOR_MARKER = 'session-orchestrator-vault-status-narrative@1';
|
|
@@ -869,15 +870,29 @@ async function runNarrativeMirror(opts) {
|
|
|
869
870
|
|
|
870
871
|
// Defense-in-depth: when the caller omits (or passes an empty) `repo`, derive
|
|
871
872
|
// it from the operator-configured `vault-name` override (#660/#832) when set,
|
|
872
|
-
// else the
|
|
873
|
-
//
|
|
873
|
+
// else the `.vault.yaml` slug (#1131), else the repoRoot basename — never
|
|
874
|
+
// silently mis-file under 'unknown' (#675 review).
|
|
874
875
|
const vaultNameOverride =
|
|
875
876
|
typeof vaultIntegration['vault-name'] === 'string' && vaultIntegration['vault-name'].trim()
|
|
876
877
|
? vaultIntegration['vault-name'].trim()
|
|
877
878
|
: null;
|
|
878
|
-
const
|
|
879
|
+
const explicitName = (typeof repo === 'string' && repo.trim().length > 0)
|
|
879
880
|
? repo
|
|
880
|
-
: vaultNameOverride
|
|
881
|
+
: vaultNameOverride;
|
|
882
|
+
|
|
883
|
+
// #1131: with NO explicit override, the repo's own `.vault.yaml`
|
|
884
|
+
// `metadata.slug` — the CANONICAL vault registration — outranks the
|
|
885
|
+
// directory basename. Precedence: explicit `repo` opt > `vault-name` >
|
|
886
|
+
// `.vault.yaml` slug > basename.
|
|
887
|
+
//
|
|
888
|
+
// WHY the two overrides still win: both are things a CALLER/operator states
|
|
889
|
+
// for THIS call (`repo` is only ever passed to override the derived name —
|
|
890
|
+
// the sole production caller, `skills/session-end/session-metrics-write.md`,
|
|
891
|
+
// passes `repoRoot` alone), so honouring the file over them would make an
|
|
892
|
+
// explicit override unhonourable. The file wins over the basename, which is
|
|
893
|
+
// a guess nobody stated.
|
|
894
|
+
const vaultYamlSlug = explicitName ? null : readVaultSlug(repoRoot);
|
|
895
|
+
const repoName = explicitName ?? vaultYamlSlug ?? path.basename(path.resolve(repoRoot));
|
|
881
896
|
|
|
882
897
|
const rawVaultDir = vaultIntegration['vault-dir'];
|
|
883
898
|
if (!rawVaultDir || typeof rawVaultDir !== 'string') {
|
|
@@ -886,10 +901,19 @@ async function runNarrativeMirror(opts) {
|
|
|
886
901
|
|
|
887
902
|
const vaultDir = path.resolve(expandTilde(rawVaultDir));
|
|
888
903
|
const candidateSlug = subjectToSlug(repoName) || 'unknown';
|
|
889
|
-
//
|
|
890
|
-
//
|
|
904
|
+
// A declared `.vault.yaml` slug needs no healing — it IS the canonical folder
|
|
905
|
+
// name, so resolveLooseSlug is SKIPPED for it. Running the healer over it
|
|
906
|
+
// would re-open the bug this fixes: a vault that also carries an unhealed
|
|
907
|
+
// legacy folder (e.g. `foobarapp` beside the declared `foo-bar-app`) makes
|
|
908
|
+
// the loose match ambiguous, and ambiguity falls back to the candidate —
|
|
909
|
+
// which is how a duplicate pair, once created, keeps being written to.
|
|
910
|
+
//
|
|
911
|
+
// Otherwise: loose-match against existing 01-projects/ folders before minting
|
|
912
|
+
// a new slug (issue #829 Finding 3) — see resolveLooseSlug for the ambiguity
|
|
891
913
|
// rules. Falls through to `candidateSlug` unchanged on any read failure.
|
|
892
|
-
const repoSlug =
|
|
914
|
+
const repoSlug = vaultYamlSlug
|
|
915
|
+
? candidateSlug
|
|
916
|
+
: resolveLooseSlug(vaultDir, candidateSlug, { readdirSync: injectedFs?.readdirSync });
|
|
893
917
|
const outputPath = resolveNarrativePath(vaultDir, repoSlug);
|
|
894
918
|
|
|
895
919
|
// Defense-in-depth: ensure the resolved file stays inside the vault root.
|