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
|
@@ -268,8 +268,9 @@
|
|
|
268
268
|
* hides its payload from this matcher by construction.
|
|
269
269
|
* - `sponge`, `install`, `rsync`, `awk > file` and other less common write
|
|
270
270
|
* verbs (a redirect inside an `awk` program string is quoted data here).
|
|
271
|
-
* - The Write/Edit tools — a different PreToolUse matcher entirely
|
|
272
|
-
*
|
|
271
|
+
* - The Write/Edit/MultiEdit tools — a different PreToolUse matcher entirely,
|
|
272
|
+
* guarded by `hooks/pre-edit-sessions-ledger-guard.mjs` (#1443), not this
|
|
273
|
+
* hook's surface.
|
|
273
274
|
* - The #385 repair-apply subshell/backtick deny (`unwrapSubshellLayers`)
|
|
274
275
|
* only unwraps a command that IS, in its entirety, one wrapped run —
|
|
275
276
|
* `x=$(node scripts/repair-invalid-sessions.mjs --apply)` (the wrapper is
|
|
@@ -1521,11 +1522,36 @@ async function main() {
|
|
|
1521
1522
|
? `${target.slice(0, TARGET_ECHO_MAX)}… (${target.length} chars)`
|
|
1522
1523
|
: target;
|
|
1523
1524
|
|
|
1525
|
+
// #1442 — a repair-apply VARIANT (extra flag, wrapper, env prefix) reached
|
|
1526
|
+
// here because it is not the exact sanctioned string. Text only, no matching
|
|
1527
|
+
// change: name the one command that IS allowed, and say why the most common
|
|
1528
|
+
// re-spelling cannot work — this hook reads SO_DISABLED_HOOKS from its OWN
|
|
1529
|
+
// process env (hooks/_lib/profile-gate.mjs), never from the command text, and
|
|
1530
|
+
// a leading `VAR=val` token also breaks the exact match above.
|
|
1531
|
+
if (target.startsWith(REPAIR_APPLY_MARKER)) {
|
|
1532
|
+
emitDeny(
|
|
1533
|
+
[
|
|
1534
|
+
`Direct write to the sessions ledger blocked: '${shown}'`,
|
|
1535
|
+
`The sanctioned repair invocation is allowed ONLY as the exact bare command:`,
|
|
1536
|
+
` ${REPAIR_APPLY_CANONICAL_COMMAND}`,
|
|
1537
|
+
`Any variant — an extra flag, a wrapper, an env prefix — is denied.`,
|
|
1538
|
+
`A SO_DISABLED_HOOKS=… prefix INSIDE the command has no effect: this hook reads`,
|
|
1539
|
+
`it from the environment of the whole session, never from the command text.`,
|
|
1540
|
+
`Override (intentional maintenance only, operator-side — e.g. started with \`!\`):`,
|
|
1541
|
+
`SO_DISABLED_HOOKS=pre-bash-sessions-ledger-guard as the environment of the session`,
|
|
1542
|
+
`See: GitLab #958, #1442, skills/session-end/session-metrics-write.md`,
|
|
1543
|
+
].join('\n'),
|
|
1544
|
+
);
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1524
1548
|
emitDeny(
|
|
1525
1549
|
[
|
|
1526
1550
|
`Direct write to the sessions ledger blocked: '${shown}'`,
|
|
1527
1551
|
`The ledger is append-only through its validating writer:`,
|
|
1528
1552
|
` node scripts/emit-session.mjs --entry '<json>' (or pipe the JSON on stdin)`,
|
|
1553
|
+
`After the writer succeeds, verify the record (read-only; does not repair):`,
|
|
1554
|
+
` node scripts/check-sessions-integrity.mjs --session-id '<semantic-id>'`,
|
|
1529
1555
|
`Hand-composing a record and appending it — with a shell redirect, or from`,
|
|
1530
1556
|
`inside an interpreter (node -e / python -c) — skips schema validation. That is`,
|
|
1531
1557
|
`exactly how the malformed records in GitLab #958 and #1408 landed.`,
|
|
@@ -75,6 +75,7 @@ import crypto from 'node:crypto';
|
|
|
75
75
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
76
76
|
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
77
77
|
import { stableHostname } from '../scripts/lib/host-identity.mjs';
|
|
78
|
+
import { dotGitAncestor, launchDirFromEnv } from '../scripts/lib/platform.mjs';
|
|
78
79
|
|
|
79
80
|
// ---------------------------------------------------------------------------
|
|
80
81
|
// Constants
|
|
@@ -400,16 +401,36 @@ function deriveAgentId() {
|
|
|
400
401
|
}
|
|
401
402
|
|
|
402
403
|
/**
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
404
|
+
* The repo whose fence directory this `git add` is recorded in — the SAME root
|
|
405
|
+
* the reader uses (#1504 point 4). The only reader, `hooks/wave-scope-commit-guard.mjs`,
|
|
406
|
+
* runs as git's pre-commit hook and walks `<git rev-parse --show-toplevel>/.orchestrator/staging-fence/`,
|
|
407
|
+
* i.e. the work-tree root the commit is made in. `dotGitAncestor(cwd)` is that
|
|
408
|
+
* root without a spawn: the nearest `.git` entry at or above the payload `cwd` —
|
|
409
|
+
* the worktree root for a session that entered a worktree (measured 2026-10-03:
|
|
410
|
+
* launch dir `<main>`, cwd `<worktree>/sub` → `<worktree>`, git toplevel
|
|
411
|
+
* `<worktree>`), the repo root for a launch in a repo SUBDIRECTORY. The launch
|
|
412
|
+
* dir (`$CLAUDE_PROJECT_DIR`) was wrong in both: the fence landed where no
|
|
413
|
+
* commit guard looked, and every cross-agent overlap passed.
|
|
406
414
|
*
|
|
415
|
+
* Deliberately NOT `resolveSessionRoot()`: its clamp answers the launch
|
|
416
|
+
* SUBDIRECTORY of a monorepo package, while git's toplevel is the repo root.
|
|
417
|
+
*
|
|
418
|
+
* Fallback when the payload carries no `cwd`, or `cwd` is in no repo (where no
|
|
419
|
+
* commit can read a fence anyway): the launch dir from env, then `process.cwd()`.
|
|
420
|
+
*
|
|
421
|
+
* NAMED CEILING (BV-004): the root follows the payload `cwd`, not the command —
|
|
422
|
+
* `git -C <other-tree> add` or `cd <other-tree> && git add` records the intent
|
|
423
|
+
* in the CURRENT tree's fence dir while the commit in `<other-tree>` reads its
|
|
424
|
+
* own, so that overlap stays unseen (as before). Revisit if a fence miss is
|
|
425
|
+
* ever traced to a redirected `git add`; the fix then is to resolve the root
|
|
426
|
+
* from the tokenized command's `-C` / leading `cd`.
|
|
427
|
+
*
|
|
428
|
+
* @param {object|null} input the hook payload
|
|
407
429
|
* @returns {string}
|
|
408
430
|
*/
|
|
409
|
-
function
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
?? process.cwd();
|
|
431
|
+
function resolveFenceRoot(input) {
|
|
432
|
+
const cwd = typeof input?.cwd === 'string' ? input.cwd.trim() : '';
|
|
433
|
+
return (cwd !== '' && dotGitAncestor(cwd)) || launchDirFromEnv() || process.cwd();
|
|
413
434
|
}
|
|
414
435
|
|
|
415
436
|
/**
|
|
@@ -494,7 +515,7 @@ async function main() {
|
|
|
494
515
|
if (!recognised) return emitAllow();
|
|
495
516
|
|
|
496
517
|
// G5 — derive paths.
|
|
497
|
-
const projectDir =
|
|
518
|
+
const projectDir = resolveFenceRoot(input);
|
|
498
519
|
const agentId = deriveAgentId();
|
|
499
520
|
const fenceFile = path.join(
|
|
500
521
|
projectDir,
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* pre-edit-sessions-ledger-guard.mjs — PreToolUse `Edit|Write|MultiEdit` hook:
|
|
4
|
+
* denies a TOOL write into the sessions ledger
|
|
5
|
+
* (`.orchestrator/metrics/sessions.jsonl`, plus rotated `sessions.jsonl.<N>`).
|
|
6
|
+
*
|
|
7
|
+
* The sibling `pre-bash-sessions-ledger-guard.mjs` covers shell writes only; the
|
|
8
|
+
* Edit/Write/MultiEdit tools were a second, unguarded route to the same file
|
|
9
|
+
* (GitLab #1443 item 4). Editing the ledger in place skips the validating writer
|
|
10
|
+
* (`scripts/emit-session.mjs`) and can silently rewrite records that are already
|
|
11
|
+
* there — the ledger is append-only by contract.
|
|
12
|
+
*
|
|
13
|
+
* ## Fleet safety — allow is the default, deny is the exception
|
|
14
|
+
*
|
|
15
|
+
* This hook runs on EVERY Edit/Write/MultiEdit in every session on the host, so
|
|
16
|
+
* it denies exactly one thing and allows everything else, including every
|
|
17
|
+
* internal failure: empty or unparsable stdin, a missing `tool_input`, an
|
|
18
|
+
* unknown `tool_name`, an absent `file_path`, or a repo module that fails to
|
|
19
|
+
* load all exit 0 with no decision (the harness proceeds). Only a positively
|
|
20
|
+
* identified ledger path is denied.
|
|
21
|
+
*
|
|
22
|
+
* ## Path scope — narrower than the Bash guard, on purpose
|
|
23
|
+
*
|
|
24
|
+
* The Bash guard matches on BASENAME (`sessions.jsonl` in any directory), because
|
|
25
|
+
* a shell `cd .orchestrator/metrics && echo … >> sessions.jsonl` leaves only the
|
|
26
|
+
* basename to see. The Edit/Write tools always carry the full target path, so
|
|
27
|
+
* here the path must END in `.orchestrator/metrics/sessions.jsonl` (optionally
|
|
28
|
+
* `.<digits>` for a rotated archive). Tmp-dir fixtures outside a
|
|
29
|
+
* `.orchestrator/metrics/` directory, `sessions.jsonl.bak`, `learnings.jsonl` and
|
|
30
|
+
* `events.jsonl` all allow. The comparison is case-insensitive because the
|
|
31
|
+
* default macOS filesystem is: `Sessions.JSONL` there IS the ledger.
|
|
32
|
+
*
|
|
33
|
+
* Not covered (by design — a guard against the accident, not a containment
|
|
34
|
+
* boundary): a symlink that points at the ledger under another name, and the
|
|
35
|
+
* `NotebookEdit` tool (not in the matcher; it cannot target a `.jsonl` file
|
|
36
|
+
* meaningfully).
|
|
37
|
+
*
|
|
38
|
+
* Named limits (measured 2026-09-25, security review of #1443):
|
|
39
|
+
*
|
|
40
|
+
* - The TRACKED fixture `tests/fixtures/harness-audit/clean-repo/.orchestrator/
|
|
41
|
+
* metrics/sessions.jsonl` matches the suffix and is DENIED (the Bash guard
|
|
42
|
+
* denies it too, by basename). No `tests/fixtures/` exemption on purpose: a
|
|
43
|
+
* path-prefix carve-out is a second hole, not a narrower guard. Editing that
|
|
44
|
+
* fixture is intentional maintenance — run with
|
|
45
|
+
* `SO_DISABLED_HOOKS=pre-edit-sessions-ledger-guard`.
|
|
46
|
+
* - A payload over 1 MB fails OPEN: `readStdin` (`scripts/lib/io.mjs`) rejects
|
|
47
|
+
* past 1,048,576 bytes and the catch below allows. A whole-ledger `Write`
|
|
48
|
+
* carries the file as `content`, so it bypasses the guard once the ledger
|
|
49
|
+
* nears that size. Largest ledger on the reference host: 481,543 bytes
|
|
50
|
+
* (`wc -c` over all 31 `.orchestrator/metrics/sessions.jsonl` under the
|
|
51
|
+
* projects root). Revisit when any ledger passes ~900 KB.
|
|
52
|
+
* - The case-insensitive match can DENY a distinct file on a case-sensitive
|
|
53
|
+
* filesystem (Linux): `.Orchestrator/METRICS/Sessions.JSONL` is not the
|
|
54
|
+
* ledger there, but is refused. Fails closed; same override as above.
|
|
55
|
+
*
|
|
56
|
+
* ## Platforms
|
|
57
|
+
*
|
|
58
|
+
* Cursor and Pi reach this hook through their bridges
|
|
59
|
+
* (`scripts/lib/cursor-hook-bridge.mjs`, `scripts/lib/pi-hook-bridge.mjs`), which
|
|
60
|
+
* rewrite native tool names to `Edit`/`Write`/`MultiEdit` and populate
|
|
61
|
+
* `tool_input.file_path`. Codex is a documented gap in
|
|
62
|
+
* `scripts/lib/validate/check-hooks-symmetry.mjs` (no Claude tool-name vocabulary).
|
|
63
|
+
*
|
|
64
|
+
* ## Override
|
|
65
|
+
*
|
|
66
|
+
* SO_DISABLED_HOOKS=pre-edit-sessions-ledger-guard (session-level)
|
|
67
|
+
* SO_HOOK_PROFILE=minimal|off
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
import path from 'node:path';
|
|
71
|
+
|
|
72
|
+
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
73
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
74
|
+
|
|
75
|
+
/** This hook's name — the `SO_DISABLED_HOOKS` key. */
|
|
76
|
+
const HOOK_NAME = 'pre-edit-sessions-ledger-guard';
|
|
77
|
+
|
|
78
|
+
/** The tools this hook judges. Anything else allows. */
|
|
79
|
+
const GUARDED_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The ledger, or a rotated archive of it, at the END of a normalized path.
|
|
83
|
+
* Anchored on a separator (or the start) so `x.orchestrator/metrics/…` does not match.
|
|
84
|
+
*/
|
|
85
|
+
const LEDGER_PATH_RE = /(?:^|\/)\.orchestrator\/metrics\/sessions\.jsonl(?:\.\d+)?$/i;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Upper bound on the echoed target in the deny reason. `emitDeny` clamps the
|
|
89
|
+
* whole reason, so an unbounded path on line 1 would push the writer/reader
|
|
90
|
+
* instructions off the end (same reasoning as the Bash guard's TARGET_ECHO_MAX).
|
|
91
|
+
*/
|
|
92
|
+
const TARGET_ECHO_MAX = 200;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Does this `file_path` name the sessions ledger (or a rotated archive)?
|
|
96
|
+
*
|
|
97
|
+
* Backslashes are normalized first so a Windows-spelled path is not read as one
|
|
98
|
+
* long filename (mirrors the Bash guard's `refersToLedger`), then
|
|
99
|
+
* `path.posix.normalize` folds `..` and `//` so `metrics/../metrics/sessions.jsonl`
|
|
100
|
+
* cannot sidestep the suffix match.
|
|
101
|
+
*
|
|
102
|
+
* @param {unknown} filePath
|
|
103
|
+
* @returns {boolean}
|
|
104
|
+
*/
|
|
105
|
+
export function isLedgerPath(filePath) {
|
|
106
|
+
if (typeof filePath !== 'string' || filePath.length === 0) return false;
|
|
107
|
+
const normalized = path.posix.normalize(filePath.replace(/\\/g, '/'));
|
|
108
|
+
return LEDGER_PATH_RE.test(normalized);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Read the payload and decide. Every non-ledger outcome returns without output
|
|
113
|
+
* (exit 0 = no decision = allow).
|
|
114
|
+
*
|
|
115
|
+
* @returns {Promise<void>}
|
|
116
|
+
*/
|
|
117
|
+
async function main() {
|
|
118
|
+
// Late-bound so a broken io.mjs lands in the fail-open catch below instead of
|
|
119
|
+
// an ESM link-time crash, and so the stdin reader is the repo's shared one.
|
|
120
|
+
const { readStdin, emitDeny } = await import('../scripts/lib/io.mjs');
|
|
121
|
+
|
|
122
|
+
let input;
|
|
123
|
+
try {
|
|
124
|
+
input = await readStdin();
|
|
125
|
+
} catch {
|
|
126
|
+
return; // unparsable / oversized / timed-out stdin → allow
|
|
127
|
+
}
|
|
128
|
+
if (!input || typeof input !== 'object') return;
|
|
129
|
+
if (!GUARDED_TOOLS.has(input.tool_name)) return;
|
|
130
|
+
|
|
131
|
+
const filePath = input.tool_input?.file_path;
|
|
132
|
+
if (!isLedgerPath(filePath)) return;
|
|
133
|
+
|
|
134
|
+
const shown = filePath.length > TARGET_ECHO_MAX ? `${filePath.slice(0, TARGET_ECHO_MAX)}…` : filePath;
|
|
135
|
+
emitDeny(
|
|
136
|
+
[
|
|
137
|
+
`${input.tool_name} on the sessions ledger blocked: '${shown}'`,
|
|
138
|
+
`The ledger is append-only through its validating writer:`,
|
|
139
|
+
` node scripts/emit-session.mjs --entry '<json>' (or pipe the JSON on stdin)`,
|
|
140
|
+
`Read or verify it without writing:`,
|
|
141
|
+
` node scripts/check-sessions-integrity.mjs`,
|
|
142
|
+
`Editing the file in place skips schema validation and can rewrite existing records.`,
|
|
143
|
+
`Override (intentional maintenance only): run the session with`,
|
|
144
|
+
`SO_DISABLED_HOOKS=${HOOK_NAME}`,
|
|
145
|
+
`See: GitLab #1443, skills/session-end/session-metrics-write.md`,
|
|
146
|
+
].join('\n'),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Entry guard (#1393): run only as the node script the harness execs — a bare
|
|
151
|
+
// `import()` must run no handler and must not exit the importing process.
|
|
152
|
+
if (isMainModule(import.meta.url)) {
|
|
153
|
+
if (!shouldRunHook(HOOK_NAME)) process.exit(0);
|
|
154
|
+
|
|
155
|
+
// Fail-OPEN on any internal error: exit 0 with no stdout is "no decision".
|
|
156
|
+
main()
|
|
157
|
+
.then(() => process.exit(0))
|
|
158
|
+
.catch((e) => {
|
|
159
|
+
process.stderr.write(`⚠ ${HOOK_NAME}: internal error — ${e?.message || e}\n`);
|
|
160
|
+
process.exit(0);
|
|
161
|
+
});
|
|
162
|
+
}
|