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
package/scripts/vault-mirror.mjs
CHANGED
|
@@ -8,23 +8,40 @@
|
|
|
8
8
|
* CLI usage:
|
|
9
9
|
* node vault-mirror.mjs --vault-dir <path> --source <jsonl-path> --kind <learning|session>
|
|
10
10
|
* [--dry-run] [--strict-schema] [--no-commit] [--force]
|
|
11
|
-
* [--session-id <id>]
|
|
11
|
+
* [--session-id <id>] [--repo-root <path>] [--vault-name <name>]
|
|
12
12
|
* [--quality-min-narrative-chars <int>] (sessions only; default 400)
|
|
13
13
|
* [--quality-min-confidence <float>] (learnings only; default 0.5)
|
|
14
14
|
*
|
|
15
15
|
* Exit codes:
|
|
16
16
|
* 0 — success (including idempotent no-op)
|
|
17
17
|
* 1 — validation error (malformed JSON line, bad slug, etc.)
|
|
18
|
-
* 2 — filesystem error
|
|
18
|
+
* 2 — filesystem error, or a pre-loop refusal (missing vault-dir / source,
|
|
19
|
+
* non-canonical vault, `--source` belonging to a different repo than
|
|
20
|
+
* `--repo-root` — #1503)
|
|
19
21
|
*
|
|
20
22
|
* Output: one JSON line per action on stdout:
|
|
21
|
-
* {"action":"created|updated|skipped-noop|skipped-handwritten|skipped-collision-resolved|skipped-invalid|skipped-quality-low","path":"...","kind":"...","id":"..."}
|
|
23
|
+
* {"action":"created|updated|skipped-noop|skipped-handwritten|skipped-collision-resolved|skipped-foreign-owner|skipped-invalid|skipped-quality-low|skipped-abandoned|skipped-duplicate-session","path":"...","kind":"...","id":"..."}
|
|
22
24
|
*
|
|
23
25
|
* Idempotency rules:
|
|
24
26
|
* 1. File does not exist → create.
|
|
25
27
|
* 2. File exists, has _generator marker, id matches → overwrite only if updated would advance; else skipped-noop.
|
|
26
28
|
* 3. File exists, lacks _generator → skip (hand-written). Log to stderr.
|
|
27
29
|
* 4. File exists, has _generator, id differs → collision-disambiguate by appending -<first8 of uuid>.
|
|
30
|
+
* (Sessions have no disambiguation: a different-id note is skipped-foreign-owner.)
|
|
31
|
+
* 5. File exists, has _generator, but its `source-record` names a different record
|
|
32
|
+
* than this write (#1503): learnings → collision-disambiguate as in rule 4;
|
|
33
|
+
* sessions → skipped-foreign-owner unless the key occurs in this --source ledger.
|
|
34
|
+
* A leak in such a foreign note is masked IN PLACE (owner, content and
|
|
35
|
+
* source-record kept; `healed_leak: true` on the action) — never rewritten
|
|
36
|
+
* from this record. If the masked frontmatter would not parse, it is left
|
|
37
|
+
* unchanged with a stderr WARN.
|
|
38
|
+
*
|
|
39
|
+
* Repo root (#1503): the namespace is the identity of the repo the --source
|
|
40
|
+
* ledger belongs to — `--repo-root` when given, else the project holding
|
|
41
|
+
* --source (`<root>/.orchestrator/metrics/`), else the git repo containing it,
|
|
42
|
+
* else the cwd (a loose source outside any git repo). It is never the cwd
|
|
43
|
+
* when the source sits in a repo: running from another repo's checkout used to
|
|
44
|
+
* write one project's records over another project's notes.
|
|
28
45
|
*
|
|
29
46
|
* Quality gate (PRD F1.2):
|
|
30
47
|
* Learnings with confidence < --quality-min-confidence emit `skipped-quality-low`.
|
|
@@ -48,22 +65,28 @@
|
|
|
48
65
|
* Part of session-orchestrator vault-mirror (Issue #14).
|
|
49
66
|
*/
|
|
50
67
|
|
|
51
|
-
import {
|
|
52
|
-
import {
|
|
53
|
-
import { resolve } from 'node:path';
|
|
68
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
69
|
+
import { basename, dirname, resolve } from 'node:path';
|
|
54
70
|
import { createInterface } from 'node:readline';
|
|
55
71
|
import { createReadStream } from 'node:fs';
|
|
56
72
|
|
|
57
|
-
import {
|
|
73
|
+
import {
|
|
74
|
+
processLearning,
|
|
75
|
+
processSession,
|
|
76
|
+
getMaskerStats,
|
|
77
|
+
emitAction,
|
|
78
|
+
} from './lib/vault-mirror/process.mjs';
|
|
58
79
|
import { emitMirrorEvent, emitMirrorRunEvent } from './lib/vault-mirror/telemetry.mjs';
|
|
59
80
|
import { emitEvent } from './lib/events.mjs';
|
|
60
81
|
import { autoCommitVaultMirror } from './lib/vault-mirror/auto-commit.mjs';
|
|
61
82
|
import { parseColumnFlags, CliFlagError } from './lib/cli-flags.mjs';
|
|
62
83
|
import { resolveRepoNamespace } from './lib/vault-mirror/namespace.mjs';
|
|
63
|
-
import {
|
|
84
|
+
import { sessionSourceRecord } from './lib/vault-mirror/render-sessions.mjs';
|
|
85
|
+
import { checkCanonicalVault, describeOriginForLog, findRepoRoot, normalizeRemote } from './lib/named-vault-resolver.mjs';
|
|
64
86
|
import { loadOwnerConfig } from './lib/owner-yaml.mjs';
|
|
65
87
|
import { canonicalizeSessions } from './lib/sessions-canonical.mjs';
|
|
66
88
|
import { isMainModule } from './lib/is-main-module.mjs';
|
|
89
|
+
import { expandTilde, redactHomeDir } from './lib/common.mjs';
|
|
67
90
|
|
|
68
91
|
// ── Canonical-vault helpers (#600 D2 / #607 D2) ────────────────────────────────
|
|
69
92
|
// These are module-level (above the CLI bootstrap) so the module is import-safe
|
|
@@ -85,7 +108,7 @@ export function _resolveCanonicalSuffix(envValue) {
|
|
|
85
108
|
return envValue && envValue.trim() ? envValue.trim() : '/agents/vault';
|
|
86
109
|
}
|
|
87
110
|
|
|
88
|
-
// Kept for documentation; the guard now uses
|
|
111
|
+
// Kept for documentation; the guard now uses checkCanonicalVault() which
|
|
89
112
|
// generalises this to N suffixes. The `_` prefix satisfies the no-unused-vars rule.
|
|
90
113
|
const _CANONICAL_VAULT_SUFFIX = _resolveCanonicalSuffix(
|
|
91
114
|
process.env.VAULT_MIRROR_CANONICAL_SUFFIX,
|
|
@@ -93,18 +116,14 @@ const _CANONICAL_VAULT_SUFFIX = _resolveCanonicalSuffix(
|
|
|
93
116
|
|
|
94
117
|
/**
|
|
95
118
|
* Normalize a git remote URL to a host/path tail for canonical-suffix matching
|
|
96
|
-
* (#607 D2 — exported for unit tests).
|
|
97
|
-
*
|
|
119
|
+
* (#607 D2 — exported for unit tests). Since #1450 the ONE implementation is
|
|
120
|
+
* `normalizeRemote` in lib/named-vault-resolver.mjs, shared with the board
|
|
121
|
+
* writer's guard; this name is kept for the existing test imports.
|
|
98
122
|
* @param {string} url
|
|
99
123
|
* @returns {string}
|
|
100
124
|
*/
|
|
101
125
|
export function _normalizeRemote(url) {
|
|
102
|
-
return
|
|
103
|
-
.trim()
|
|
104
|
-
.replace(/\.git$/, '')
|
|
105
|
-
.replace(/^git@([^:]+):/, '$1/')
|
|
106
|
-
.replace(/^[a-z]+:\/\//, '')
|
|
107
|
-
.replace(/\/+$/, '');
|
|
126
|
+
return normalizeRemote(url);
|
|
108
127
|
}
|
|
109
128
|
|
|
110
129
|
// ── Mirror telemetry (#1116) ──────────────────────────────────────────────────
|
|
@@ -130,9 +149,10 @@ export function _normalizeRemote(url) {
|
|
|
130
149
|
// is always measured.
|
|
131
150
|
// - Same ledger as the masker emit: `emitEvent` is called 2-arg so both events
|
|
132
151
|
// from one run resolve the SAME destination (`SO_PROJECT_DIR`, i.e.
|
|
133
|
-
// `CLAUDE_PROJECT_DIR` or the CWD walk-up).
|
|
134
|
-
//
|
|
135
|
-
//
|
|
152
|
+
// `CLAUDE_PROJECT_DIR` or the CWD walk-up). The repo root that decides the
|
|
153
|
+
// vault NAMESPACE (#1503: `--repo-root`, else derived from `--source`) is
|
|
154
|
+
// deliberately NOT used for the ledger — routing telemetry by it would split
|
|
155
|
+
// a single run's records across two ledgers.
|
|
136
156
|
//
|
|
137
157
|
// #1147 moved both emitters into `scripts/lib/vault-mirror/telemetry.mjs` and
|
|
138
158
|
// widened the coverage from "the two skipped-invalid branches" to "every entry,
|
|
@@ -185,13 +205,14 @@ if (_isDirectInvocation) {
|
|
|
185
205
|
kind: null,
|
|
186
206
|
'session-id': null,
|
|
187
207
|
'vault-name': null,
|
|
208
|
+
'repo-root': null,
|
|
188
209
|
'quality-min-narrative-chars': null,
|
|
189
210
|
'quality-min-confidence': null,
|
|
190
211
|
},
|
|
191
212
|
});
|
|
192
213
|
} catch (err) {
|
|
193
214
|
if (err instanceof CliFlagError) {
|
|
194
|
-
process.stderr.write(`vault-mirror: ${err.message}\n`);
|
|
215
|
+
process.stderr.write(`vault-mirror: ${redactHomeDir(err.message)}\n`);
|
|
195
216
|
process.exit(1);
|
|
196
217
|
}
|
|
197
218
|
throw err;
|
|
@@ -205,7 +226,7 @@ if (flagValues.help === true) {
|
|
|
205
226
|
[
|
|
206
227
|
'Usage: node vault-mirror.mjs --vault-dir <path> --source <jsonl-path> --kind <learning|session>',
|
|
207
228
|
' [--dry-run] [--strict-schema] [--no-commit] [--force]',
|
|
208
|
-
' [--session-id <id>]',
|
|
229
|
+
' [--session-id <id>] [--repo-root <path>] [--vault-name <name>]',
|
|
209
230
|
' [--quality-min-narrative-chars <int>]',
|
|
210
231
|
' [--quality-min-confidence <float>]',
|
|
211
232
|
'',
|
|
@@ -223,6 +244,11 @@ if (flagValues.help === true) {
|
|
|
223
244
|
' When set, mirrors write under <vault-name>/ instead of the',
|
|
224
245
|
' git-derived repo identifier. Maps to vault-integration.vault-name',
|
|
225
246
|
' in Session Config. Sanitised to a lowercase kebab slug.',
|
|
247
|
+
' --repo-root <path> The repo whose ledger --source is (#1503). Its .vault.yaml',
|
|
248
|
+
' slug / git remote decides the vault namespace. Default: the',
|
|
249
|
+
' project holding --source (<root>/.orchestrator/metrics/), else',
|
|
250
|
+
' the git repo containing it, else the cwd. Refused (exit 2,',
|
|
251
|
+
' "source-repo-mismatch") when --source lies in a different repo.',
|
|
226
252
|
' --quality-min-narrative-chars <int> Sessions: minimum rendered-narrative length (default 400).',
|
|
227
253
|
' Entries below the threshold emit "skipped-quality-low".',
|
|
228
254
|
' --quality-min-confidence <float> Learnings: minimum confidence threshold (default 0.5).',
|
|
@@ -235,7 +261,11 @@ if (flagValues.help === true) {
|
|
|
235
261
|
process.exit(0);
|
|
236
262
|
}
|
|
237
263
|
|
|
238
|
-
|
|
264
|
+
// Expand a leading `~` before anything touches the path: owner.yaml `vault-dirs:` /
|
|
265
|
+
// `paths.vault-dir` and the committed Session Config all accept `~/…`, and a quoted
|
|
266
|
+
// `--vault-dir "~/…"` reaches us literally. `resolve('~/x')` would yield `<cwd>/~/x`
|
|
267
|
+
// and the existsSync check below would abort with `missing-vault-dir` (agents/vault#319).
|
|
268
|
+
const vaultDir = expandTilde(flagValues['vault-dir']);
|
|
239
269
|
const source = flagValues.source;
|
|
240
270
|
const kind = flagValues.kind;
|
|
241
271
|
const dryRun = flagValues['dry-run'] === true;
|
|
@@ -244,6 +274,7 @@ const noCommit = flagValues['no-commit'] === true;
|
|
|
244
274
|
const force = flagValues.force === true;
|
|
245
275
|
const sessionIdArg = flagValues['session-id'];
|
|
246
276
|
const vaultName = flagValues['vault-name'] ?? null;
|
|
277
|
+
const repoRootArg = flagValues['repo-root'] ?? null;
|
|
247
278
|
|
|
248
279
|
// Quality-gate thresholds (PRD F1.2). Parse as numbers; reject malformed input
|
|
249
280
|
// loudly so CI cannot accidentally pass a string ("400px") and silently fall
|
|
@@ -284,7 +315,7 @@ const qualityMinConfidence = parseFloatFlag(
|
|
|
284
315
|
|
|
285
316
|
if (!vaultDir || !source || !kind) {
|
|
286
317
|
process.stderr.write(
|
|
287
|
-
'Usage: node vault-mirror.mjs --vault-dir <path> --source <jsonl-path> --kind <learning|session> [--dry-run] [--strict-schema] [--no-commit] [--force] [--session-id <id>] [--quality-min-narrative-chars <int>] [--quality-min-confidence <float>]\n',
|
|
318
|
+
'Usage: node vault-mirror.mjs --vault-dir <path> --source <jsonl-path> --kind <learning|session> [--dry-run] [--strict-schema] [--no-commit] [--force] [--session-id <id>] [--repo-root <path>] [--vault-name <name>] [--quality-min-narrative-chars <int>] [--quality-min-confidence <float>]\n',
|
|
288
319
|
);
|
|
289
320
|
process.exit(1);
|
|
290
321
|
}
|
|
@@ -323,16 +354,62 @@ if (kind !== 'learning' && kind !== 'session') {
|
|
|
323
354
|
// imports) so they are import-safe and unit-testable; see _resolveCanonicalSuffix
|
|
324
355
|
// / _normalizeRemote there (#607 D2).
|
|
325
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Render a CLI path flag for a stderr line as its basename only (#1479).
|
|
359
|
+
*
|
|
360
|
+
* This CLI's stderr lands in CI logs and session transcripts, and an absolute
|
|
361
|
+
* `--vault-dir` / `--source` carries the operator's home directory — the
|
|
362
|
+
* owner-leakage class `.claude/rules/security.md` § Owner-Privacy guards. The
|
|
363
|
+
* basename still says WHICH value was wrong; the caller passed the full path
|
|
364
|
+
* and already knows it.
|
|
365
|
+
* @param {string} p
|
|
366
|
+
* @returns {string}
|
|
367
|
+
*/
|
|
368
|
+
function pathForLog(p) {
|
|
369
|
+
return `.../${basename(String(p ?? ''))}`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* The project a ledger belongs to (#1503): the directory that holds it under
|
|
374
|
+
* the `.orchestrator/metrics/` convention, else the git repo containing it,
|
|
375
|
+
* else `null`.
|
|
376
|
+
*
|
|
377
|
+
* The convention comes FIRST because the git toplevel is the wrong answer for
|
|
378
|
+
* a project nested inside a repo — a monorepo package with its own
|
|
379
|
+
* `.orchestrator/` and `.vault.yaml` (measured: `--repo-root <package>` was
|
|
380
|
+
* refused against the monorepo's namespace, and without the flag the package's
|
|
381
|
+
* notes moved to the monorepo's folder). Every producer writes the ledgers to
|
|
382
|
+
* `<project>/.orchestrator/metrics/`, which is also exactly the directory whose
|
|
383
|
+
* `.vault.yaml` named the namespace before #1503 (callers ran from it).
|
|
384
|
+
*
|
|
385
|
+
* Named ceiling: a ledger stored under some project's `.orchestrator/metrics/`
|
|
386
|
+
* is attributed to that project whoever wrote its records — a foreign ledger
|
|
387
|
+
* COPIED there is not detected here; the session `source-record` guard in
|
|
388
|
+
* process.mjs is the remaining defence for that shape.
|
|
389
|
+
*
|
|
390
|
+
* @param {string} realSource - the source path after `realpathSync`.
|
|
391
|
+
* @returns {string|null}
|
|
392
|
+
*/
|
|
393
|
+
function ledgerOwnerRoot(realSource) {
|
|
394
|
+
const metricsDir = dirname(realSource);
|
|
395
|
+
const orchestratorDir = dirname(metricsDir);
|
|
396
|
+
if (basename(metricsDir) === 'metrics' && basename(orchestratorDir) === '.orchestrator') {
|
|
397
|
+
return dirname(orchestratorDir);
|
|
398
|
+
}
|
|
399
|
+
return findRepoRoot(dirname(realSource));
|
|
400
|
+
}
|
|
401
|
+
|
|
326
402
|
// ── Run-level accounting + run close-out (#1147) ──────────────────────────────
|
|
327
403
|
//
|
|
328
404
|
// Deliberately OUTSIDE main(): the run event's whole contract is that it is
|
|
329
405
|
// written ONCE PER RUN and that its ABSENCE is the broken-emitter signal
|
|
330
|
-
// (HR-105).
|
|
331
|
-
// the top of main (missing vault-dir, non-canonical vault, missing source
|
|
332
|
-
// malformed-JSON abort and the filesystem-error
|
|
333
|
-
// `process.exit`, which no `finally` and no
|
|
334
|
-
// top-level `main().catch`, which runs in a
|
|
335
|
-
// exist. Keeping the counters and the
|
|
406
|
+
// (HR-105). Seven exits bypass main's normal tail — the four PRE-LOOP aborts at
|
|
407
|
+
// the top of main (missing vault-dir, non-canonical vault, missing source,
|
|
408
|
+
// source/repo-root mismatch), the malformed-JSON abort and the filesystem-error
|
|
409
|
+
// abort inside the loop (all six `process.exit`, which no `finally` and no
|
|
410
|
+
// `catch` can intercept), and the top-level `main().catch`, which runs in a
|
|
411
|
+
// scope where main's locals no longer exist. Keeping the counters and the
|
|
412
|
+
// emitter out here is what lets all seven
|
|
336
413
|
// close the run out through ONE function instead of each re-deriving the
|
|
337
414
|
// payload.
|
|
338
415
|
const runState = {
|
|
@@ -361,10 +438,10 @@ const tally = (action) => {
|
|
|
361
438
|
* counted were the ones that vanished from the ledger, in the one shape
|
|
362
439
|
* ("no record") that the docstring reserves for a broken emitter.
|
|
363
440
|
*
|
|
364
|
-
* @param {'missing-vault-dir'|'vault-not-canonical'|'missing-source'|'malformed-json'|'filesystem-error'|'unexpected-error'} [aborted]
|
|
441
|
+
* @param {'missing-vault-dir'|'vault-not-canonical'|'missing-source'|'source-repo-mismatch'|'malformed-json'|'filesystem-error'|'unexpected-error'} [aborted]
|
|
365
442
|
* Omitted on a complete run. When present it LABELS the counters as partial:
|
|
366
443
|
* every line after the abort was never attempted, so the classes no longer
|
|
367
|
-
* partition `total`. On the
|
|
444
|
+
* partition `total`. On the PRE-LOOP values (#1151, #1503) nothing was
|
|
368
445
|
* attempted at all — `total` is a measured 0 and the label is what separates
|
|
369
446
|
* "never started" from "ran over an empty source".
|
|
370
447
|
* @returns {Promise<void>}
|
|
@@ -422,7 +499,7 @@ async function finishRun(aborted) {
|
|
|
422
499
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
423
500
|
|
|
424
501
|
async function main() {
|
|
425
|
-
// The
|
|
502
|
+
// The PRE-LOOP aborts below (#1151, #1503) close the run out through the same
|
|
426
503
|
// `finishRun` every other exit uses. They are the runs that never reached
|
|
427
504
|
// their first entry — a bad vault-dir, a wrong vault, a missing source — and
|
|
428
505
|
// until now they were the only outcomes that left NO record at all, which is
|
|
@@ -430,36 +507,107 @@ async function main() {
|
|
|
430
507
|
// are all `0`, and `aborted` is what makes that zero readable as "never
|
|
431
508
|
// started" rather than "ran over an empty source".
|
|
432
509
|
if (!existsSync(resolve(vaultDir))) {
|
|
433
|
-
process.stderr.write(`vault-mirror: vault-dir not found: ${vaultDir}\n`);
|
|
510
|
+
process.stderr.write(`vault-mirror: vault-dir not found: ${pathForLog(vaultDir)}\n`);
|
|
434
511
|
await finishRun('missing-vault-dir');
|
|
435
512
|
process.exit(2);
|
|
436
513
|
}
|
|
437
514
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
await finishRun('vault-not-canonical');
|
|
453
|
-
process.exit(2);
|
|
454
|
-
}
|
|
515
|
+
// The check itself — origin probe, suffix match, the SKIP_CANONICAL_CHECK
|
|
516
|
+
// escape hatch — is the shared `checkCanonicalVault` (#1450), the same one the
|
|
517
|
+
// board writer applies, so the two vault writers cannot drift apart again.
|
|
518
|
+
const canonical = checkCanonicalVault({
|
|
519
|
+
vaultDir: resolve(vaultDir),
|
|
520
|
+
ownerConfig: loadOwnerConfig().config,
|
|
521
|
+
env: process.env,
|
|
522
|
+
});
|
|
523
|
+
if (!canonical.ok) {
|
|
524
|
+
process.stderr.write(
|
|
525
|
+
`vault-mirror: refusing to mirror — 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`,
|
|
526
|
+
);
|
|
527
|
+
await finishRun('vault-not-canonical');
|
|
528
|
+
process.exit(2);
|
|
455
529
|
}
|
|
456
530
|
|
|
457
531
|
if (!existsSync(resolve(source))) {
|
|
458
|
-
process.stderr.write(`vault-mirror: source file not found: ${source}\n`);
|
|
532
|
+
process.stderr.write(`vault-mirror: source file not found: ${pathForLog(source)}\n`);
|
|
459
533
|
await finishRun('missing-source');
|
|
460
534
|
process.exit(2);
|
|
461
535
|
}
|
|
462
536
|
|
|
537
|
+
// ── Repo root + source/repo-root agreement (#1503) ──────────────────────────
|
|
538
|
+
//
|
|
539
|
+
// The namespace every note is written under is the identity of `repoRoot`.
|
|
540
|
+
// Until #1503 that was hard-wired to the invocation cwd, while `--source` was
|
|
541
|
+
// free to name ANY ledger: a session that ran this CLI from another repo's
|
|
542
|
+
// checkout with `--source <ThisRepo>/.orchestrator/metrics/sessions.jsonl`
|
|
543
|
+
// wrote 130 of this repo's records under the OTHER repo's namespace and
|
|
544
|
+
// overwrote 3 of its notes — slug and session-id collisions are routine across
|
|
545
|
+
// repos. The fix is to stop asking the cwd:
|
|
546
|
+
// 1. `--repo-root` when given — the caller states which repo it mirrors;
|
|
547
|
+
// 2. else the ledger's own project — see {@link ledgerOwnerRoot};
|
|
548
|
+
// 3. else the cwd — only for a source that is neither a project ledger nor
|
|
549
|
+
// inside a git repo (a copied ledger, the test suites' mkdtemp sources),
|
|
550
|
+
// where nothing better is knowable.
|
|
551
|
+
// Step 2's answer is also what an explicit `--repo-root` is checked against.
|
|
552
|
+
//
|
|
553
|
+
// The ledger's REAL path decides, not the path it was handed under: a symlink
|
|
554
|
+
// inside repo A that points at repo B's ledger is B's ledger. `realpathSync`
|
|
555
|
+
// falls back to the lexical path only when it throws (the file was just
|
|
556
|
+
// proven to exist, so that is a race or a permission edge, not a normal case).
|
|
557
|
+
// The same holds for `--repo-root`: a remote-less, slug-less repo is named by
|
|
558
|
+
// its directory, so a root reached through a symlink would otherwise carry the
|
|
559
|
+
// LINK's name and be refused against its own ledger.
|
|
560
|
+
const realOrLexical = (p) => {
|
|
561
|
+
try {
|
|
562
|
+
return realpathSync(p);
|
|
563
|
+
} catch {
|
|
564
|
+
return p;
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
const sourceRealPath = realOrLexical(resolve(source));
|
|
568
|
+
const sourceRepoRoot = ledgerOwnerRoot(sourceRealPath);
|
|
569
|
+
const repoRoot = repoRootArg
|
|
570
|
+
? realOrLexical(resolve(expandTilde(repoRootArg)))
|
|
571
|
+
: (sourceRepoRoot ?? process.cwd());
|
|
572
|
+
|
|
573
|
+
// A source with no owning project cannot be checked against anything: the
|
|
574
|
+
// namespace then rests on the cwd or on an unverifiable `--repo-root`, and a
|
|
575
|
+
// ledger copied out of ANOTHER repo would be written over this repo's notes
|
|
576
|
+
// without a refusal. Say so once — the run proceeds, because a loose ledger
|
|
577
|
+
// (a copy, a test fixture) is a legitimate input.
|
|
578
|
+
if (sourceRepoRoot === null) {
|
|
579
|
+
const ns = resolveRepoNamespace({ vaultName, repoRoot });
|
|
580
|
+
process.stderr.write(
|
|
581
|
+
repoRootArg
|
|
582
|
+
? `WARN vault-mirror: --source ${pathForLog(source)} is neither a project ledger (.orchestrator/metrics/) nor inside a git repo, so --repo-root cannot be checked against it; writing under namespace '${ns}'\n`
|
|
583
|
+
: `WARN vault-mirror: --source ${pathForLog(source)} is neither a project ledger (.orchestrator/metrics/) nor inside a git repo; the namespace '${ns}' comes from the cwd — pass --repo-root to state which repo this ledger belongs to\n`,
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Fail closed when the two disagree: an explicit `--repo-root` whose identity
|
|
588
|
+
// differs from the repo the ledger lives in is exactly the incident shape
|
|
589
|
+
// (callers pass `--repo-root "$PWD"`, so a run from the wrong checkout lands
|
|
590
|
+
// here instead of in another project's notes). NAMESPACES are compared, not
|
|
591
|
+
// paths — a worktree and its main checkout are different directories that
|
|
592
|
+
// share one `.vault.yaml` slug and one remote. `vaultName` is left out on
|
|
593
|
+
// purpose: it overrides the FOLDER, and the question here is whether the
|
|
594
|
+
// ledger belongs to the repo being mirrored. Named ceiling: two owner-leaky
|
|
595
|
+
// repos without a pseudonym-map entry both resolve to `redacted-repo` and pass
|
|
596
|
+
// this check — they already share that one folder (#725 D5), so the refusal
|
|
597
|
+
// could not keep them apart anyway.
|
|
598
|
+
if (repoRootArg && sourceRepoRoot !== null) {
|
|
599
|
+
const repoNs = resolveRepoNamespace({ repoRoot });
|
|
600
|
+
const sourceNs = resolveRepoNamespace({ repoRoot: sourceRepoRoot });
|
|
601
|
+
if (repoNs !== sourceNs) {
|
|
602
|
+
// Both values are leak-guarded namespaces — safe for logs, unlike the paths.
|
|
603
|
+
process.stderr.write(
|
|
604
|
+
`vault-mirror: refusing to mirror — --source ${pathForLog(source)} belongs to repo namespace '${sourceNs}', but --repo-root resolves to '${repoNs}'. Run with the repo root the ledger belongs to.\n`,
|
|
605
|
+
);
|
|
606
|
+
await finishRun('source-repo-mismatch');
|
|
607
|
+
process.exit(2);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
463
611
|
const rl = createInterface({
|
|
464
612
|
input: createReadStream(resolve(source), 'utf8'),
|
|
465
613
|
crlfDelay: Infinity,
|
|
@@ -485,10 +633,14 @@ async function main() {
|
|
|
485
633
|
kind,
|
|
486
634
|
force,
|
|
487
635
|
vaultName,
|
|
488
|
-
// #1389: the repo whose `.vault.yaml`
|
|
489
|
-
//
|
|
490
|
-
// shared by every processor call and the auto-commit below.
|
|
491
|
-
repoRoot
|
|
636
|
+
// #1389/#1503: the repo whose identity (`.vault.yaml` slug, else git remote)
|
|
637
|
+
// is the namespace — resolved ONCE above from `--repo-root` / `--source`,
|
|
638
|
+
// and shared by every processor call and the auto-commit below.
|
|
639
|
+
repoRoot,
|
|
640
|
+
// #1503: every `source-record` key the `--kind session` source carries,
|
|
641
|
+
// superseded duplicates included — filled while buffering below, read by
|
|
642
|
+
// processSession's owner guard (a key from this ledger is never foreign).
|
|
643
|
+
sourceRecordKeys: new Set(),
|
|
492
644
|
qualityMinNarrativeChars,
|
|
493
645
|
qualityMinConfidence,
|
|
494
646
|
};
|
|
@@ -524,7 +676,7 @@ async function main() {
|
|
|
524
676
|
} catch (err) {
|
|
525
677
|
// Validation errors (missing required fields) → per-entry skip, not a global failure
|
|
526
678
|
if (err.message.startsWith('vault-mirror:')) {
|
|
527
|
-
process.stderr.write(`${err.message}\n`);
|
|
679
|
+
process.stderr.write(`${redactHomeDir(err.message)}\n`);
|
|
528
680
|
const entryId = entry?.id ?? entry?.session_id ?? null;
|
|
529
681
|
process.stdout.write(
|
|
530
682
|
JSON.stringify({ action: 'skipped-invalid', path: null, kind, id: entryId }) + '\n',
|
|
@@ -537,7 +689,7 @@ async function main() {
|
|
|
537
689
|
line: entryLineNum,
|
|
538
690
|
recordId: entryId,
|
|
539
691
|
skipClass: 'validation',
|
|
540
|
-
reason: err.message,
|
|
692
|
+
reason: redactHomeDir(err.message),
|
|
541
693
|
dryRun,
|
|
542
694
|
});
|
|
543
695
|
return;
|
|
@@ -556,7 +708,7 @@ async function main() {
|
|
|
556
708
|
(typeof err.code === 'string' && err.code.length > 0) || Boolean(err.syscall);
|
|
557
709
|
if (!isSystemError) {
|
|
558
710
|
process.stderr.write(
|
|
559
|
-
`vault-mirror: mapper crash on line ${entryLineNum} (${err.message}) — record skipped\n`,
|
|
711
|
+
`vault-mirror: mapper crash on line ${entryLineNum} (${redactHomeDir(err.message)}) — record skipped\n`,
|
|
560
712
|
);
|
|
561
713
|
const entryId = entry?.id ?? entry?.session_id ?? null;
|
|
562
714
|
process.stdout.write(
|
|
@@ -576,13 +728,13 @@ async function main() {
|
|
|
576
728
|
line: entryLineNum,
|
|
577
729
|
recordId: entryId,
|
|
578
730
|
skipClass: 'mapper-crash',
|
|
579
|
-
reason: err.message,
|
|
731
|
+
reason: redactHomeDir(err.message),
|
|
580
732
|
dryRun,
|
|
581
733
|
});
|
|
582
734
|
return;
|
|
583
735
|
}
|
|
584
736
|
// Unexpected filesystem errors → fatal
|
|
585
|
-
process.stderr.write(`vault-mirror: filesystem error on line ${entryLineNum}: ${err.message}\n`);
|
|
737
|
+
process.stderr.write(`vault-mirror: filesystem error on line ${entryLineNum}: ${redactHomeDir(err.message)}\n`);
|
|
586
738
|
await finishRun('filesystem-error');
|
|
587
739
|
process.exit(2);
|
|
588
740
|
}
|
|
@@ -608,7 +760,7 @@ async function main() {
|
|
|
608
760
|
try {
|
|
609
761
|
entry = JSON.parse(trimmed);
|
|
610
762
|
} catch (err) {
|
|
611
|
-
process.stderr.write(`vault-mirror: malformed JSON on line ${lineNum}: ${err.message}\n`);
|
|
763
|
+
process.stderr.write(`vault-mirror: malformed JSON on line ${lineNum}: ${redactHomeDir(err.message)}\n`);
|
|
612
764
|
// Close the run out BEFORE exiting: `process.exit` runs no `finally`, so
|
|
613
765
|
// without this the abort is the one outcome that leaves no run record —
|
|
614
766
|
// exactly the shape reserved for a broken emitter.
|
|
@@ -618,6 +770,8 @@ async function main() {
|
|
|
618
770
|
|
|
619
771
|
if (kind === 'session') {
|
|
620
772
|
sessionEntries.push(entry);
|
|
773
|
+
const recordKey = sessionSourceRecord(entry);
|
|
774
|
+
if (recordKey !== null) ctx.sourceRecordKeys.add(recordKey);
|
|
621
775
|
sessionLineNums.push(lineNum);
|
|
622
776
|
continue;
|
|
623
777
|
}
|
|
@@ -654,18 +808,27 @@ async function main() {
|
|
|
654
808
|
if (isIdentifiable(entry) && !survivors.has(entry)) {
|
|
655
809
|
// A losing duplicate: an earlier line whose `session_id` a LATER line
|
|
656
810
|
// in this same batch supersedes or overwrites (crash-recovery
|
|
657
|
-
// re-append, #1068 stub/supersede pair).
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
//
|
|
662
|
-
//
|
|
663
|
-
//
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
811
|
+
// re-append, #1068 stub/supersede pair). Never dispatched — the winning
|
|
812
|
+
// occurrence (dispatched below, at its own position) produces the ONE
|
|
813
|
+
// note this physical session gets. It is still ACCOUNTED for (#1291):
|
|
814
|
+
// `skipped-duplicate-session` goes through the same per-entry path as
|
|
815
|
+
// every other skip (stdout line + per-entry ledger record, no `path`
|
|
816
|
+
// because no target file exists for it) and into the tally, so
|
|
817
|
+
// `created+updated+skipped+failed === total` partitions a `--kind
|
|
818
|
+
// session` run that collapsed duplicates, and the ledger names WHICH
|
|
819
|
+
// lines were collapsed, not just how many notes were written.
|
|
820
|
+
tally(
|
|
821
|
+
await emitAction({
|
|
822
|
+
action: 'skipped-duplicate-session',
|
|
823
|
+
path: null,
|
|
824
|
+
kind,
|
|
825
|
+
id: entry.session_id,
|
|
826
|
+
vaultDir: ctx.vaultDir,
|
|
827
|
+
meta: { line: sessionLineNums[i] },
|
|
828
|
+
line: sessionLineNums[i],
|
|
829
|
+
dryRun,
|
|
830
|
+
}),
|
|
831
|
+
);
|
|
669
832
|
continue;
|
|
670
833
|
}
|
|
671
834
|
await dispatchEntry(entry, sessionLineNums[i]);
|
|
@@ -706,7 +869,7 @@ async function main() {
|
|
|
706
869
|
}
|
|
707
870
|
|
|
708
871
|
main().catch(async (err) => {
|
|
709
|
-
process.stderr.write(`vault-mirror: unexpected error: ${err.message}\n`);
|
|
872
|
+
process.stderr.write(`vault-mirror: unexpected error: ${redactHomeDir(err.message)}\n`);
|
|
710
873
|
// Same reason as the two in-loop aborts: an unexpected throw is a run that
|
|
711
874
|
// ENDED, and the ledger has to say so. `finishRun` never throws, so this
|
|
712
875
|
// cannot turn a diagnosable crash into a silent one.
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
39
|
import { parseArgs } from 'node:util';
|
|
40
|
+
import path from 'node:path';
|
|
40
41
|
import { attributionForRecord, emitEvent } from './lib/events.mjs';
|
|
41
42
|
import { MANIFEST_SESSION_KEYS } from './lib/session-identity/own-session.mjs';
|
|
42
43
|
import { isMainModule } from './lib/is-main-module.mjs';
|
|
@@ -109,7 +110,7 @@ async function main(argv) {
|
|
|
109
110
|
return;
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
const repoRoot = parsed.values['repo-root'] ?? process.cwd();
|
|
113
|
+
const repoRoot = path.resolve(parsed.values['repo-root'] ?? process.cwd());
|
|
113
114
|
const binding = resolveBinding(repoRoot);
|
|
114
115
|
|
|
115
116
|
if (Object.keys(binding).length === 0) {
|
|
@@ -133,14 +133,14 @@ Learnings live exclusively in `.orchestrator/metrics/learnings.jsonl`. The pre-`
|
|
|
133
133
|
|
|
134
134
|
The learning lifecycle states are:
|
|
135
135
|
|
|
136
|
-
- **Created**: `confidence: 0.5`, `expires_at
|
|
137
|
-
- **Confirmed** (same type+subject seen again): `confidence += 0.15` (cap 1.0), `expires_at` reset
|
|
136
|
+
- **Created**: `confidence: 0.5`, `expires_at` = `created_at` + a per-TYPE TTL — `deriveExpiresAt(created_at, type)` with `LEARNING_TTL_DAYS[type] ?? LEARNING_TTL_DAYS.default` (`scripts/lib/learnings/schema.mjs`; 30–90 days per type, 60 for an unknown type). A caller-supplied `expires_at` is kept. `learning-expiry-days` does NOT set it: `scripts/lib/config.mjs` still parses that key (default 30), but no script or hook reads the parsed value — it survives only in skill prose (the `/evolve` Review-mode *Extend* action).
|
|
137
|
+
- **Confirmed** (same type+subject seen again): `confidence += 0.15` (cap 1.0), `expires_at` reset to `deriveExpiresAt(now, type)`
|
|
138
138
|
- **Contradicted** (evidence against): `confidence -= 0.2` — do NOT reset `expires_at` (let the learning decay naturally if contradicted)
|
|
139
139
|
- **Decayed** (untouched this session): `confidence -= learning-decay-rate` (from Session Config, default `0.05`). Applied at session-end after touched-set update, before prune. Clamped to 0.0. Does NOT reset `expires_at`.
|
|
140
140
|
- **Expired**: `expires_at < current date` — removed on next write
|
|
141
141
|
- **Dead**: `confidence <= 0.0` — removed on next write
|
|
142
142
|
|
|
143
|
-
**Expiration check semantics:**
|
|
143
|
+
**Expiration check semantics:** The code compares full timestamps, not dates: `pruneLearnings()` (`scripts/lib/learnings/expiry-sweep.mjs`) archives when `expires_at < now`; the time-driven sweep archives only after a 14-day grace window past `expires_at`. When writing `expires_at`, derive it with `deriveExpiresAt()` (full ISO timestamp, no midnight rounding) — never from `learning-expiry-days`.
|
|
144
144
|
|
|
145
145
|
**Confidence bounds enforcement:** After EVERY increment or decrement, clamp confidence to [0.0, 1.0]. A learning at 0.95 confirmed becomes 1.0 (not 1.10). A learning at 0.1 contradicted becomes 0.0 and is pruned.
|
|
146
146
|
|