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
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* render-learnings.mjs — Learning markdown generators for vault-mirror (Issue #283 split).
|
|
3
3
|
*
|
|
4
|
-
* Exports: detectLearningSchema, normalizeLearningEntry, generateLearningNote, generateLearningNoteV2
|
|
4
|
+
* Exports: detectLearningSchema, normalizeLearningEntry, generateLearningNote, generateLearningNoteV2,
|
|
5
|
+
* sourceRecordValue, learningSourceRecord, sourceRecordLine (#1503)
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { toDate, truncateAtWord, yamlQuoteIfNeeded, subjectToSlug, isValidSlug, buildTag, resolveSourceSessionLink } from './utils.mjs';
|
|
@@ -18,6 +19,63 @@ export function detectLearningSchema(entry) {
|
|
|
18
19
|
return entry && typeof entry.text === 'string' ? 'v2' : 'v1';
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Characters a `source-record:` value may carry (#1503). The value is always
|
|
24
|
+
* written double-quoted, and BOTH readers of it — the vault-sync YAML loader and
|
|
25
|
+
* the line-based `parseFrontmatter` in utils.mjs, which only strips the
|
|
26
|
+
* surrounding quotes and never unescapes — must read back exactly the string
|
|
27
|
+
* that was rendered. Excluding `"`, `\`, whitespace and anything non-ASCII is
|
|
28
|
+
* what makes that round trip lossless without an escaping scheme. Record uuids,
|
|
29
|
+
* kebab ids, session uuids and ISO timestamps all fit.
|
|
30
|
+
*/
|
|
31
|
+
const SOURCE_RECORD_RE = /^[A-Za-z0-9][A-Za-z0-9._:+@/-]{0,199}$/;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Normalise one candidate `source-record` value (#1503): the identity of the
|
|
35
|
+
* JSONL record a vault note was rendered from. Returns `null` for anything that
|
|
36
|
+
* cannot round-trip through the frontmatter (see {@link SOURCE_RECORD_RE}) — the
|
|
37
|
+
* line is then omitted and the record-level ownership check is skipped for that
|
|
38
|
+
* entry, which is the same "absent = allow" reading an old note gets.
|
|
39
|
+
*
|
|
40
|
+
* Shared by both renderers so the rendered value and the value process.mjs
|
|
41
|
+
* compares against come from ONE derivation.
|
|
42
|
+
*
|
|
43
|
+
* @param {unknown} value
|
|
44
|
+
* @returns {string|null}
|
|
45
|
+
*/
|
|
46
|
+
export function sourceRecordValue(value) {
|
|
47
|
+
if (typeof value !== 'string') return null;
|
|
48
|
+
const trimmed = value.trim();
|
|
49
|
+
return SOURCE_RECORD_RE.test(trimmed) ? trimmed : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The `source-record` of a learning note: the record's own `id` (a uuid for v1
|
|
54
|
+
* producers, the kebab id for v2 and for alias producers whose id was derived
|
|
55
|
+
* from the subject by {@link normalizeLearningEntry}).
|
|
56
|
+
*
|
|
57
|
+
* Named ceiling: a derived id IS the subject slug, so two repos holding the same
|
|
58
|
+
* subject under derived ids render the same `source-record` and this field
|
|
59
|
+
* cannot tell them apart — `source-repo` and the CLI's repo-root resolution are
|
|
60
|
+
* what separate those.
|
|
61
|
+
*
|
|
62
|
+
* @param {object} entry - normalized learning entry.
|
|
63
|
+
* @returns {string|null}
|
|
64
|
+
*/
|
|
65
|
+
export function learningSourceRecord(entry) {
|
|
66
|
+
return sourceRecordValue(entry?.id);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Render the `source-record:` frontmatter line, or `''` when the entry has no
|
|
71
|
+
* representable record key.
|
|
72
|
+
* @param {string|null} value - output of {@link sourceRecordValue}.
|
|
73
|
+
* @returns {string}
|
|
74
|
+
*/
|
|
75
|
+
export function sourceRecordLine(value) {
|
|
76
|
+
return value === null ? '' : `source-record: "${value}"\n`;
|
|
77
|
+
}
|
|
78
|
+
|
|
21
79
|
const firstString = (...vals) => vals.find((v) => typeof v === 'string' && v.length > 0);
|
|
22
80
|
|
|
23
81
|
const joinList = (v) =>
|
|
@@ -112,6 +170,9 @@ export function generateLearningNote(entry, slug, opts = {}) {
|
|
|
112
170
|
// vault-sync schema declares source-repo OPTIONAL).
|
|
113
171
|
const sourceRepoLine =
|
|
114
172
|
typeof opts.repoNs === 'string' && opts.repoNs.length > 0 ? `source-repo: ${opts.repoNs}\n` : '';
|
|
173
|
+
// #1503: the record this note was rendered from — process.mjs refuses to
|
|
174
|
+
// overwrite a note whose `source-record` names a different record.
|
|
175
|
+
const recordLine = sourceRecordLine(learningSourceRecord(entry));
|
|
115
176
|
|
|
116
177
|
// source_session is emitted as an Obsidian wikilink ONLY when source_session
|
|
117
178
|
// resolves to a real, mirror-able session id (semantic or UUID-v4). Anything
|
|
@@ -132,7 +193,7 @@ created: ${created}
|
|
|
132
193
|
updated: ${updated}
|
|
133
194
|
tags: ${tags}
|
|
134
195
|
source_session: ${sourceSessionLink}
|
|
135
|
-
${expiresLine}${sourceRepoLine}_generator: ${GENERATOR_MARKER}
|
|
196
|
+
${expiresLine}${sourceRepoLine}${recordLine}_generator: ${GENERATOR_MARKER}
|
|
136
197
|
---
|
|
137
198
|
|
|
138
199
|
# ${titleRaw}
|
|
@@ -176,6 +237,8 @@ export function generateLearningNoteV2(entry, slug, opts = {}) {
|
|
|
176
237
|
// opts.repoNs (see generateLearningNote for the full contract). Backward-compatible.
|
|
177
238
|
const sourceRepoLine =
|
|
178
239
|
typeof opts.repoNs === 'string' && opts.repoNs.length > 0 ? `source-repo: ${opts.repoNs}\n` : '';
|
|
240
|
+
// #1503: see generateLearningNote.
|
|
241
|
+
const recordLine = sourceRecordLine(learningSourceRecord(entry));
|
|
179
242
|
|
|
180
243
|
return `---
|
|
181
244
|
id: ${slug}
|
|
@@ -185,7 +248,7 @@ status: ${status}
|
|
|
185
248
|
created: ${created}
|
|
186
249
|
updated: ${updated}
|
|
187
250
|
tags: ${tags}
|
|
188
|
-
${sourceRepoLine}_generator: ${GENERATOR_MARKER}
|
|
251
|
+
${sourceRepoLine}${recordLine}_generator: ${GENERATOR_MARKER}
|
|
189
252
|
---
|
|
190
253
|
|
|
191
254
|
# ${titleRaw}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* render-sessions.mjs — Session markdown generators for vault-mirror (Issue #283 split).
|
|
3
3
|
*
|
|
4
|
-
* Exports: detectSessionSchema, normalizeSessionEntry, generateSessionNote, generateSessionNoteV2, generateSessionNoteV3
|
|
4
|
+
* Exports: detectSessionSchema, normalizeSessionEntry, generateSessionNote, generateSessionNoteV2, generateSessionNoteV3,
|
|
5
|
+
* sessionSourceRecord (#1503)
|
|
5
6
|
*
|
|
6
7
|
* #732: all three generators accept `options.repoNs` (the leak-guarded /
|
|
7
8
|
* pseudonym-mapped namespace segment from resolveRepoNamespace(), threaded
|
|
@@ -15,6 +16,7 @@
|
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
import { toDate, buildTag, slugifyIdSafe } from './utils.mjs';
|
|
19
|
+
import { sourceRecordValue, sourceRecordLine } from './render-learnings.mjs';
|
|
18
20
|
|
|
19
21
|
const GENERATOR_MARKER = 'session-orchestrator-vault-mirror@1';
|
|
20
22
|
|
|
@@ -282,6 +284,28 @@ function fmLine(key, value) {
|
|
|
282
284
|
return `${key}: ${value}\n`;
|
|
283
285
|
}
|
|
284
286
|
|
|
287
|
+
/**
|
|
288
|
+
* The `source-record` of a session note (#1503): the physical session the note
|
|
289
|
+
* was rendered from — `raw_session_id` (the harness's session uuid) when the
|
|
290
|
+
* record carries one, else `started_at`. `session_id` itself cannot serve: it is
|
|
291
|
+
* the FILENAME, and semantic ids such as `main-2026-10-02-session-1` recur across
|
|
292
|
+
* repos by construction, which is exactly the collision this field exists to
|
|
293
|
+
* detect. All three generators require `started_at`, so every renderable record
|
|
294
|
+
* has a key.
|
|
295
|
+
*
|
|
296
|
+
* Named ceiling: a record whose key changes between runs — a later backfill
|
|
297
|
+
* adding `raw_session_id` to a record first mirrored under its `started_at` —
|
|
298
|
+
* reads as a different record, and process.mjs then refuses the rewrite
|
|
299
|
+
* (`skipped-foreign-owner`, visible on stdout). Revisit if that is ever observed
|
|
300
|
+
* on a same-repo record.
|
|
301
|
+
*
|
|
302
|
+
* @param {object} entry - normalized session entry.
|
|
303
|
+
* @returns {string|null}
|
|
304
|
+
*/
|
|
305
|
+
export function sessionSourceRecord(entry) {
|
|
306
|
+
return sourceRecordValue(entry?.raw_session_id) ?? sourceRecordValue(entry?.started_at);
|
|
307
|
+
}
|
|
308
|
+
|
|
285
309
|
/**
|
|
286
310
|
* Session JSONL has three producer schemas in production:
|
|
287
311
|
* v1 (legacy): total_waves, total_agents, total_files_changed, agent_summary, waves[{agent_count, files_changed, quality}]
|
|
@@ -521,7 +545,7 @@ status: ${vaultStatus}
|
|
|
521
545
|
created: ${created}
|
|
522
546
|
updated: ${updated}
|
|
523
547
|
tags: ${tags}
|
|
524
|
-
${fmLine('source-repo', repoNs)}_generator: ${GENERATOR_MARKER}
|
|
548
|
+
${fmLine('source-repo', repoNs)}${sourceRecordLine(sessionSourceRecord(entry))}_generator: ${GENERATOR_MARKER}
|
|
525
549
|
---
|
|
526
550
|
|
|
527
551
|
# Session ${session_id}
|
|
@@ -606,7 +630,7 @@ status: ${vaultStatus}
|
|
|
606
630
|
created: ${created}
|
|
607
631
|
updated: ${updated}
|
|
608
632
|
tags: ${tags}
|
|
609
|
-
${fmLine('source-repo', repoNs)}_generator: ${GENERATOR_MARKER}
|
|
633
|
+
${fmLine('source-repo', repoNs)}${sourceRecordLine(sessionSourceRecord(entry))}_generator: ${GENERATOR_MARKER}
|
|
610
634
|
---
|
|
611
635
|
|
|
612
636
|
# Session ${session_id}
|
|
@@ -731,7 +755,7 @@ status: ${vaultStatus}
|
|
|
731
755
|
created: ${created}
|
|
732
756
|
updated: ${updated}
|
|
733
757
|
tags: ${tags}
|
|
734
|
-
${fmLine('source-repo', repoNs)}_generator: ${GENERATOR_MARKER}
|
|
758
|
+
${fmLine('source-repo', repoNs)}${sourceRecordLine(sessionSourceRecord(entry))}_generator: ${GENERATOR_MARKER}
|
|
735
759
|
---
|
|
736
760
|
|
|
737
761
|
# Session ${session_id}
|
|
@@ -11,6 +11,16 @@
|
|
|
11
11
|
* records that between them said nothing happened (#1151). Its count is
|
|
12
12
|
* carried by the run event's `skipped` total and `action_breakdown`, so the
|
|
13
13
|
* class stays measured — only its per-line locators are gone.
|
|
14
|
+
* `skipped-duplicate-session` (#1291) is one of these per-entry records:
|
|
15
|
+
* a `--kind session` line whose `session_id` a later line in the same
|
|
16
|
+
* source superseded, so it was never dispatched. It carries `record_id`
|
|
17
|
+
* and `line` and never `path` — no target file exists for it.
|
|
18
|
+
* `skipped-foreign-owner` (#1503) is another: the generator note at the
|
|
19
|
+
* target path names a different owner (a `source-record` the writer's
|
|
20
|
+
* ledger does not carry, or a different session id), so it was left
|
|
21
|
+
* untouched — unless it still leaks a secret, which is healed over the
|
|
22
|
+
* guard. It carries the refused note's `path` and a constant `reason`
|
|
23
|
+
* (`source-record mismatch` / `id mismatch`).
|
|
14
24
|
* - `orchestrator.vault.mirror_run_completed` — ONE record per CLI run,
|
|
15
25
|
* carrying the DENOMINATOR (`total` plus the per-class counts).
|
|
16
26
|
*
|
|
@@ -21,8 +31,8 @@
|
|
|
21
31
|
* "a rule you cannot falsify is not a rule"). The run event is emitted
|
|
22
32
|
* unconditionally — including on every abort path, where it carries an
|
|
23
33
|
* `aborted` discriminator (`missing-vault-dir` | `vault-not-canonical` |
|
|
24
|
-
* `missing-source` | `
|
|
25
|
-
* `unexpected-error`). So `total: 0` is a MEASURED zero, a partial count is a
|
|
34
|
+
* `missing-source` | `source-repo-mismatch` | `malformed-json` |
|
|
35
|
+
* `filesystem-error` | `unexpected-error`). So `total: 0` is a MEASURED zero, a partial count is a
|
|
26
36
|
* LABELLED partial rather than a silent one, and the record's absence — and
|
|
27
37
|
* nothing else — is the broken-emitter signal.
|
|
28
38
|
*
|
|
@@ -32,9 +42,10 @@
|
|
|
32
42
|
*
|
|
33
43
|
* Ledger destination: `emitEvent` is called 2-arg, so every event from one run
|
|
34
44
|
* resolves the SAME destination (`SO_PROJECT_DIR`, i.e. `CLAUDE_PROJECT_DIR` or
|
|
35
|
-
* the CWD walk-up) as the masker emit.
|
|
36
|
-
*
|
|
37
|
-
*
|
|
45
|
+
* the CWD walk-up) as the masker emit. The repo root that decides the vault
|
|
46
|
+
* NAMESPACE (#1503: `--repo-root`, else the git repo containing `--source`) is
|
|
47
|
+
* deliberately NOT used for the ledger — routing telemetry by it would split a
|
|
48
|
+
* single run's records across two ledgers.
|
|
38
49
|
*/
|
|
39
50
|
|
|
40
51
|
import { emitEvent, sessionAttribution } from '../events.mjs';
|
|
@@ -162,20 +173,23 @@ export async function emitMirrorEvent({
|
|
|
162
173
|
* @param {number} opts.updated — entries whose action was `updated`.
|
|
163
174
|
* @param {number} opts.skipped — entries skipped for a NON-failure reason
|
|
164
175
|
* (`skipped-noop`, `skipped-handwritten`, `skipped-quality-low`,
|
|
165
|
-
* `skipped-collision-resolved`, `skipped-abandoned
|
|
176
|
+
* `skipped-collision-resolved`, `skipped-foreign-owner`, `skipped-abandoned`,
|
|
177
|
+
* `skipped-duplicate-session`). The last one (#1291) is what keeps
|
|
178
|
+
* `created + updated + skipped + failed === total` true for a `--kind
|
|
179
|
+
* session` run whose dedup pass collapsed a duplicate `session_id` line.
|
|
166
180
|
* @param {number} opts.failed — entries that produced `skipped-invalid`
|
|
167
181
|
* (validation error or mapper crash). Split out from `skipped` because these
|
|
168
182
|
* are the runs where a session silently ends up WITHOUT its vault note.
|
|
169
183
|
* @param {Record<string, number>} [opts.actionBreakdown] — per-action counts;
|
|
170
184
|
* only actions observed at least once appear.
|
|
171
185
|
* @param {boolean} opts.dryRun — whether this run wrote anything at all.
|
|
172
|
-
* @param {'missing-vault-dir'|'vault-not-canonical'|'missing-source'|'malformed-json'|'filesystem-error'|'unexpected-error'} [opts.aborted]
|
|
186
|
+
* @param {'missing-vault-dir'|'vault-not-canonical'|'missing-source'|'source-repo-mismatch'|'malformed-json'|'filesystem-error'|'unexpected-error'} [opts.aborted]
|
|
173
187
|
* Present ONLY when the run exited before its normal tail. OMITTED on a
|
|
174
188
|
* complete run — absent means "ran to the end", never "unknown". The first
|
|
175
|
-
*
|
|
176
|
-
* entry, so all five counters are `0` and that zero is the point —
|
|
177
|
-
* record, a mirror that never started is indistinguishable from one
|
|
178
|
-
* never invoked.
|
|
189
|
+
* four values are PRE-LOOP aborts (#1151, #1503): the run never reached its
|
|
190
|
+
* first entry, so all five counters are `0` and that zero is the point —
|
|
191
|
+
* without a record, a mirror that never started is indistinguishable from one
|
|
192
|
+
* that was never invoked.
|
|
179
193
|
* @returns {Promise<void>}
|
|
180
194
|
*/
|
|
181
195
|
export async function emitMirrorRunEvent({
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Telemetry: every {@link mirrorBoard} call — and therefore every
|
|
14
14
|
* {@link sweepBoard} call — emits exactly ONE {@link BOARD_EVENT} record,
|
|
15
|
-
* including the no-op paths (`skipped-vault-disabled`,
|
|
16
|
-
* `skipped-
|
|
15
|
+
* including the no-op paths (`skipped-vault-disabled`,
|
|
16
|
+
* `skipped-vault-not-canonical`, `skipped-handwritten`, `skipped-noop`,
|
|
17
|
+
* `skipped-write-failed`). Those are the states that previously
|
|
17
18
|
* looked identical to a healthy write from outside the process. Emission is
|
|
18
19
|
* best-effort and can never fail a board write.
|
|
19
20
|
*
|
|
@@ -43,7 +44,8 @@
|
|
|
43
44
|
* 2. Defense-in-depth: writeBoard hard-refuses to touch `_overview.md`
|
|
44
45
|
* (sven-owned, must NEVER be written by this generator).
|
|
45
46
|
*
|
|
46
|
-
* No console noise — library code
|
|
47
|
+
* No console noise — library code — except the one stderr line on a
|
|
48
|
+
* canonical-vault refusal (#1450). Plain Node ESM. No external deps.
|
|
47
49
|
*/
|
|
48
50
|
|
|
49
51
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
@@ -60,6 +62,8 @@ import { enumerateCandidates } from '../dispatcher/enumerate.mjs';
|
|
|
60
62
|
import { atomicWriteWithBackup } from '../io.mjs';
|
|
61
63
|
import { withBoardLock } from './board-lock.mjs';
|
|
62
64
|
import { expandTilde } from '../common.mjs';
|
|
65
|
+
import { checkCanonicalVault, describeOriginForLog } from '../named-vault-resolver.mjs';
|
|
66
|
+
import { loadHostPaths } from '../config/host-paths.mjs';
|
|
63
67
|
|
|
64
68
|
/** Frontmatter sentinel that identifies generator-owned board files. */
|
|
65
69
|
export const GENERATOR_MARKER = 'session-orchestrator-active-sessions@1';
|
|
@@ -356,8 +360,12 @@ export async function collectRows({ repos, now = new Date(), registry, priorStat
|
|
|
356
360
|
? lock.mode
|
|
357
361
|
: (registryEntry && registryEntry.mode ? registryEntry.mode : null);
|
|
358
362
|
|
|
363
|
+
// The "Last heartbeat" column shows a heartbeat or nothing: a lock without
|
|
364
|
+
// `last_heartbeat` renders '—', never its `started_at` under that label
|
|
365
|
+
// (#595 sunset, 2026-10-02 — `isLockLive()` already calls it not live, so
|
|
366
|
+
// the status derivation below treats it as a dead lease).
|
|
359
367
|
const heartbeat = lock
|
|
360
|
-
? (lock.last_heartbeat ??
|
|
368
|
+
? (lock.last_heartbeat ?? null)
|
|
361
369
|
: (registryEntry ? (registryEntry.last_heartbeat ?? null) : null);
|
|
362
370
|
|
|
363
371
|
// ── Status derivation ──
|
|
@@ -825,6 +833,8 @@ async function emitBoardEvent({ repoRoot, caller, action, path: outputPath, rows
|
|
|
825
833
|
* Vault path assertion (Epic #673 safety): the resolved vault dir MUST live
|
|
826
834
|
* under $HOME — a vault outside the home tree is refused as `skipped-vault-disabled`
|
|
827
835
|
* so a misconfigured path can never drive a write into an arbitrary location.
|
|
836
|
+
* It must also be a canonical Meta-Vault ({@link checkCanonicalVault}, #1450):
|
|
837
|
+
* otherwise `skipped-vault-not-canonical`, nothing written, no lock, dry-run included.
|
|
828
838
|
*
|
|
829
839
|
* @param {object} opts
|
|
830
840
|
* @param {string} opts.repoRoot — the repo whose row is being updated.
|
|
@@ -840,11 +850,14 @@ async function emitBoardEvent({ repoRoot, caller, action, path: outputPath, rows
|
|
|
840
850
|
* @param {Date} [opts.now]
|
|
841
851
|
* @param {boolean} [opts.dryRun]
|
|
842
852
|
* @param {object} [opts.fs] — injectable fs for tests.
|
|
843
|
-
* @param {{ env?: Record<string, string|undefined>, ownerConfig?: object
|
|
853
|
+
* @param {{ env?: Record<string, string|undefined>, ownerConfig?: object,
|
|
854
|
+
* readOriginUrl?: (vaultDir: string) => string }} [opts.hostPaths]
|
|
844
855
|
* — forwarded verbatim to {@link parseSessionConfig}'s `hostPaths` DI seam (issue #653).
|
|
856
|
+
* The same ctx feeds {@link checkCanonicalVault}; `readOriginUrl` replaces its
|
|
857
|
+
* `git remote get-url origin` read (tests only — tmp vaults have no origin).
|
|
845
858
|
* Tests MUST pass a hermetic ctx (e.g. `{ env: {}, ownerConfig: undefined }`) when
|
|
846
859
|
* asserting a fixture's committed `vault-dir` — omitting it reads the REAL host
|
|
847
|
-
* `owner.yaml`, whose `paths.vault-dir` override (if set) wins over the fixture value
|
|
860
|
+
* `owner.yaml`, whose `vault-dirs:` cwd match or `paths.vault-dir` override (if set) wins over the fixture value
|
|
848
861
|
* and bleeds into the assertion (issue #783). Production callers omit this — the
|
|
849
862
|
* default (real owner.yaml resolution) is the correct host-local behavior there.
|
|
850
863
|
* @returns {Promise<{ result: { action: string, path?: string }, rows?: number,
|
|
@@ -861,10 +874,19 @@ async function mirrorBoardInner({ repoRoot, repos, explicitStatus, now = new Dat
|
|
|
861
874
|
}
|
|
862
875
|
|
|
863
876
|
// Read + parse Session Config. Any failure → silent no-op.
|
|
877
|
+
// The host context is resolved ONCE here and reused by the canonical-vault
|
|
878
|
+
// guard below: loadOwnerConfig() has no cache and its section-drop WARNs are
|
|
879
|
+
// not rate-limited, so a second load would print every WARN of a broken
|
|
880
|
+
// owner.yaml twice per board write (#1450 follow-up). The config file is read
|
|
881
|
+
// BEFORE owner.yaml — the order narrative-mirror.mjs shares (#1479); vault-dir
|
|
882
|
+
// precedence is applied in parseSessionConfig, so the order changes only
|
|
883
|
+
// whether a repo without a config file reads owner.yaml at all.
|
|
864
884
|
let config;
|
|
885
|
+
let hostCtx;
|
|
865
886
|
try {
|
|
866
887
|
const text = await readConfigFile(repoRoot);
|
|
867
|
-
|
|
888
|
+
hostCtx = hostPaths ?? loadHostPaths();
|
|
889
|
+
config = parseSessionConfig(text, { hostPaths: hostCtx });
|
|
868
890
|
} catch {
|
|
869
891
|
return { result: { action: 'skipped-vault-disabled' } };
|
|
870
892
|
}
|
|
@@ -886,6 +908,24 @@ async function mirrorBoardInner({ repoRoot, repos, explicitStatus, now = new Dat
|
|
|
886
908
|
return { result: { action: 'skipped-vault-disabled' } };
|
|
887
909
|
}
|
|
888
910
|
|
|
911
|
+
// Safety: the vault must be a canonical Meta-Vault (#1450) — the same guard
|
|
912
|
+
// vault-mirror applies, so a vault-dir that resolved to a FOREIGN vault can
|
|
913
|
+
// never receive the board. Reuses the `hostCtx` the config parse used, so a
|
|
914
|
+
// hermetic ctx never reads the real owner.yaml and owner.yaml is loaded at
|
|
915
|
+
// most once. Runs before the dry-run branch and the lock: fail-closed.
|
|
916
|
+
const canonical = checkCanonicalVault({
|
|
917
|
+
vaultDir: expandedVault,
|
|
918
|
+
ownerConfig: hostCtx.ownerConfig,
|
|
919
|
+
env: hostCtx.env ?? process.env,
|
|
920
|
+
readOriginUrl: hostCtx.readOriginUrl,
|
|
921
|
+
});
|
|
922
|
+
if (!canonical.ok) {
|
|
923
|
+
process.stderr.write(
|
|
924
|
+
`vault-status board: refusing to write — vault is not the canonical Meta-Vault (expected git origin ending in one of: ${canonical.expected.join(', ')}; got ${describeOriginForLog(canonical.got) || 'no git origin'})\n`,
|
|
925
|
+
);
|
|
926
|
+
return { result: { action: 'skipped-vault-not-canonical' } };
|
|
927
|
+
}
|
|
928
|
+
|
|
889
929
|
// `vault-name` (#660) overrides the git-derived repo slug for per-project
|
|
890
930
|
// vault namespacing. Without it `collectRows` falls back to
|
|
891
931
|
// `path.basename(repoRoot)`, so a repo whose directory name differs from its
|
|
@@ -1112,10 +1152,10 @@ async function mirrorBoardInner({ repoRoot, repos, explicitStatus, now = new Dat
|
|
|
1112
1152
|
* Public {@link mirrorBoardInner} wrapper that emits exactly ONE
|
|
1113
1153
|
* {@link BOARD_EVENT} per call — on EVERY path, no-ops included.
|
|
1114
1154
|
*
|
|
1115
|
-
* The wrapper exists so the emit cannot be forgotten: the
|
|
1116
|
-
* inside {@link mirrorBoardInner} (five `skipped-vault-disabled` guards
|
|
1117
|
-
* whatever {@link writeBoard}
|
|
1118
|
-
*
|
|
1155
|
+
* The wrapper exists so the emit cannot be forgotten: the seven return points
|
|
1156
|
+
* inside {@link mirrorBoardInner} (five `skipped-vault-disabled` guards, the
|
|
1157
|
+
* `skipped-vault-not-canonical` guard, plus whatever {@link writeBoard}
|
|
1158
|
+
* decides) all funnel through here, and so does the eighth someone adds next. Emitting per-return instead would leave each new
|
|
1119
1159
|
* early return silent by default — which is the exact defect being fixed.
|
|
1120
1160
|
*
|
|
1121
1161
|
* A THROW from the inner function is deliberately NOT converted into an event:
|
|
@@ -1276,7 +1316,8 @@ export function buildSweepRepos(candidates, { thisRepoRoot } = {}) {
|
|
|
1276
1316
|
* @param {object} [opts.deps] — injectable deps for {@link enumerateCandidates}
|
|
1277
1317
|
* (test seam: `readdirSync`, `existsSync`, `readLock`, `isLockLive`,
|
|
1278
1318
|
* `getCrossRepoProjects`, `validatePathInsideProject`, `now`).
|
|
1279
|
-
* @param {{ env?: Record<string, string|undefined>, ownerConfig?: object
|
|
1319
|
+
* @param {{ env?: Record<string, string|undefined>, ownerConfig?: object,
|
|
1320
|
+
* readOriginUrl?: (vaultDir: string) => string }} [opts.hostPaths]
|
|
1280
1321
|
* — forwarded verbatim to every {@link mirrorBoard} call below (both the happy-path
|
|
1281
1322
|
* and the enumeration-failure-fallback path), which in turn forwards it to
|
|
1282
1323
|
* {@link parseSessionConfig}'s `hostPaths` DI seam (issue #653/#783). See
|
|
@@ -44,6 +44,8 @@ import { validatePathInsideProject } from '../path-utils.mjs';
|
|
|
44
44
|
import { createSecretValueMasker } from '../secret-masker.mjs';
|
|
45
45
|
import { expandTilde } from '../common.mjs';
|
|
46
46
|
import { readVaultSlug } from '../vault-yaml.mjs';
|
|
47
|
+
import { checkCanonicalVault, describeOriginForLog } from '../named-vault-resolver.mjs';
|
|
48
|
+
import { loadHostPaths } from '../config/host-paths.mjs';
|
|
47
49
|
|
|
48
50
|
/** Frontmatter sentinel that identifies generator-owned narrative files. */
|
|
49
51
|
export const GENERATOR_MARKER = 'session-orchestrator-vault-status-narrative@1';
|
|
@@ -763,9 +765,10 @@ async function emitMaskerEvent({ repoRoot, needleCount, hits, dryRun }) {
|
|
|
763
765
|
* before any write.
|
|
764
766
|
*
|
|
765
767
|
* THIS FUNCTION WRITES TO THE OPERATOR'S REAL VAULT UNLESS YOU STOP IT. `vault-dir`
|
|
766
|
-
* resolves HOST-LOCALLY — `SO_VAULT_DIR` > `owner.yaml` `
|
|
767
|
-
* committed Session Config (`
|
|
768
|
-
* value in a CLAUDE.md you just wrote
|
|
768
|
+
* resolves HOST-LOCALLY — `SO_VAULT_DIR` > `owner.yaml` `vault-dirs:` cwd match >
|
|
769
|
+
* `owner.yaml` `paths.vault-dir` > committed Session Config (`resolveVaultDir` in
|
|
770
|
+
* `scripts/lib/config.mjs`) — so the literal value in a CLAUDE.md you just wrote
|
|
771
|
+
* into a tmp dir LOSES to every override. A
|
|
769
772
|
* probe that passes a synthetic `repoRoot` and nothing else has already written
|
|
770
773
|
* into the live vault once (#1025 review). Any caller that must not touch the real
|
|
771
774
|
* vault passes `hostPaths: { env: {}, ownerConfig: undefined }` (see below),
|
|
@@ -777,13 +780,16 @@ async function emitMaskerEvent({ repoRoot, needleCount, hits, dryRun }) {
|
|
|
777
780
|
* now?: Date,
|
|
778
781
|
* dryRun?: boolean,
|
|
779
782
|
* fs?: object,
|
|
780
|
-
* hostPaths?: { env?: Record<string, string|undefined>, ownerConfig?: object
|
|
783
|
+
* hostPaths?: { env?: Record<string, string|undefined>, ownerConfig?: object,
|
|
784
|
+
* readOriginUrl?: (vaultDir: string) => string },
|
|
781
785
|
* }} opts
|
|
782
786
|
* `hostPaths` is forwarded verbatim to {@link parseSessionConfig}'s `hostPaths` DI
|
|
783
|
-
* seam (issue #653).
|
|
787
|
+
* seam (issue #653). The same ctx feeds {@link checkCanonicalVault} (#1450);
|
|
788
|
+
* `readOriginUrl` replaces its `git remote get-url origin` read (tests only —
|
|
789
|
+
* tmp vaults have no origin). Tests MUST pass a hermetic ctx (e.g. `{ env: {}, ownerConfig:
|
|
784
790
|
* undefined }`) when asserting a fixture's committed `vault-dir` — omitting it reads
|
|
785
|
-
* the REAL host `owner.yaml`, whose `paths.vault-dir`
|
|
786
|
-
* fixture value and bleeds into the assertion (issue #783). Production callers omit
|
|
791
|
+
* the REAL host `owner.yaml`, whose `vault-dirs:` cwd match or `paths.vault-dir`
|
|
792
|
+
* override (if set) wins over the fixture value and bleeds into the assertion (issue #783). Production callers omit
|
|
787
793
|
* this — the default (real owner.yaml resolution) is the correct host-local behavior.
|
|
788
794
|
* @returns {Promise<{ action: string, path?: string }>}
|
|
789
795
|
*/
|
|
@@ -829,7 +835,7 @@ export async function mirrorNarrative(opts) {
|
|
|
829
835
|
* Split out so that the two emit sites in `mirrorNarrative` (the narrative
|
|
830
836
|
* event AND, since #1028, the masker event) each cover EVERY outcome from
|
|
831
837
|
* ONE call: a future early return added inside this function is telemetered
|
|
832
|
-
* by construction, whereas hand-placing an emit beside each of the
|
|
838
|
+
* by construction, whereas hand-placing an emit beside each of the eight
|
|
833
839
|
* `return`s makes "forgot the new one" the default failure. The `chars` companion travels beside the result
|
|
834
840
|
* rather than inside it because the returned object is a PUBLIC shape that
|
|
835
841
|
* callers (and tests) compare with `toEqual` — adding a key there would be an
|
|
@@ -855,10 +861,19 @@ async function runNarrativeMirror(opts) {
|
|
|
855
861
|
const masker = createSecretValueMasker(process.env);
|
|
856
862
|
|
|
857
863
|
// Read Session Config (CLAUDE.md / AGENTS.md) and resolve vault settings.
|
|
864
|
+
// The host context is loaded ONCE and shared by the Session Config parse and
|
|
865
|
+
// the canonical-vault guard below, so owner.yaml is read a single time per run.
|
|
866
|
+
// Order (#1479, same as board-writer.mjs): config file first, owner.yaml
|
|
867
|
+
// second. The `vault-dir` precedence (SO_VAULT_DIR > owner.yaml > committed)
|
|
868
|
+
// is applied inside parseSessionConfig, after both are in hand, so the order
|
|
869
|
+
// cannot change the resolved dir; it only spares a repo without a config file
|
|
870
|
+
// the owner.yaml read and its section-drop WARNs on a run that skips anyway.
|
|
858
871
|
let config;
|
|
872
|
+
let hostCtx;
|
|
859
873
|
try {
|
|
860
874
|
const configText = await readConfigFile(repoRoot);
|
|
861
|
-
|
|
875
|
+
hostCtx = hostPaths ?? loadHostPaths();
|
|
876
|
+
config = parseSessionConfig(configText, { hostPaths: hostCtx });
|
|
862
877
|
} catch {
|
|
863
878
|
return { result: { action: 'skipped-vault-disabled' }, needleCount: masker.needleCount, hits: 0, dryRun };
|
|
864
879
|
}
|
|
@@ -900,6 +915,22 @@ async function runNarrativeMirror(opts) {
|
|
|
900
915
|
}
|
|
901
916
|
|
|
902
917
|
const vaultDir = path.resolve(expandTilde(rawVaultDir));
|
|
918
|
+
|
|
919
|
+
// #1450: same shared guard as board-writer.mjs and scripts/vault-mirror.mjs —
|
|
920
|
+
// refuse a vault-dir whose git origin is not a canonical Meta-Vault. The
|
|
921
|
+
// stderr line names the expected suffixes and the origin, never the vault
|
|
922
|
+
// path itself.
|
|
923
|
+
const canonical = checkCanonicalVault({
|
|
924
|
+
vaultDir,
|
|
925
|
+
ownerConfig: hostCtx.ownerConfig,
|
|
926
|
+
env: hostCtx.env ?? process.env,
|
|
927
|
+
readOriginUrl: hostCtx.readOriginUrl,
|
|
928
|
+
});
|
|
929
|
+
if (!canonical.ok) {
|
|
930
|
+
process.stderr.write(`narrative-mirror: refusing to write — vault is not the canonical Meta-Vault (expected git origin ending in one of: ${canonical.expected.join(', ')}; got ${describeOriginForLog(canonical.got) || 'no git origin'})\n`);
|
|
931
|
+
return { result: { action: 'skipped-vault-not-canonical' }, needleCount: masker.needleCount, hits: 0, dryRun };
|
|
932
|
+
}
|
|
933
|
+
|
|
903
934
|
const candidateSlug = subjectToSlug(repoName) || 'unknown';
|
|
904
935
|
// A declared `.vault.yaml` slug needs no healing — it IS the canonical folder
|
|
905
936
|
// name, so resolveLooseSlug is SKIPPED for it. Running the healer over it
|
|
@@ -160,6 +160,14 @@ export function stripUrlCredentials(url) {
|
|
|
160
160
|
* `--repo <spec>` override path, which bypasses the source-level strip in
|
|
161
161
|
* {@link resolveRawRemoteUrl}. Never throws; a non-string returns unchanged.
|
|
162
162
|
*
|
|
163
|
+
* Scope is CREDENTIALS only, as {@link userinfoIsCredential} classifies them.
|
|
164
|
+
* A bare login on a non-HTTP scheme (`ssh://alice@host/…`) is left verbatim,
|
|
165
|
+
* the URL path is never touched (`ssh://***@host/home/alice/vault` keeps the
|
|
166
|
+
* remote home dir), and the scp-like `user@host:path` form is not matched at
|
|
167
|
+
* all. So this is NOT the scp-consistent origin renderer: `describeOriginForLog`
|
|
168
|
+
* (`named-vault-resolver.mjs`) redacts every non-`git` login in both forms and
|
|
169
|
+
* hides a home-dir path (#1490) — use that for a git origin in a log line.
|
|
170
|
+
*
|
|
163
171
|
* @param {string} text
|
|
164
172
|
* @returns {string}
|
|
165
173
|
*/
|
|
@@ -21,6 +21,14 @@
|
|
|
21
21
|
* exists to be.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Re-exported, not re-declared: the value lives in `process-group.mjs` since
|
|
26
|
+
* 773a994b, but this module exported it in 5.3.x and the package ships
|
|
27
|
+
* `scripts/` with no `exports` map — so a deep importer still links against
|
|
28
|
+
* this name. One definition, two import paths.
|
|
29
|
+
*/
|
|
30
|
+
export { DEFAULT_KILL_GRACE_MS } from '../process-group.mjs';
|
|
31
|
+
|
|
24
32
|
/**
|
|
25
33
|
* Roles that may NEVER run on a foreign model. Copied verbatim from
|
|
26
34
|
* account-switch `tools/routing/routing.yaml` § never_foreign (read 2026-08-25).
|
|
@@ -44,22 +52,6 @@ export const NEVER_FOREIGN_ROLES = Object.freeze([
|
|
|
44
52
|
* manufactures timeouts and reads as model failure. */
|
|
45
53
|
export const DEFAULT_TIMEOUT_SEC = 900;
|
|
46
54
|
|
|
47
|
-
/**
|
|
48
|
-
* Grace period between SIGTERM and SIGKILL, in ms.
|
|
49
|
-
*
|
|
50
|
-
* `child.kill('SIGTERM')` is a REQUEST: a child that installs a handler and
|
|
51
|
-
* ignores it never emits `close`, and this module's only resolve paths are
|
|
52
|
-
* `close` and `error` — so without escalation a wedged `cursor-agent` hangs
|
|
53
|
-
* `dispatchForeign` forever and takes the whole wave with it. SIGKILL is not
|
|
54
|
-
* catchable, so the escalation always terminates.
|
|
55
|
-
*
|
|
56
|
-
* Named ceiling (BV-004): 10 s is enough for a cooperative child to flush its
|
|
57
|
-
* stream-json tail and exit. Revisit if a well-behaved `cursor-agent` is ever
|
|
58
|
-
* measured needing longer than that to shut down — raise the constant, never
|
|
59
|
-
* drop the escalation.
|
|
60
|
-
*/
|
|
61
|
-
export const DEFAULT_KILL_GRACE_MS = 10_000;
|
|
62
|
-
|
|
63
55
|
/** A runId names a directory and a log file. Anything outside this alphabet —
|
|
64
56
|
* or the two relative-path literals the alphabet happens to admit — can escape
|
|
65
57
|
* the parent it is joined to. */
|
|
@@ -90,8 +82,8 @@ export function isNeverForeignRole(role) {
|
|
|
90
82
|
*
|
|
91
83
|
* The escalation is the difference between a bounded dispatch and a hung wave:
|
|
92
84
|
* this function's only resolve paths are the child's `close` and `error`
|
|
93
|
-
* events, so a child that ignores SIGTERM never lets the promise settle.
|
|
94
|
-
*
|
|
85
|
+
* events, so a child that ignores SIGTERM never lets the promise settle. The
|
|
86
|
+
* callers pass `DEFAULT_KILL_GRACE_MS` from `process-group.mjs`.
|
|
95
87
|
*
|
|
96
88
|
* @returns {Promise<{capture: string, exitCode: number|null, signal: string|null,
|
|
97
89
|
* timerFired: boolean, killSignals: string[], errorCode: string|null}>}
|
|
@@ -54,8 +54,8 @@ import path from 'node:path';
|
|
|
54
54
|
|
|
55
55
|
import { emitEvent, sessionAttribution } from '../events.mjs';
|
|
56
56
|
import { isPathInside } from '../path-utils.mjs';
|
|
57
|
+
import { DEFAULT_KILL_GRACE_MS } from '../process-group.mjs';
|
|
57
58
|
import {
|
|
58
|
-
DEFAULT_KILL_GRACE_MS,
|
|
59
59
|
DEFAULT_TIMEOUT_SEC,
|
|
60
60
|
isNeverForeignRole,
|
|
61
61
|
isSafeRunId,
|
|
@@ -34,8 +34,8 @@ import path from 'node:path';
|
|
|
34
34
|
|
|
35
35
|
import { emitEvent, sessionAttribution } from '../events.mjs';
|
|
36
36
|
import { isPathInside } from '../path-utils.mjs';
|
|
37
|
+
import { DEFAULT_KILL_GRACE_MS } from '../process-group.mjs';
|
|
37
38
|
import {
|
|
38
|
-
DEFAULT_KILL_GRACE_MS,
|
|
39
39
|
DEFAULT_TIMEOUT_SEC,
|
|
40
40
|
isNeverForeignRole,
|
|
41
41
|
isSafeRunId,
|