session-orchestrator 5.3.0 → 5.5.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/claude-md-drift-check/SKILL.md +1 -1
- package/.agents/skills/test-audit/SKILL.md +21 -0
- package/.agents/skills/test-audit/agents/openai.yaml +5 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
- package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
- package/.cursor/commands/test-audit.md +14 -0
- package/.cursor/rules/060-evolve.mdc +28 -13
- package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.cursor/skills/test-audit/SKILL.md +12 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +1 -1
- package/AGENTS.md +9 -31
- package/CHANGELOG.md +162 -0
- package/README.md +14 -10
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +10 -1
- package/docs/ci-setup.md +154 -4
- package/docs/components.md +11 -11
- package/docs/events-schema.md +25 -19
- package/docs/install.md +14 -2
- package/docs/migration-v4.md +24 -17
- package/docs/persona-panel.md +5 -4
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +27 -12
- package/docs/scope-collision-guard.md +40 -16
- package/docs/session-config-reference.md +61 -12
- package/docs/session-config-template.md +11 -2
- package/docs/vault-docs-architecture.md +8 -4
- package/hooks/_lib/hook-import-set.json +42 -29
- package/hooks/_lib/lock-bootstrap.mjs +118 -26
- package/hooks/config-protection.mjs +1 -1
- package/hooks/enforce-commands.mjs +28 -13
- package/hooks/enforce-scope.mjs +201 -23
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/on-session-start.mjs +57 -2
- package/hooks/on-stop.mjs +70 -168
- package/hooks/post-bash-write-verify.mjs +98 -17
- package/hooks/post-edit-validate.mjs +15 -5
- package/hooks/post-tool-batch-wave-signal.mjs +32 -152
- package/hooks/pre-bash-memory-propose-audit.mjs +71 -10
- package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
- package/hooks/pre-bash-staging-fence.mjs +29 -8
- package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
- package/hooks/pre-task-scope-disjoint.mjs +1143 -279
- package/hooks/subagent-telemetry.mjs +96 -19
- package/hooks/wave-scope-commit-guard.mjs +168 -72
- package/package.json +2 -6
- package/pi/prompts/test-audit.md +12 -0
- package/rules/README.md +6 -1
- package/rules/_index.md +3 -3
- package/rules/always-on/bash-harness-pitfalls.md +7 -1
- package/rules/opt-in-domain/prompt-caching.md +5 -1
- package/rules/opt-in-stack/backend-data.md +9 -1
- package/rules/opt-in-stack/backend.md +7 -1
- package/rules/opt-in-stack/frontend.md +6 -1
- package/rules/opt-in-stack/security-web.md +7 -1
- package/rules/opt-in-stack/swift.md +5 -1
- package/scripts/apply-session-learnings.mjs +513 -0
- package/scripts/archive-closed-prds.mjs +40 -12
- package/scripts/autopilot.mjs +57 -54
- package/scripts/backfill-abandoned-sessions.mjs +81 -6
- package/scripts/backfill-learnings-from-vault.mjs +49 -22
- package/scripts/baseline-archetypes.mjs +1 -1
- package/scripts/check-package-manager.mjs +62 -5
- package/scripts/codex-install.mjs +3 -0
- package/scripts/cursor-install.mjs +47 -6
- package/scripts/emit-session.mjs +298 -2
- package/scripts/export-hw-learnings.mjs +40 -10
- package/scripts/fleet-checkin.mjs +170 -0
- package/scripts/lib/agent-frontmatter.mjs +3 -3
- package/scripts/lib/auto-dream.mjs +7 -6
- package/scripts/lib/autopilot/flags.mjs +6 -1
- package/scripts/lib/autopilot/loop.mjs +32 -13
- package/scripts/lib/autopilot/stall-sampler.mjs +13 -9
- package/scripts/lib/baseline-archetypes.mjs +41 -22
- package/scripts/lib/blocked-commands-policy.mjs +40 -0
- package/scripts/lib/common.mjs +41 -0
- package/scripts/lib/config/host-paths.mjs +79 -0
- package/scripts/lib/config/persona-gate-wave.mjs +3 -3
- package/scripts/lib/config/reaper.mjs +7 -6
- package/scripts/lib/config/vault-integration.mjs +151 -16
- package/scripts/lib/config-schema.mjs +13 -0
- package/scripts/lib/config.mjs +75 -10
- package/scripts/lib/eval/engine.mjs +13 -0
- package/scripts/lib/events-retention-banner.mjs +393 -0
- package/scripts/lib/events-rotation.mjs +21 -6
- package/scripts/lib/events.mjs +72 -5
- package/scripts/lib/fleet-protocol.mjs +288 -0
- package/scripts/lib/gates/gate-baseline.mjs +13 -3
- package/scripts/lib/gates/gate-full.mjs +7 -2
- package/scripts/lib/gates/gate-helpers.mjs +93 -2
- package/scripts/lib/gates/gate-incremental.mjs +23 -22
- package/scripts/lib/gates/gate-per-file.mjs +16 -7
- package/scripts/lib/hardening.mjs +1 -0
- package/scripts/lib/harness-audit/categories/category4.mjs +8 -6
- package/scripts/lib/instruction-budget-guard.mjs +77 -11
- package/scripts/lib/learnings/expiry-sweep.mjs +420 -22
- package/scripts/lib/learnings/io.mjs +274 -37
- package/scripts/lib/learnings/schema.mjs +163 -15
- package/scripts/lib/learnings/sizing-subject.mjs +2 -2
- package/scripts/lib/lock-reaper.mjs +38 -14
- package/scripts/lib/maintenance-due-banner.mjs +23 -8
- package/scripts/lib/memory-proposals/schema.mjs +11 -5
- package/scripts/lib/named-baseline-resolver.mjs +53 -28
- package/scripts/lib/named-vault-resolver.mjs +147 -5
- package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
- package/scripts/lib/orphan-reaper/no-follow-append.mjs +87 -0
- package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +323 -0
- package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
- package/scripts/lib/orphan-reaper/scan-throttle.mjs +144 -0
- package/scripts/lib/orphan-reaper/scan.mjs +510 -0
- package/scripts/lib/orphan-reaper/trigger.mjs +173 -0
- package/scripts/lib/orphan-reaper.mjs +47 -1315
- package/scripts/lib/owner-yaml.mjs +104 -6
- package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
- package/scripts/lib/platform.mjs +206 -2
- package/scripts/lib/plugin-update-banner.mjs +3 -1
- package/scripts/lib/process-group.mjs +134 -22
- package/scripts/lib/project-hygiene.mjs +21 -1
- package/scripts/lib/quality-gate.mjs +45 -5
- package/scripts/lib/resource-probe.mjs +1 -1
- package/scripts/lib/rule-loader.mjs +100 -62
- package/scripts/lib/rules-sync.mjs +27 -12
- package/scripts/lib/scope-baseline.mjs +119 -29
- package/scripts/lib/scope-echo.mjs +7 -0
- package/scripts/lib/scope-gate.mjs +194 -27
- package/scripts/lib/session-close-backfill.mjs +129 -4
- package/scripts/lib/session-end/tail-runner.mjs +10 -5
- package/scripts/lib/session-lock-shape.mjs +6 -3
- package/scripts/lib/session-lock.mjs +320 -67
- package/scripts/lib/session-record-repair.mjs +183 -26
- package/scripts/lib/session-registry.mjs +67 -17
- package/scripts/lib/session-schema/constants.mjs +30 -4
- package/scripts/lib/session-schema/validator.mjs +28 -1
- package/scripts/lib/session-start-probes.mjs +242 -68
- package/scripts/lib/session-token-rollup.mjs +252 -54
- package/scripts/lib/sessions-canonical.mjs +100 -9
- package/scripts/lib/sessions-staleness-banner.mjs +3 -1
- package/scripts/lib/sh-quote.mjs +29 -0
- package/scripts/lib/subagents-schema.mjs +30 -3
- package/scripts/lib/tail-window.mjs +27 -3
- package/scripts/lib/telemetry/pricing.mjs +121 -71
- package/scripts/lib/telemetry/sync.mjs +17 -0
- package/scripts/lib/tests-src-ratio.mjs +215 -23
- package/scripts/lib/tmux-layout/layouts.mjs +1 -11
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +12 -0
- package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
- package/scripts/lib/ux-grill/reconcile.mjs +1 -1
- package/scripts/lib/validate/check-agents.mjs +1 -1
- package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +388 -27
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate/check-unwired-features.mjs +1 -1
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +1 -1
- package/scripts/lib/validate-vendored-rules.mjs +206 -81
- package/scripts/lib/vault-mirror/auto-commit.mjs +5 -3
- package/scripts/lib/vault-mirror/namespace.mjs +47 -26
- package/scripts/lib/vault-mirror/process.mjs +350 -62
- package/scripts/lib/vault-mirror/pseudonym-map.mjs +14 -5
- package/scripts/lib/vault-mirror/render-learnings.mjs +66 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +28 -4
- package/scripts/lib/vault-mirror/telemetry.mjs +25 -11
- package/scripts/lib/vault-status/board-writer.mjs +53 -12
- package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
- package/scripts/lib/vcs-repo-spec.mjs +8 -0
- package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
- package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
- package/scripts/lib/wave-resource-gate.mjs +34 -8
- package/scripts/materialize-wave-scope.mjs +7 -4
- package/scripts/memory-propose.mjs +18 -5
- package/scripts/release-session-lock.mjs +2 -1
- package/scripts/release.mjs +11 -3
- package/scripts/repair-invalid-sessions.mjs +37 -4
- package/scripts/run-migrate-v2-cross-repo.mjs +171 -31
- package/scripts/run-quality-gate.mjs +331 -307
- package/scripts/self-update.mjs +229 -0
- package/scripts/site-numbers.mjs +163 -5
- package/scripts/sweep-expired-learnings.mjs +603 -20
- package/scripts/sweep-expired-rules.mjs +2 -1
- package/scripts/validate-plugin.mjs +7 -0
- package/scripts/validate-wave-scope.mjs +21 -2
- package/scripts/vault-backfill.mjs +50 -37
- package/scripts/vault-integration-watcher.mjs +2 -1
- package/scripts/vault-mirror.mjs +238 -75
- package/scripts/wave-scope-binding.mjs +2 -1
- package/skills/_shared/config-reading.md +3 -3
- package/skills/_shared/fleet-protocol.md +134 -0
- package/skills/_shared/state-ownership.md +41 -12
- package/skills/autopilot/SKILL.md +25 -11
- package/skills/bootstrap/SKILL.md +6 -4
- package/skills/bootstrap/_shared-template.md +97 -2
- package/skills/bootstrap/fast-template.md +4 -1
- package/skills/bootstrap/private-contract.md +2 -1
- package/skills/bootstrap/public-fallback.md +6 -3
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +5 -3
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
- package/skills/bootstrap/standard-template.md +2 -0
- package/skills/claude-md-drift-check/SKILL.md +10 -10
- package/skills/claude-md-drift-check/checker.mjs +26 -18
- package/skills/discovery/probes/ssot-code-diff.mjs +12 -6
- package/skills/discovery/probes/vault-narrative-staleness.mjs +6 -2
- package/skills/discovery/probes/vault-staleness.mjs +9 -3
- package/skills/docs-orchestrator/SKILL.md +12 -5
- package/skills/docs-orchestrator/audience-mapping.md +5 -3
- package/skills/evolve/SKILL.md +13 -4
- package/skills/evolve/references/evolve-analyze-mode.md +42 -18
- package/skills/memory-cleanup/SKILL.md +1 -1
- package/skills/persona-panel/SKILL.md +2 -2
- package/skills/persona-panel/persona-format.md +2 -2
- package/skills/persona-panel/presets/designer-lens.md +1 -1
- package/skills/persona-panel/presets/engineer-lens.md +1 -1
- package/skills/persona-panel/presets/pm-lens.md +1 -1
- package/skills/plan/mode-retro.md +19 -4
- package/skills/quality-gates/SKILL.md +6 -4
- package/skills/remote-offload/SKILL.md +4 -2
- package/skills/session-end/learning-patterns.md +23 -13
- package/skills/session-end/metrics-collection.md +6 -7
- package/skills/session-end/phase-3-2-docs-verification.md +7 -6
- package/skills/session-end/plan-verification.md +25 -9
- package/skills/session-end/references/phase-3-documentation-updates.md +13 -3
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
- package/skills/session-end/references/session-summary-template.md +1 -1
- package/skills/session-end/session-metrics-write.md +65 -68
- package/skills/session-plan/SKILL.md +0 -3
- package/skills/session-start/SKILL.md +8 -2
- package/skills/session-start/references/operations-contract.md +6 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +32 -7
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +10 -3
- package/skills/test-audit/SKILL.md +152 -0
- package/skills/test-audit/references/campaign.md +111 -0
- package/skills/test-audit/references/mutate.sh +50 -0
- package/skills/vault-mirror/SKILL.md +21 -11
- package/skills/wave-executor/SKILL.md +2 -2
- package/skills/wave-executor/circuit-breaker.md +1 -1
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +11 -1
- package/skills/wave-executor/references/wave-loop-review.md +4 -6
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +13 -5
- package/templates/personas/README.md +5 -5
- package/templates/personas/accounting-compliance.v1.md +1 -1
- package/templates/personas/accounting-tax-advisor.v1.md +1 -1
- package/templates/personas/buyer-p1-cto.v1.md +1 -1
- package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
- package/templates/personas/buyer-p3-build.v1.md +1 -1
- package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
- package/templates/personas/buyer-p5-solo.v1.md +1 -1
- package/templates/personas/buyer-p6-ld.v1.md +1 -1
- package/templates/personas/klima-ai-expert.v1.md +1 -1
- package/templates/personas/klima-physicist.v1.md +1 -1
|
@@ -6,17 +6,21 @@
|
|
|
6
6
|
* Catches vendoring bugs that are invisible at the source-of-truth layer but
|
|
7
7
|
* become live footguns once synced into a target repo:
|
|
8
8
|
*
|
|
9
|
-
* - `
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* - `frontmatter-not-at-top` — a `---` block preceded by anything (the
|
|
10
|
+
* provenance comment, a blank line). Claude Code reads frontmatter only
|
|
11
|
+
* from line 1 and reads only its `paths:` key, so such a rule silently
|
|
12
|
+
* loads ALWAYS-ON in every consumer repo (#1449).
|
|
13
|
+
* - `globs-paths-mismatch` — a library rule's frontmatter must carry
|
|
14
|
+
* `globs:` AND `paths:` with the identical list (order-insensitive), or
|
|
15
|
+
* neither. `paths:` is what Claude Code reads; `globs:` stays because
|
|
16
|
+
* `rule-loader.mjs` prefers it (#742/#795). One key alone, or two
|
|
17
|
+
* different lists, scopes the rule differently per loader.
|
|
15
18
|
* - Missing provenance header — `rules-sync.mjs` detects "plugin-owned vs.
|
|
16
|
-
* local override"
|
|
17
|
-
* `PLUGIN_HEADER_PREFIX
|
|
18
|
-
*
|
|
19
|
-
*
|
|
19
|
+
* local override" via `isPluginOwnedContent()`: the header line starting
|
|
20
|
+
* with `PLUGIN_HEADER_PREFIX` on line 1 (a rule without frontmatter) or as
|
|
21
|
+
* the first non-blank line after a line-1 frontmatter block. A source file
|
|
22
|
+
* missing it gets mis-detected as a local override on the NEXT re-sync (it
|
|
23
|
+
* looks hand-authored), so the plugin can never update it again.
|
|
20
24
|
* - Unfilled placeholder tokens (`{{PROJECT_NAME}}`-style, `## TODO:
|
|
21
25
|
* Customize` headings, `<!-- TODO:` comments) leaking from an
|
|
22
26
|
* un-filled-in skeleton into a synced rule.
|
|
@@ -30,9 +34,10 @@
|
|
|
30
34
|
* Stdlib-only ESM, with the same picomatch-with-fallback resolution pattern
|
|
31
35
|
* as `rule-loader.mjs` (duplicated here rather than imported because those
|
|
32
36
|
* helpers are module-private there — the *approach* is reused, not
|
|
33
|
-
* reinvented).
|
|
34
|
-
*
|
|
35
|
-
* `
|
|
37
|
+
* reinvented). The globs-derived probes use the already-exported
|
|
38
|
+
* `parseGlobsFrontmatter` from `rule-loader.mjs` directly; only
|
|
39
|
+
* `globs-paths-mismatch` reads the two keys itself (`readScopeKeys`), because
|
|
40
|
+
* the loader merges them and cannot say which key carried which list.
|
|
36
41
|
*
|
|
37
42
|
* ## Scope: the `rules/` fleet library, never a repo's own `.claude/rules/`
|
|
38
43
|
*
|
|
@@ -43,15 +48,14 @@
|
|
|
43
48
|
* written target is never read back through the gate. The CLI's `--dir` is
|
|
44
49
|
* operator-supplied and means that same library.
|
|
45
50
|
*
|
|
46
|
-
* This
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* probe for `.claude/rules/`.
|
|
51
|
+
* This matters for `globs-paths-mismatch` and the provenance probe: they encode
|
|
52
|
+
* the LIBRARY convention (both scope keys, a provenance header). A repo's own
|
|
53
|
+
* CONSOLIDATED rules under `.claude/rules/` are `paths:`-only and header-free
|
|
54
|
+
* by design, because Claude Code's native loader reads ONLY `paths:`
|
|
55
|
+
* (`validate/check-rules.mjs` #1108; `docs/rule-authoring.md` § Consolidated
|
|
56
|
+
* rules point 3). Measured 2026-09-16 in this repo: 10 path-scoped rule files
|
|
57
|
+
* there, 9 of them `paths:`-only. Pointing `--dir` at such a tree reports
|
|
58
|
+
* findings about a convention those files were never meant to follow.
|
|
55
59
|
*
|
|
56
60
|
* @module validate-vendored-rules
|
|
57
61
|
*/
|
|
@@ -61,7 +65,7 @@ import { join, resolve, relative, basename } from 'node:path';
|
|
|
61
65
|
import { fileURLToPath } from 'node:url';
|
|
62
66
|
import { execFileSync } from 'node:child_process';
|
|
63
67
|
import { createRequire } from 'node:module';
|
|
64
|
-
import { parseGlobsFrontmatter } from './rule-loader.mjs';
|
|
68
|
+
import { parseGlobsFrontmatter, FRONTMATTER_NOT_AT_TOP } from './rule-loader.mjs';
|
|
65
69
|
import { isMainModule } from './is-main-module.mjs';
|
|
66
70
|
|
|
67
71
|
// Mirrors rules-sync.mjs's exported PLUGIN_HEADER_PREFIX (rules-sync.mjs
|
|
@@ -137,21 +141,26 @@ function matchGlob(filePath, globPattern) {
|
|
|
137
141
|
// Frontmatter helpers
|
|
138
142
|
// ---------------------------------------------------------------------------
|
|
139
143
|
|
|
144
|
+
// A line that may precede a MISPLACED frontmatter opener: blank, or one
|
|
145
|
+
// complete single-line HTML comment (the provenance header's shape).
|
|
140
146
|
const HEADER_LINE_RE = /^[ \t]*(?:<!--.*-->)?[ \t]*$/;
|
|
141
147
|
|
|
142
148
|
/**
|
|
143
|
-
*
|
|
149
|
+
* Locates a YAML frontmatter block delimited by `---` lines.
|
|
144
150
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
151
|
+
* Deliberately tolerant of a leading run of blank lines and/or single-line
|
|
152
|
+
* HTML comments before the opener — NOT because such a block is valid (Claude
|
|
153
|
+
* Code reads frontmatter only when `---` is line 1, and `rule-loader.mjs`
|
|
154
|
+
* no longer tolerates the prefix either), but so `frontmatter-not-at-top` can
|
|
155
|
+
* SEE the misplaced block and report it. Callers that judge what a loader
|
|
156
|
+
* reads must check `startLine === 0` first.
|
|
148
157
|
*
|
|
149
|
-
* @param {string} content
|
|
150
|
-
* @returns {{
|
|
151
|
-
* 0-based file-line
|
|
158
|
+
* @param {string[]} lines - file content split on `\r?\n`
|
|
159
|
+
* @returns {{ bodyLines: string[], startLine: number, endLine: number } | null}
|
|
160
|
+
* startLine / endLine are the 0-based file-line indices of the opening and
|
|
161
|
+
* closing `---`.
|
|
152
162
|
*/
|
|
153
|
-
function
|
|
154
|
-
const lines = content.split(/\r?\n/);
|
|
163
|
+
function locateFrontmatter(lines) {
|
|
155
164
|
let startLine = 0;
|
|
156
165
|
while (startLine < lines.length && HEADER_LINE_RE.test(lines[startLine])) {
|
|
157
166
|
startLine++;
|
|
@@ -159,22 +168,93 @@ function extractFrontmatter(content) {
|
|
|
159
168
|
if (lines[startLine] !== '---') return null;
|
|
160
169
|
for (let i = startLine + 1; i < lines.length; i++) {
|
|
161
170
|
if (lines[i] === '---') {
|
|
162
|
-
return {
|
|
171
|
+
return { bodyLines: lines.slice(startLine + 1, i), startLine, endLine: i };
|
|
163
172
|
}
|
|
164
173
|
}
|
|
165
174
|
return null;
|
|
166
175
|
}
|
|
167
176
|
|
|
168
177
|
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
178
|
+
* True when `content` carries the plugin provenance header in one of the two
|
|
179
|
+
* positions the vendoring convention allows (#1449):
|
|
180
|
+
*
|
|
181
|
+
* (a) line 1 — a rule WITHOUT frontmatter, and also every vendored copy
|
|
182
|
+
* written before #1449 (header on line 1, frontmatter after it);
|
|
183
|
+
* (b) the first non-blank line after the closing `---` of a frontmatter
|
|
184
|
+
* block that opens on line 1 — the current format for a rule WITH
|
|
185
|
+
* frontmatter, because Claude Code reads `paths:` only from a line-1
|
|
186
|
+
* block.
|
|
187
|
+
*
|
|
188
|
+
* This is the ownership predicate `rules-sync.mjs` uses to tell a
|
|
189
|
+
* plugin-owned vendored copy (rewrite it) from a local rule (preserve it), and
|
|
190
|
+
* the `provenance-header` probe's acceptance test. Accepting (a) with a
|
|
191
|
+
* frontmatter after it keeps old-format consumer copies plugin-owned, so the
|
|
192
|
+
* next sync REWRITES them into the new format instead of preserving them
|
|
193
|
+
* forever. Lives here rather than in rules-sync.mjs because rules-sync.mjs
|
|
194
|
+
* imports from this module; it re-exports this function.
|
|
195
|
+
*
|
|
196
|
+
* @param {string} content - raw rule file content
|
|
197
|
+
* @returns {boolean}
|
|
175
198
|
*/
|
|
176
|
-
function
|
|
177
|
-
|
|
199
|
+
export function isPluginOwnedContent(content) {
|
|
200
|
+
const lines = content.split(/\r?\n/);
|
|
201
|
+
if ((lines[0] ?? '').startsWith(PLUGIN_HEADER_PREFIX)) return true;
|
|
202
|
+
const fm = locateFrontmatter(lines);
|
|
203
|
+
if (!fm || fm.startLine !== 0) return false;
|
|
204
|
+
const next = lines.slice(fm.endLine + 1).find((l) => l.trim() !== '');
|
|
205
|
+
return next !== undefined && next.startsWith(PLUGIN_HEADER_PREFIX);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Strips one pair of surrounding quotes, as rule-loader.mjs does for list items.
|
|
210
|
+
* @param {string} s
|
|
211
|
+
* @returns {string}
|
|
212
|
+
*/
|
|
213
|
+
function unquoteItem(s) {
|
|
214
|
+
return s.trim().replace(/^["']|["']$/g, '');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Reads the `globs:` and `paths:` sequences of a frontmatter body EACH ON ITS
|
|
219
|
+
* OWN. `parseGlobsFrontmatter()` cannot answer this: it merges the two keys by
|
|
220
|
+
* design (`globs:` wins, `paths:` is discarded), so it cannot say which key
|
|
221
|
+
* carried which list. Accepts the three value shapes rule-loader.mjs accepts —
|
|
222
|
+
* indented block list, flow list `[a, b]`, single inline value.
|
|
223
|
+
*
|
|
224
|
+
* @param {string[]} bodyLines - frontmatter lines between the `---` markers
|
|
225
|
+
* @returns {{ globs?: { values: string[], index: number }, paths?: { values: string[], index: number } }}
|
|
226
|
+
* `index` is the 0-based body line of the key.
|
|
227
|
+
*/
|
|
228
|
+
function readScopeKeys(bodyLines) {
|
|
229
|
+
/** @type {{ globs?: { values: string[], index: number }, paths?: { values: string[], index: number } }} */
|
|
230
|
+
const found = {};
|
|
231
|
+
let active = null;
|
|
232
|
+
bodyLines.forEach((raw, index) => {
|
|
233
|
+
const line = raw.replace(/\s+$/, '');
|
|
234
|
+
if (line === '' || /^\s*#/.test(line)) return;
|
|
235
|
+
if (active) {
|
|
236
|
+
const item = /^\s+-\s+(.*)$/.exec(line);
|
|
237
|
+
if (item) {
|
|
238
|
+
found[active].values.push(unquoteItem(item[1]));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
active = null;
|
|
242
|
+
}
|
|
243
|
+
const key = /^(globs|paths):\s*(.*)$/.exec(line);
|
|
244
|
+
if (!key) return;
|
|
245
|
+
const [, name, value] = key;
|
|
246
|
+
let values = [];
|
|
247
|
+
if (value === '') {
|
|
248
|
+
active = name;
|
|
249
|
+
} else if (value.startsWith('[') && value.endsWith(']')) {
|
|
250
|
+
const inner = value.slice(1, -1).trim();
|
|
251
|
+
values = inner === '' ? [] : inner.split(',').map(unquoteItem);
|
|
252
|
+
} else {
|
|
253
|
+
values = [unquoteItem(value)];
|
|
254
|
+
}
|
|
255
|
+
found[name] = { values, index };
|
|
256
|
+
});
|
|
257
|
+
return found;
|
|
178
258
|
}
|
|
179
259
|
|
|
180
260
|
/**
|
|
@@ -463,7 +543,7 @@ const FOREIGN_TOKEN_RE = /[A-Z][a-z]+[A-Z]/;
|
|
|
463
543
|
|
|
464
544
|
/**
|
|
465
545
|
* @typedef {object} RuleViolation
|
|
466
|
-
* @property {string} rule - check id (paths-
|
|
546
|
+
* @property {string} rule - check id (frontmatter-not-at-top | globs-paths-mismatch | provenance-header | placeholder | zero-match-globs | foreign-glob)
|
|
467
547
|
* @property {'error'|'warn'} severity
|
|
468
548
|
* @property {string} message
|
|
469
549
|
* @property {number} [line] - 1-based line number, when known
|
|
@@ -486,52 +566,94 @@ export function validateRuleContent({ content, relPath, targetRoot = null, requi
|
|
|
486
566
|
/** @type {RuleViolation[]} */
|
|
487
567
|
const violations = [];
|
|
488
568
|
|
|
489
|
-
|
|
490
|
-
const fm =
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
569
|
+
const lines = content.split(/\r?\n/);
|
|
570
|
+
const fm = locateFrontmatter(lines);
|
|
571
|
+
|
|
572
|
+
// ---- frontmatter-not-at-top (error) ----
|
|
573
|
+
// Claude Code reads frontmatter only when `---` is line 1. Anything before
|
|
574
|
+
// it (the provenance comment, a blank line) makes it see NO frontmatter, so
|
|
575
|
+
// a path-scoped rule loads ALWAYS-ON in every consumer repo, with no error.
|
|
576
|
+
// Two detectors, one verdict (BV-003): `locateFrontmatter` sees a block
|
|
577
|
+
// behind blank lines / single-line comments (the provenance shape);
|
|
578
|
+
// `parseGlobsFrontmatter` (the loader — the same code the wave injector
|
|
579
|
+
// runs) additionally sees one behind ANY text, e.g. a heading. Either
|
|
580
|
+
// signal is the defect; the finding is reported once.
|
|
581
|
+
const loaderDefect = parseGlobsFrontmatter(content).defect === FRONTMATTER_NOT_AT_TOP;
|
|
582
|
+
if ((fm && fm.startLine !== 0) || (!(fm && fm.startLine === 0) && loaderDefect)) {
|
|
583
|
+
const openerLine = fm ? fm.startLine + 1 : lines.findIndex((l) => l === '---') + 1;
|
|
584
|
+
violations.push({
|
|
585
|
+
rule: 'frontmatter-not-at-top',
|
|
586
|
+
severity: 'error',
|
|
587
|
+
message:
|
|
588
|
+
`${relPath}: the frontmatter block opens on line ${openerLine}, not line 1. ` +
|
|
589
|
+
`Claude Code reads frontmatter only when '---' is the very first line, so it sees none ` +
|
|
590
|
+
`here and loads this rule always-on, ignoring its 'paths:' scope. Move the block to the ` +
|
|
591
|
+
`top; the provenance header goes on the first line after the closing '---'.`,
|
|
592
|
+
line: 1,
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ---- globs-paths-mismatch (error) ----
|
|
597
|
+
// Judged only on a line-1 block: a misplaced block is read by no loader,
|
|
598
|
+
// and frontmatter-not-at-top already reports it.
|
|
599
|
+
if (fm && fm.startLine === 0) {
|
|
600
|
+
const { globs, paths } = readScopeKeys(fm.bodyLines);
|
|
601
|
+
// body index i sits on 1-based file line i + 2 when the opener is line 1.
|
|
602
|
+
const keyLine = (k) => k.index + 2;
|
|
603
|
+
let detail = null;
|
|
604
|
+
let line;
|
|
605
|
+
if (globs && !paths) {
|
|
606
|
+
detail =
|
|
607
|
+
`declares 'globs:' but no 'paths:'. Claude Code reads only 'paths:', so it loads this ` +
|
|
608
|
+
`rule always-on. Add 'paths:' carrying the identical list.`;
|
|
609
|
+
line = keyLine(globs);
|
|
610
|
+
} else if (paths && !globs) {
|
|
611
|
+
detail =
|
|
612
|
+
`declares 'paths:' but no 'globs:'. rule-loader.mjs prefers 'globs:' (#742/#795); a ` +
|
|
613
|
+
`rule vendored out through the plugin's rules/ library carries both keys. Add 'globs:' ` +
|
|
614
|
+
`carrying the identical list. This does NOT apply to a repo's own consolidated ` +
|
|
615
|
+
`.claude/rules/ files — those are paths:-only by design and this gate never scans them.`;
|
|
616
|
+
line = keyLine(paths);
|
|
617
|
+
} else if (globs && paths) {
|
|
618
|
+
const g = new Set(globs.values);
|
|
619
|
+
const p = new Set(paths.values);
|
|
620
|
+
const onlyGlobs = [...g].filter((x) => !p.has(x));
|
|
621
|
+
const onlyPaths = [...p].filter((x) => !g.has(x));
|
|
622
|
+
if (onlyGlobs.length > 0 || onlyPaths.length > 0) {
|
|
623
|
+
detail =
|
|
624
|
+
`'globs:' and 'paths:' carry different lists (only in globs: ${JSON.stringify(onlyGlobs)}; ` +
|
|
625
|
+
`only in paths: ${JSON.stringify(onlyPaths)}). Claude Code scopes by 'paths:', ` +
|
|
626
|
+
`rule-loader.mjs by 'globs:', so the two would activate this rule on different files. ` +
|
|
627
|
+
`Make both lists identical.`;
|
|
628
|
+
line = keyLine(globs.index <= paths.index ? globs : paths);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (detail) {
|
|
494
632
|
violations.push({
|
|
495
|
-
rule: 'paths-
|
|
633
|
+
rule: 'globs-paths-mismatch',
|
|
496
634
|
severity: 'error',
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
// since #795 — `rule-loader.mjs:275` accepts `paths` alongside `globs`, and `:313`
|
|
500
|
-
// treats it as a fallback alias (`globs:` wins silently when both are present), so a
|
|
501
|
-
// `paths:`-only rule IS glob-scoped. The probe itself stays: it enforces the canonical
|
|
502
|
-
// vendoring form, which is a convention gate, not a loader-compatibility gate. That
|
|
503
|
-
// intent survives #795 and is the subject of the #742 fleet canonicalisation sweep.
|
|
504
|
-
// SCOPE (2026-09-16): this fires only over `rules/` library sources — the module doc's
|
|
505
|
-
// § Scope section names every caller. Do NOT re-point it at a repo's own
|
|
506
|
-
// `.claude/rules/`: those consolidated files are `paths:`-canonical and obeying this
|
|
507
|
-
// remedy there would undo the consolidation (docs/rule-authoring.md point 3).
|
|
508
|
-
message:
|
|
509
|
-
`${relPath}: frontmatter declares a top-level 'paths:' key. It is a recognized alias ` +
|
|
510
|
-
`for 'globs:' (issue #795), so the rule does load glob-scoped — but 'globs:' is the ` +
|
|
511
|
-
`canonical form for a rule VENDORED OUT through the plugin's rules/ library, which is ` +
|
|
512
|
-
`this probe's only population. Migrate to 'globs:' (see issue #742). This does NOT ` +
|
|
513
|
-
`apply to a repo's own consolidated .claude/rules/ files — those are paths:-canonical ` +
|
|
514
|
-
`(Claude Code's native loader reads only 'paths:') and this gate never scans them.`,
|
|
515
|
-
line: lineWithinFrontmatter(fm.startLine, fm.body, pathsMatch.index),
|
|
635
|
+
message: `${relPath}: frontmatter ${detail}`,
|
|
636
|
+
line,
|
|
516
637
|
});
|
|
517
638
|
}
|
|
518
639
|
}
|
|
519
640
|
|
|
520
641
|
// ---- provenance-header (error, opt-in) ----
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
line
|
|
533
|
-
|
|
534
|
-
|
|
642
|
+
// A header on line 1 followed by a frontmatter is accepted HERE (the file
|
|
643
|
+
// has a header, and isPluginOwnedContent must keep old vendored copies
|
|
644
|
+
// plugin-owned); frontmatter-not-at-top reports that shape, once.
|
|
645
|
+
if (requireProvenance && !isPluginOwnedContent(content)) {
|
|
646
|
+
violations.push({
|
|
647
|
+
rule: 'provenance-header',
|
|
648
|
+
severity: 'error',
|
|
649
|
+
message:
|
|
650
|
+
`${relPath}: missing provenance header — a line starting with ` +
|
|
651
|
+
`${JSON.stringify(PLUGIN_HEADER_PREFIX)} (see rules-sync.mjs PLUGIN_HEADER_PREFIX) must ` +
|
|
652
|
+
`stand either on line 1 (a rule without frontmatter) or on the first non-blank line after ` +
|
|
653
|
+
`the closing '---' of a frontmatter that opens on line 1. Without it, rules-sync.mjs ` +
|
|
654
|
+
`mis-detects this file as a local override on the next re-sync and will never update it again.`,
|
|
655
|
+
line: 1,
|
|
656
|
+
});
|
|
535
657
|
}
|
|
536
658
|
|
|
537
659
|
// ---- placeholder (error) — skip matches inside fenced code blocks ----
|
|
@@ -569,6 +691,9 @@ export function validateRuleContent({ content, relPath, targetRoot = null, requi
|
|
|
569
691
|
}
|
|
570
692
|
|
|
571
693
|
// ---- globs: derived checks (zero-match-globs, foreign-glob) ----
|
|
694
|
+
// Driven by the loader's own parse, so these judge exactly the list
|
|
695
|
+
// rule-loader.mjs will scope by (`globs:`, else its `paths:` alias). On a
|
|
696
|
+
// misplaced block that yields no globs — frontmatter-not-at-top covers it.
|
|
572
697
|
let globs;
|
|
573
698
|
try {
|
|
574
699
|
({ globs } = parseGlobsFrontmatter(content));
|
|
@@ -11,6 +11,8 @@ import { readFileSync, existsSync } from 'node:fs';
|
|
|
11
11
|
import { resolve, sep } from 'node:path';
|
|
12
12
|
import { spawnSync } from 'node:child_process';
|
|
13
13
|
|
|
14
|
+
import { redactHomeDir } from '../common.mjs';
|
|
15
|
+
|
|
14
16
|
const GENERATOR_MARKER = 'session-orchestrator-vault-mirror@1';
|
|
15
17
|
const MIRROR_DIRS = ['40-learnings', '50-sessions'];
|
|
16
18
|
|
|
@@ -79,7 +81,7 @@ export function autoCommitVaultMirror(vaultDirPath, sessionId, repo) {
|
|
|
79
81
|
|
|
80
82
|
const addResult = git(vaultDirPath, ['add', '--', ...stagingTargets]);
|
|
81
83
|
if (addResult.status !== 0) {
|
|
82
|
-
process.stderr.write(`vault-mirror: auto-commit git-add failed: ${addResult.stderr}\n`);
|
|
84
|
+
process.stderr.write(`vault-mirror: auto-commit git-add failed: ${redactHomeDir(addResult.stderr)}\n`);
|
|
83
85
|
process.stdout.write(
|
|
84
86
|
JSON.stringify({ action: 'auto-commit-skipped', reason: 'git-add-failed' }) + '\n',
|
|
85
87
|
);
|
|
@@ -88,7 +90,7 @@ export function autoCommitVaultMirror(vaultDirPath, sessionId, repo) {
|
|
|
88
90
|
|
|
89
91
|
const diff = git(vaultDirPath, ['diff', '--cached', '--name-only', '--', ...stagingTargets]);
|
|
90
92
|
if (diff.status !== 0) {
|
|
91
|
-
process.stderr.write(`vault-mirror: auto-commit git-diff failed: ${diff.stderr}\n`);
|
|
93
|
+
process.stderr.write(`vault-mirror: auto-commit git-diff failed: ${redactHomeDir(diff.stderr)}\n`);
|
|
92
94
|
process.stdout.write(
|
|
93
95
|
JSON.stringify({ action: 'auto-commit-skipped', reason: 'git-diff-failed' }) + '\n',
|
|
94
96
|
);
|
|
@@ -182,7 +184,7 @@ export function autoCommitVaultMirror(vaultDirPath, sessionId, repo) {
|
|
|
182
184
|
// committed content is already validated. See SKILL.md § "Pre-commit hook bypass (--no-verify)".
|
|
183
185
|
const commit = git(vaultDirPath, ['commit', '-m', subject, '--no-verify']);
|
|
184
186
|
if (commit.status !== 0) {
|
|
185
|
-
process.stderr.write(`vault-mirror: auto-commit git-commit failed: ${commit.stderr}\n`);
|
|
187
|
+
process.stderr.write(`vault-mirror: auto-commit git-commit failed: ${redactHomeDir(commit.stderr)}\n`);
|
|
186
188
|
process.stdout.write(
|
|
187
189
|
JSON.stringify({ action: 'auto-commit-skipped', reason: 'git-commit-failed' }) + '\n',
|
|
188
190
|
);
|
|
@@ -10,13 +10,18 @@
|
|
|
10
10
|
* - Pure + deterministic (given the same vaultName, the same `.vault.yaml`
|
|
11
11
|
* under `repoRoot`, the same git remote, and the same host-local pseudonym
|
|
12
12
|
* map).
|
|
13
|
-
* - `repoRoot` (#1389) selects the
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* caller
|
|
19
|
-
*
|
|
13
|
+
* - `repoRoot` (#1389, #1503) selects the repo whose identity is resolved —
|
|
14
|
+
* BOTH the `.vault.yaml` lookup and the {@link deriveRepo} fallback (its
|
|
15
|
+
* preferred git remote, else its directory name). It defaults to
|
|
16
|
+
* `process.cwd()`. Until #1503 only the `.vault.yaml` half honoured it: the
|
|
17
|
+
* git fallback still read the process cwd, cached once per process, so a
|
|
18
|
+
* caller mirroring a repo other than the one it ran in got the CWD's
|
|
19
|
+
* namespace — which is how one repo's ledger overwrote another repo's vault
|
|
20
|
+
* notes. The revisit trigger this comment used to name ("a caller mirrors a
|
|
21
|
+
* repo other than the one it runs in") fired; the cache is now keyed per
|
|
22
|
+
* root. Remaining ceiling: a caller that passes NO `repoRoot` still gets the
|
|
23
|
+
* cwd's identity — `scripts/vault-mirror.mjs` derives a root from
|
|
24
|
+
* `--source` precisely so that it never relies on that default.
|
|
20
25
|
* - Returns a lowercase kebab slug safe for use as a filesystem path segment.
|
|
21
26
|
* - Host-local pseudonym mapping (Epic #725 D5): consulted ONLY at the redaction
|
|
22
27
|
* site (only when a segment is owner-leaky). If such a repo (raw or sanitised)
|
|
@@ -48,7 +53,7 @@
|
|
|
48
53
|
* (itself a leaf — `node:child_process` only) — never the pipeline.
|
|
49
54
|
*/
|
|
50
55
|
|
|
51
|
-
import { basename } from 'node:path';
|
|
56
|
+
import { basename, resolve } from 'node:path';
|
|
52
57
|
|
|
53
58
|
import { subjectToSlug } from './utils.mjs';
|
|
54
59
|
import { isOwnerLeakySegment } from '../../lib/validate/check-owner-leakage.mjs';
|
|
@@ -97,7 +102,13 @@ function currentMapPath() {
|
|
|
97
102
|
|
|
98
103
|
// ── Repo identity (issue #343; moved here from process.mjs for #734b) ────────
|
|
99
104
|
|
|
100
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Resolved repo root → derived identity. Keyed per root (#1503): a single
|
|
107
|
+
* process-wide slot answered every later caller with the FIRST root's identity,
|
|
108
|
+
* whichever root that caller asked about.
|
|
109
|
+
* @type {Map<string, string>}
|
|
110
|
+
*/
|
|
111
|
+
const _cachedRepos = new Map();
|
|
101
112
|
|
|
102
113
|
/** scp-like SSH remote: `git@host:org/name.git` (no `://`, an `@` before any `/`). */
|
|
103
114
|
const SCP_LIKE_REMOTE_RE = /^[^@/\s]+@[^:/\s]+:(.+)$/;
|
|
@@ -177,7 +188,7 @@ function repoIdentifierFromRemoteUrl(url) {
|
|
|
177
188
|
* {@link resolveRepoNamespace} returned `'unknown-repo'`. Measured
|
|
178
189
|
* 2026-08-19; it turned a namespace assertion red and blocked a push.
|
|
179
190
|
*
|
|
180
|
-
* Fallback: `path.basename(
|
|
191
|
+
* Fallback: `path.basename(<root>)` — but the two reasons for
|
|
181
192
|
* reaching it are no longer indistinguishable. A QUERY FAILURE (not a git repo,
|
|
182
193
|
* git not on PATH, git errored — {@link isQueryFailure}) emits a stderr WARN,
|
|
183
194
|
* because the identity under which vault notes are written was GUESSED. A real
|
|
@@ -186,8 +197,11 @@ function repoIdentifierFromRemoteUrl(url) {
|
|
|
186
197
|
* An `ok` resolution whose URL yields no usable identifier also falls back
|
|
187
198
|
* silently — the query succeeded and the answer was simply unusable.
|
|
188
199
|
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
200
|
+
* `<root>` is `repoRoot` when given, else `process.cwd()` (#1503: the remote is
|
|
201
|
+
* read with `git -C <root>`, so the identity is the root's, never the cwd's).
|
|
202
|
+
*
|
|
203
|
+
* Cached per resolved root — repo identity does not change mid-run, and the
|
|
204
|
+
* cache also keeps the WARN to at most one line per root.
|
|
191
205
|
*
|
|
192
206
|
* NOTE — this is the RAW identifier and is NOT leak-guarded. Never write its
|
|
193
207
|
* output to the vault directly; route it through {@link resolveRepoNamespace}
|
|
@@ -198,18 +212,25 @@ function repoIdentifierFromRemoteUrl(url) {
|
|
|
198
212
|
* until the #734b cycle break, and the module-level cache means there must remain
|
|
199
213
|
* exactly ONE definition.
|
|
200
214
|
*
|
|
215
|
+
* @param {object} [opts]
|
|
216
|
+
* @param {string|null} [opts.repoRoot] - Repo whose identity to derive;
|
|
217
|
+
* `undefined`/`null` → `process.cwd()`.
|
|
201
218
|
* @returns {string} e.g. 'Kanevry/session-orchestrator' or a bare directory name.
|
|
202
219
|
*/
|
|
203
|
-
export function deriveRepo() {
|
|
204
|
-
|
|
220
|
+
export function deriveRepo({ repoRoot = null } = {}) {
|
|
221
|
+
const root = resolve(repoRoot ?? process.cwd());
|
|
222
|
+
const cached = _cachedRepos.get(root);
|
|
223
|
+
if (cached !== undefined) return cached;
|
|
224
|
+
|
|
225
|
+
const remember = (identity) => {
|
|
226
|
+
_cachedRepos.set(root, identity);
|
|
227
|
+
return identity;
|
|
228
|
+
};
|
|
205
229
|
|
|
206
|
-
const resolved = resolvePreferredRemote({});
|
|
230
|
+
const resolved = resolvePreferredRemote({ repoRoot: root });
|
|
207
231
|
if (resolved.ok) {
|
|
208
232
|
const identifier = repoIdentifierFromRemoteUrl(resolved.url);
|
|
209
|
-
if (identifier !== '')
|
|
210
|
-
_cachedRepo = identifier;
|
|
211
|
-
return _cachedRepo;
|
|
212
|
-
}
|
|
233
|
+
if (identifier !== '') return remember(identifier);
|
|
213
234
|
} else if (isQueryFailure(resolved.reason)) {
|
|
214
235
|
process.stderr.write(
|
|
215
236
|
`WARN vault-mirror/namespace: could not query git remotes (${resolved.reason}); ` +
|
|
@@ -218,8 +239,7 @@ export function deriveRepo() {
|
|
|
218
239
|
);
|
|
219
240
|
}
|
|
220
241
|
|
|
221
|
-
|
|
222
|
-
return _cachedRepo;
|
|
242
|
+
return remember(basename(root));
|
|
223
243
|
}
|
|
224
244
|
|
|
225
245
|
/**
|
|
@@ -249,9 +269,9 @@ function lookupPseudonym(base, seg) {
|
|
|
249
269
|
* When non-empty and non-whitespace, used in place of the declared/git-derived
|
|
250
270
|
* repo name. When absent, the base is the repo's declared `.vault.yaml`
|
|
251
271
|
* `metadata.slug` (#1131), else the git origin via deriveRepo().
|
|
252
|
-
* @param {string|null} [opts.repoRoot] -
|
|
253
|
-
* the declared slug (#1389)
|
|
254
|
-
*
|
|
272
|
+
* @param {string|null} [opts.repoRoot] - Repo whose identity is resolved: its
|
|
273
|
+
* `.vault.yaml` supplies the declared slug (#1389), and its git remote feeds
|
|
274
|
+
* the deriveRepo() fallback (#1503). `undefined`/`null` → `process.cwd()`.
|
|
255
275
|
* @returns {string} A single kebab-slug path segment, e.g. 'session-orchestrator'.
|
|
256
276
|
* Special returns:
|
|
257
277
|
* - 'unknown-repo' — slug derivation produced an empty string.
|
|
@@ -284,10 +304,11 @@ export function resolveRepoNamespace({ vaultName = null, repoRoot = null } = {})
|
|
|
284
304
|
// guard below deliberately — a declared slug is operator data flowing into a
|
|
285
305
|
// written path and is checked by CP1/CP6/CP10 and the pseudonym map exactly
|
|
286
306
|
// like any git-derived identifier.
|
|
287
|
-
const
|
|
307
|
+
const root = repoRoot ?? process.cwd();
|
|
308
|
+
const declaredSlug = readVaultSlug(root);
|
|
288
309
|
const base = (vaultName && typeof vaultName === 'string' && vaultName.trim())
|
|
289
310
|
? vaultName.trim()
|
|
290
|
-
: (declaredSlug ?? deriveRepo());
|
|
311
|
+
: (declaredSlug ?? deriveRepo({ repoRoot: root }));
|
|
291
312
|
|
|
292
313
|
// Sanitise: collapse to last path segment, lowercase, strip non-[a-z0-9-].
|
|
293
314
|
const seg = subjectToSlug(base);
|