session-orchestrator 5.2.0 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/architecture/SKILL.md +3 -1
- package/.agents/skills/autopilot/SKILL.md +5 -1
- package/.agents/skills/autopilot/agents/openai.yaml +5 -0
- package/.agents/skills/bootstrap/SKILL.md +5 -1
- package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
- package/.agents/skills/brainstorm/SKILL.md +5 -1
- package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
- package/.agents/skills/close/SKILL.md +5 -1
- package/.agents/skills/close/agents/openai.yaml +5 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
- package/.agents/skills/debug/SKILL.md +5 -1
- package/.agents/skills/debug/agents/openai.yaml +5 -0
- package/.agents/skills/discovery/SKILL.md +5 -1
- package/.agents/skills/discovery/agents/openai.yaml +5 -0
- package/.agents/skills/dispatcher/SKILL.md +5 -1
- package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ecosystem-health/SKILL.md +3 -1
- package/.agents/skills/eli5/SKILL.md +5 -1
- package/.agents/skills/eli5/agents/openai.yaml +5 -0
- package/.agents/skills/eval/SKILL.md +6 -2
- package/.agents/skills/eval/agents/openai.yaml +5 -0
- package/.agents/skills/evolve/SKILL.md +6 -2
- package/.agents/skills/evolve/agents/openai.yaml +5 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
- package/.agents/skills/gitlab-ops/SKILL.md +3 -1
- package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
- package/.agents/skills/go/SKILL.md +5 -1
- package/.agents/skills/go/agents/openai.yaml +5 -0
- package/.agents/skills/grill/SKILL.md +5 -1
- package/.agents/skills/grill/agents/openai.yaml +5 -0
- package/.agents/skills/harness-audit/SKILL.md +5 -1
- package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
- package/.agents/skills/hook-development/SKILL.md +3 -1
- package/.agents/skills/mcp-builder/SKILL.md +3 -1
- package/.agents/skills/memory-cleanup/SKILL.md +5 -1
- package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
- package/.agents/skills/mode-selector/SKILL.md +3 -1
- package/.agents/skills/npm-publish/SKILL.md +4 -2
- package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
- package/.agents/skills/persona-panel/SKILL.md +5 -1
- package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
- package/.agents/skills/plan/SKILL.md +5 -1
- package/.agents/skills/plan/agents/openai.yaml +5 -0
- package/.agents/skills/playwright-driver/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +5 -1
- package/.agents/skills/portfolio/agents/openai.yaml +5 -0
- package/.agents/skills/quality-gates/SKILL.md +3 -1
- package/.agents/skills/reconcile/SKILL.md +5 -1
- package/.agents/skills/reconcile/agents/openai.yaml +5 -0
- package/.agents/skills/release/SKILL.md +5 -1
- package/.agents/skills/release/agents/openai.yaml +5 -0
- package/.agents/skills/remote-offload/SKILL.md +3 -1
- package/.agents/skills/repo-audit/SKILL.md +5 -1
- package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
- package/.agents/skills/session/SKILL.md +21 -0
- package/.agents/skills/session/agents/openai.yaml +5 -0
- package/.agents/skills/session-end/SKILL.md +3 -1
- package/.agents/skills/session-plan/SKILL.md +3 -1
- package/.agents/skills/session-start/SKILL.md +3 -1
- package/.agents/skills/spinout/SKILL.md +5 -1
- package/.agents/skills/spinout/agents/openai.yaml +5 -0
- package/.agents/skills/sunset-review/SKILL.md +5 -1
- package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
- package/.agents/skills/templates-ack/SKILL.md +21 -0
- package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
- package/.agents/skills/test/SKILL.md +5 -1
- package/.agents/skills/test/agents/openai.yaml +5 -0
- package/.agents/skills/test-runner/SKILL.md +3 -1
- package/.agents/skills/tmux-layout/SKILL.md +3 -1
- package/.agents/skills/using-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ux-grill/SKILL.md +5 -1
- package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
- package/.agents/skills/vault-mirror/SKILL.md +3 -1
- package/.agents/skills/vault-sync/SKILL.md +3 -1
- package/.agents/skills/wave-executor/SKILL.md +3 -1
- package/.agents/skills/write-executable-plan/SKILL.md +3 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +4 -4
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
- package/.codex-plugin/skills/eval/SKILL.md +1 -1
- package/.codex-plugin/skills/evolve/SKILL.md +1 -1
- package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
- package/.codex-plugin/skills/session/SKILL.md +1 -1
- package/.cursor/commands/eval.md +1 -1
- package/.cursor/commands/session.md +1 -1
- package/.cursor/rules/000-session-orchestrator.mdc +0 -2
- package/.cursor/rules/050-plan.mdc +1 -1
- package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/npm-publish/SKILL.md +1 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +12 -3
- package/AGENTS.md +3 -2
- package/CHANGELOG.md +136 -0
- package/README.md +9 -9
- package/SECURITY.md +12 -0
- package/agents/dialectic-deriver.md +13 -10
- package/agents/eval-judge.md +67 -45
- package/agents/skill-applied-judge.md +34 -19
- package/commands/session.md +7 -3
- package/docs/baseline.md +12 -6
- package/docs/codex-setup.md +14 -2
- package/docs/components.md +7 -5
- package/docs/events-schema.md +56 -9
- package/docs/rule-authoring.md +58 -6
- package/docs/session-config-reference.md +100 -7
- package/docs/session-config-template.md +31 -2
- package/docs/telemetry.md +2 -0
- package/hooks/_lib/hook-import-set.json +85 -8
- package/hooks/_lib/subagent-transcript.mjs +582 -31
- package/hooks/config-protection.mjs +11 -3
- package/hooks/cwd-change-restore.mjs +11 -3
- package/hooks/enforce-commands.mjs +70 -23
- package/hooks/enforce-scope.mjs +143 -33
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/loop-guard.mjs +11 -3
- package/hooks/on-session-end.mjs +58 -23
- package/hooks/on-session-start.mjs +48 -11
- package/hooks/on-stop.mjs +168 -22
- package/hooks/operator-steer.mjs +11 -3
- package/hooks/post-bash-issue-budget-refund.mjs +18 -8
- package/hooks/post-bash-write-verify.mjs +3 -2
- package/hooks/post-edit-import-probe.mjs +17 -9
- package/hooks/post-edit-validate.mjs +13 -5
- package/hooks/post-subagent-discovery-validator.mjs +98 -13
- package/hooks/post-tool-batch-wave-signal.mjs +200 -38
- package/hooks/post-tool-failure-corrective-context.mjs +11 -5
- package/hooks/post-tooluse-frontend-slop.mjs +10 -4
- package/hooks/pre-auq-clarity.mjs +15 -2
- package/hooks/pre-bash-destructive-guard.mjs +80 -9
- package/hooks/pre-bash-issue-budget.mjs +16 -11
- package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
- package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
- package/hooks/pre-bash-staging-fence.mjs +335 -31
- package/hooks/pre-bash-templates-first.mjs +19 -14
- package/hooks/pre-task-scope-disjoint.mjs +233 -2
- package/hooks/subagent-telemetry.mjs +15 -19
- package/hooks/wave-scope-commit-guard.mjs +197 -100
- package/monitors/monitors.json +1 -1
- package/output-styles/wave-summary.md +1 -1
- package/package.json +1 -1
- package/pi/prompts/eval.md +1 -1
- package/pi/prompts/session.md +1 -1
- package/rules/README.md +1 -1
- package/rules/opt-in-domain/prompt-caching.md +1 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +3 -3
- package/rules/opt-in-stack/frontend.md +1 -1
- package/rules/opt-in-stack/security-web.md +3 -3
- package/rules/opt-in-stack/swift.md +1 -1
- package/scripts/autopilot.mjs +23 -2
- package/scripts/backfill-abandoned-sessions.mjs +117 -15
- package/scripts/check-sessions-integrity.mjs +300 -0
- package/scripts/dialectic-deriver.mjs +50 -13
- package/scripts/emit-session.mjs +75 -29
- package/scripts/eval-session.mjs +65 -3
- package/scripts/generate-agents-skills.mjs +102 -29
- package/scripts/generate-cursor-adapter.mjs +61 -16
- package/scripts/lib/agent-status.mjs +2 -31
- package/scripts/lib/auq/clarity.mjs +10 -2
- package/scripts/lib/auq/parse.mjs +12 -31
- package/scripts/lib/auq/schema.mjs +56 -41
- package/scripts/lib/auto-dialectic.mjs +304 -15
- package/scripts/lib/autopilot/flags.mjs +12 -1
- package/scripts/lib/autopilot/kill-switches.mjs +6 -3
- package/scripts/lib/autopilot/loop.mjs +14 -1
- package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
- package/scripts/lib/ci-status-banner.mjs +376 -16
- package/scripts/lib/command-blocker.mjs +275 -28
- package/scripts/lib/config/dialectic.mjs +12 -3
- package/scripts/lib/config/gate.mjs +74 -0
- package/scripts/lib/config/reaper.mjs +162 -0
- package/scripts/lib/config.mjs +14 -0
- package/scripts/lib/convergence-monitor.mjs +74 -11
- package/scripts/lib/ecosystem-health.mjs +11 -0
- package/scripts/lib/eval/engine.mjs +421 -53
- package/scripts/lib/eval/judge.mjs +463 -40
- package/scripts/lib/eval/schema.mjs +10 -1
- package/scripts/lib/events-rotation.mjs +221 -25
- package/scripts/lib/events-schema.mjs +114 -0
- package/scripts/lib/events.mjs +524 -5
- package/scripts/lib/frontmatter-guard.mjs +21 -10
- package/scripts/lib/gates/gate-baseline.mjs +27 -2
- package/scripts/lib/gates/gate-full.mjs +28 -3
- package/scripts/lib/gates/gate-helpers.mjs +243 -21
- package/scripts/lib/gates/gate-incremental.mjs +28 -3
- package/scripts/lib/gates/gate-per-file.mjs +27 -2
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
- package/scripts/lib/instruction-budget-guard.mjs +146 -4
- package/scripts/lib/io.mjs +42 -8
- package/scripts/lib/issue-close-strip-labels.mjs +207 -49
- package/scripts/lib/js-mask.mjs +197 -0
- package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
- package/scripts/lib/maintenance-due-banner.mjs +53 -88
- package/scripts/lib/orphan-reaper.mjs +1588 -0
- package/scripts/lib/peer-cards/merger.mjs +48 -10
- package/scripts/lib/peer-cards/reader.mjs +78 -2
- package/scripts/lib/process-group.mjs +899 -0
- package/scripts/lib/quality-gate.mjs +107 -28
- package/scripts/lib/reconcile/backlog.mjs +368 -0
- package/scripts/lib/reconcile/engine.mjs +55 -188
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
- package/scripts/lib/reconcile/sanitize.mjs +69 -3
- package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
- package/scripts/lib/resource-probe/parsers.mjs +31 -0
- package/scripts/lib/rule-loader.mjs +41 -12
- package/scripts/lib/scope-echo.mjs +39 -2
- package/scripts/lib/scope-gate.mjs +605 -1
- package/scripts/lib/session-close-backfill.mjs +33 -6
- package/scripts/lib/session-id.mjs +9 -20
- package/scripts/lib/session-invocation.mjs +20 -0
- package/scripts/lib/session-schema/constants.mjs +30 -2
- package/scripts/lib/session-schema/normalizer.mjs +56 -4
- package/scripts/lib/session-schema.mjs +8 -3
- package/scripts/lib/session-start-probes.mjs +95 -10
- package/scripts/lib/sessions-canonical.mjs +23 -0
- package/scripts/lib/sessions-integrity-banner.mjs +7 -1
- package/scripts/lib/sessions-staleness-banner.mjs +193 -51
- package/scripts/lib/skill-evidence-window.mjs +891 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
- package/scripts/lib/skill-evolution/engine.mjs +18 -9
- package/scripts/lib/skill-judge.mjs +45 -3
- package/scripts/lib/tail-window.mjs +56 -0
- package/scripts/lib/telemetry/schema.mjs +30 -0
- package/scripts/lib/telemetry/sync.mjs +61 -6
- package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
- package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
- package/scripts/lib/user-invocable-skills.mjs +23 -3
- package/scripts/lib/ux-grill/reconcile.mjs +48 -22
- package/scripts/lib/validate/check-agents-skills.mjs +26 -15
- package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
- package/scripts/lib/validate/check-entry-guard.mjs +13 -50
- package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
- package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
- package/scripts/lib/validate/check-rules.mjs +7 -5
- package/scripts/lib/validate/check-skill-links.mjs +9 -1
- package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
- package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
- package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
- package/scripts/lib/validate/check-unwired-features.mjs +130 -27
- package/scripts/lib/validate/check-validator-registration.mjs +34 -10
- package/scripts/lib/validate/confidential-names.mjs +10 -0
- package/scripts/lib/validate-vendored-rules.mjs +4 -3
- package/scripts/lib/vault-mirror/namespace.mjs +46 -8
- package/scripts/lib/vault-mirror/process.mjs +10 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
- package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
- package/scripts/lib/vault-yaml.mjs +118 -0
- package/scripts/lib/worktree/lifecycle.mjs +153 -1
- package/scripts/release-session-lock.mjs +305 -0
- package/scripts/release.mjs +30 -5
- package/scripts/resolve-session-invocation.mjs +59 -0
- package/scripts/run-quality-gate.mjs +156 -17
- package/scripts/sweep-expired-rules.mjs +14 -3
- package/scripts/validate-plugin.mjs +12 -0
- package/scripts/validate-wave-scope.mjs +32 -105
- package/scripts/vault-mirror.mjs +9 -1
- package/skills/_shared/platform-tools.md +23 -11
- package/skills/autopilot/SKILL.md +22 -7
- package/skills/claude-md-drift-check/SKILL.md +1 -1
- package/skills/convergence-monitoring/README.md +8 -1
- package/skills/convergence-monitoring/SIGNALS.md +50 -6
- package/skills/convergence-monitoring/SKILL.md +15 -6
- package/skills/eval/SKILL.md +39 -24
- package/skills/eval/rubric-v1.md +1 -0
- package/skills/eval/rubric-v2.md +457 -0
- package/skills/evolve/SKILL.md +1 -1
- package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
- package/skills/gitlab-ops/SKILL.md +3 -2
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +11 -0
- package/skills/session-end/SKILL.md +13 -16
- package/skills/session-end/discovery-scan.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +55 -9
- package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
- package/skills/session-end/session-metrics-write.md +10 -0
- package/skills/session-plan/SKILL.md +17 -5
- package/skills/session-plan/references/session-plan-task-classification.md +2 -2
- package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -1
- package/skills/ux-grill/SKILL.md +1 -1
- package/skills/wave-executor/SKILL.md +8 -4
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +5 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +2 -1
- package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +0 -5
- package/.codex-plugin/skills/npm-publish/agents/openai.yaml +0 -5
- package/.cursor/commands/convergence-monitoring.md +0 -13
- package/.cursor/commands/npm-publish.md +0 -13
- package/pi/prompts/convergence-monitoring.md +0 -11
- package/pi/prompts/npm-publish.md +0 -11
|
@@ -42,8 +42,7 @@
|
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
44
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
45
|
-
|
|
46
|
-
if (!shouldRunHook('config-protection')) process.exit(0);
|
|
45
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
47
46
|
|
|
48
47
|
import { promises as fs } from 'node:fs';
|
|
49
48
|
import path from 'node:path';
|
|
@@ -512,4 +511,13 @@ async function main() {
|
|
|
512
511
|
}
|
|
513
512
|
|
|
514
513
|
// Fail-open on ANY uncaught error — a guard bug must never block a legit edit.
|
|
515
|
-
|
|
514
|
+
// Entry guard (#1393): run only when this file IS the script node was invoked
|
|
515
|
+
// with — every harness path execs it (`sh run-node.sh <this file>`). A bare
|
|
516
|
+
// `import()` (a probe, a test, a curious agent) must neither run main() nor
|
|
517
|
+
// tear the importing process down. The profile gate sits INSIDE the guard for
|
|
518
|
+
// that second reason: at module top level its `process.exit(0)` exited every
|
|
519
|
+
// process that merely imported this hook.
|
|
520
|
+
if (isMainModule(import.meta.url)) {
|
|
521
|
+
if (!shouldRunHook('config-protection')) process.exit(0);
|
|
522
|
+
main().catch(() => { try { emitAllow(); } catch { process.exit(0); } });
|
|
523
|
+
}
|
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
import path from 'node:path';
|
|
28
28
|
|
|
29
29
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
30
|
-
|
|
31
|
-
if (!shouldRunHook('cwd-change-restore')) process.exit(0);
|
|
30
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
32
31
|
|
|
33
32
|
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
34
33
|
import { atomicMutateJson } from './_lib/atomic-json.mjs';
|
|
@@ -108,4 +107,13 @@ async function main() {
|
|
|
108
107
|
}
|
|
109
108
|
|
|
110
109
|
// Exit 0 always — informational hook must never block Claude.
|
|
111
|
-
|
|
110
|
+
// Entry guard (#1393): run only when this file IS the script node was invoked
|
|
111
|
+
// with — every harness path execs it (`sh run-node.sh <this file>`). A bare
|
|
112
|
+
// `import()` (a probe, a test, a curious agent) must neither run main() nor
|
|
113
|
+
// tear the importing process down. The profile gate sits INSIDE the guard for
|
|
114
|
+
// that second reason: at module top level its `process.exit(0)` exited every
|
|
115
|
+
// process that merely imported this hook.
|
|
116
|
+
if (isMainModule(import.meta.url)) {
|
|
117
|
+
if (!shouldRunHook('cwd-change-restore')) process.exit(0);
|
|
118
|
+
main().catch(() => {}).finally(() => process.exit(0));
|
|
119
|
+
}
|
|
@@ -31,9 +31,44 @@
|
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
34
|
-
//
|
|
35
|
-
|
|
34
|
+
// Static for the SAME reason profile-gate.mjs is (#993, see the late-binding
|
|
35
|
+
// block below): a leaf predicate with ZERO repo imports (node:fs + node:url
|
|
36
|
+
// only) that decides whether this hook runs at all. Everything carrying a
|
|
37
|
+
// transitive repo graph stays late-bound inside bootstrap().
|
|
38
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
36
39
|
|
|
40
|
+
/**
|
|
41
|
+
* sha256(command), auf 16 Hex-Zeichen gekuerzt.
|
|
42
|
+
*
|
|
43
|
+
* WORTGLEICH zu `hashCommand()` in `hooks/pre-bash-destructive-guard.mjs:245`
|
|
44
|
+
* (das seinerseits `loop-guard.mjs` hashArgs() spiegelt). Bewusst dupliziert
|
|
45
|
+
* statt geteilt: ein Hook darf beim Start nicht an einem weiteren Modul
|
|
46
|
+
* haengen, das fehlen kann — die drei Zeilen sind billiger als ein
|
|
47
|
+
* Ladefehler auf dem PreToolUse-Pfad.
|
|
48
|
+
*
|
|
49
|
+
* WARUM ES DEN HELFER HIER BRAUCHT (2026-09-19, EventDrop #1140):
|
|
50
|
+
* Die `foreign_session_ignored`-Nutzlast dieses Hooks trug bis heute das
|
|
51
|
+
* ROHE Kommando. Gemessen in EventDrop.at: 3.816 Zeilen in der getrackten
|
|
52
|
+
* `.orchestrator/metrics/events.jsonl` mit rohem `command`, darin 24
|
|
53
|
+
* distinkte ECHTE Produktions-Share-Codes aus 17 fremden Kundenkonten und
|
|
54
|
+
* ein protokollierter `select access_pin_hash`. Bei 23 dieser Events ist
|
|
55
|
+
* der Share-Code die vollstaendige Capability — `/event/<code>` oeffnet das
|
|
56
|
+
* Album ohne Anmeldung. Das Journal ist getrackt und geht bei jedem Klon mit.
|
|
57
|
+
* Der Geschwisterhook `pre-bash-destructive-guard.mjs` machte es von Anfang
|
|
58
|
+
* an richtig und sagt es im eigenen Kopf: „Payload never includes the raw
|
|
59
|
+
* command — only a truncated sha256 command_hash."
|
|
60
|
+
*
|
|
61
|
+
* Der Hash haelt das Ereignis ZAEHLBAR und GRUPPIERBAR — genau die
|
|
62
|
+
* Eigenschaft, fuer die es laut `docs/scope-collision-guard.md` existiert.
|
|
63
|
+
* Das rohe Kommando wurde von keinem Konsumenten gelesen.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} command
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
function hashCommand(command) {
|
|
69
|
+
return crypto.createHash('sha256').update(command).digest('hex').slice(0, 16);
|
|
70
|
+
}
|
|
71
|
+
import crypto from 'node:crypto';
|
|
37
72
|
import path from 'node:path';
|
|
38
73
|
import { pathToFileURL } from 'node:url';
|
|
39
74
|
|
|
@@ -298,7 +333,7 @@ async function main() {
|
|
|
298
333
|
manifest_session: manifestIds,
|
|
299
334
|
own_session: [...ownIds],
|
|
300
335
|
wave: scope.wave,
|
|
301
|
-
command,
|
|
336
|
+
command_hash: hashCommand(command),
|
|
302
337
|
},
|
|
303
338
|
{ repoRoot: projectRoot },
|
|
304
339
|
);
|
|
@@ -430,26 +465,38 @@ function targetInWaveScope(target, allowedPaths, projectRoot) {
|
|
|
430
465
|
// guard armed and then tripped over a specific command; that fails CLOSED
|
|
431
466
|
// via emitDeny (SECURITY-REQ-01). The two paths MUST stay separate.
|
|
432
467
|
// ---------------------------------------------------------------------------
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
468
|
+
// Entry guard (#1393): run only when this file IS the script node was invoked
|
|
469
|
+
// with — every harness path execs it (`sh run-node.sh <this file>`). A bare
|
|
470
|
+
// `import()` (a probe, a test, a curious agent) must neither run main() nor
|
|
471
|
+
// tear the importing process down. The profile gate sits INSIDE the guard for
|
|
472
|
+
// that second reason: at module top level its `process.exit(0)` exited every
|
|
473
|
+
// process that merely imported this hook. bootstrap() is inside too — loading
|
|
474
|
+
// the guard sources is work a disabled hook and a bare importer must not do.
|
|
475
|
+
if (isMainModule(import.meta.url)) {
|
|
476
|
+
// #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
|
|
477
|
+
if (!shouldRunHook('enforce-commands')) process.exit(0);
|
|
478
|
+
|
|
436
479
|
try {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
480
|
+
await bootstrap();
|
|
481
|
+
} catch (loadError) {
|
|
482
|
+
try {
|
|
483
|
+
const { emitGuardInactiveBanner } = await import('./_lib/guard-source-loader.mjs');
|
|
484
|
+
// hookName is threaded explicitly (#993 — no hard-wired literal in the loader).
|
|
485
|
+
emitGuardInactiveBanner({ hookName: HOOK_NAME, error: loadError, consequence: GUARD_CONSEQUENCE });
|
|
486
|
+
} catch {
|
|
487
|
+
// Last resort: even the banner helper failed to load. Emit unconditionally —
|
|
488
|
+
// repeated noise beats a silent disarm.
|
|
489
|
+
process.stderr.write(
|
|
490
|
+
'🚨 enforce-commands: GUARD INACTIVE — module load failed ' +
|
|
491
|
+
`(${String(loadError?.message || loadError).split('\n')[0]}). ` +
|
|
492
|
+
'Blocked Bash commands are NOT being screened. See issue #993.\n'
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
process.exit(0); // fail-open, but no longer fail-silent
|
|
448
496
|
}
|
|
449
|
-
process.exit(0); // fail-open, but no longer fail-silent
|
|
450
|
-
}
|
|
451
497
|
|
|
452
|
-
// SECURITY-REQ-01 (F-03): top-level try/catch — never let exit 1 leak.
|
|
453
|
-
main().catch((e) => {
|
|
454
|
-
|
|
455
|
-
});
|
|
498
|
+
// SECURITY-REQ-01 (F-03): top-level try/catch — never let exit 1 leak.
|
|
499
|
+
main().catch((e) => {
|
|
500
|
+
emitDeny('Internal hook error — request blocked for safety', `${e?.message || e}`);
|
|
501
|
+
});
|
|
502
|
+
}
|
package/hooks/enforce-scope.mjs
CHANGED
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
* matches the fully realpath-resolved candidate → allow, BEFORE G6.
|
|
22
22
|
* Runs before G6 so a deliberate out-of-repo grant (e.g. a vault path)
|
|
23
23
|
* is reachable at all — G6 would otherwise deny every out-of-repo path
|
|
24
|
-
* without ever consulting allowedPaths. See
|
|
24
|
+
* without ever consulting allowedPaths. See matchedAbsoluteGrant.
|
|
25
|
+
* (#1398 cond. 4) The matched grant is GRADED through the shared
|
|
26
|
+
* `gradeScopeEntry` predicate; an `error` verdict emits ONE WARN and the
|
|
27
|
+
* write is still ALLOWED — this gate never denies on a grading verdict.
|
|
25
28
|
* G5c (#1295) out-of-root carveout for THIS repo's Claude Code auto-memory
|
|
26
29
|
* directory (`~/.claude/projects/<encoded-repo-path>/memory/`). Harness-
|
|
27
30
|
* owned, lives outside the working copy, cannot collide with any wave
|
|
@@ -68,8 +71,11 @@ import { promises as fs } from 'node:fs';
|
|
|
68
71
|
import { pathToFileURL } from 'node:url';
|
|
69
72
|
|
|
70
73
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
71
|
-
//
|
|
72
|
-
|
|
74
|
+
// Static for the SAME reason profile-gate.mjs is (#993, see the late-binding
|
|
75
|
+
// block below): a leaf predicate with ZERO repo imports (node:fs + node:url
|
|
76
|
+
// only) that decides whether this hook runs at all. Everything carrying a
|
|
77
|
+
// transitive repo graph stays late-bound inside bootstrap().
|
|
78
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
73
79
|
|
|
74
80
|
// ---------------------------------------------------------------------------
|
|
75
81
|
// #993 — late-bound repo dependencies
|
|
@@ -109,6 +115,12 @@ let readJson;
|
|
|
109
115
|
let classifyEmptyScope;
|
|
110
116
|
let suggestForEmptyScope;
|
|
111
117
|
let sessionStartedAtMs;
|
|
118
|
+
// #1398 cond. 4 — THE shared grading predicate for an absolute Gate 5b grant,
|
|
119
|
+
// plus the resolver it takes. The hook WARNS on an `error` verdict and never
|
|
120
|
+
// denies on it; see Gate 5b. Both come from the module already bound below —
|
|
121
|
+
// no new module edge, so `hooks/_lib/hook-import-set.json` is unchanged.
|
|
122
|
+
let gradeScopeEntry;
|
|
123
|
+
let canonicalizeGrantPrefix;
|
|
112
124
|
// #1123 — "is this manifest even mine?" (G3b). Process-local identity only
|
|
113
125
|
// (#1194): the repo-global `session.lock` tier is shared by every session in the
|
|
114
126
|
// checkout and would classify a peer's manifest as ours.
|
|
@@ -188,7 +200,13 @@ async function bootstrap() {
|
|
|
188
200
|
({ resolveProjectDir } = modules.platform);
|
|
189
201
|
({ findScopeFile, pathMatchesPattern, suggestForScopeViolation } = modules.hardening);
|
|
190
202
|
({ readJson } = modules.common);
|
|
191
|
-
({
|
|
203
|
+
({
|
|
204
|
+
classifyEmptyScope,
|
|
205
|
+
suggestForEmptyScope,
|
|
206
|
+
sessionStartedAtMs,
|
|
207
|
+
gradeScopeEntry,
|
|
208
|
+
canonicalizeGrantPrefix,
|
|
209
|
+
} = modules.scopeGate);
|
|
192
210
|
({ readProcessLocalSessionIds, classifyManifestSession } = modules.sessionIdentity);
|
|
193
211
|
}
|
|
194
212
|
|
|
@@ -433,7 +451,52 @@ async function main() {
|
|
|
433
451
|
// out-of-repo path before allowedPaths is ever consulted. Honour such grants
|
|
434
452
|
// here — matching ONLY absolute entries against the fully realpath-resolved
|
|
435
453
|
// candidate, so relative entries can never be used to escape the repo (REQ-09).
|
|
436
|
-
|
|
454
|
+
//
|
|
455
|
+
// GRADING (#1398 acceptance condition 4): this gate ALLOWS exactly what it
|
|
456
|
+
// allowed before — the verdict below is a WARN, never a deny. Until now the
|
|
457
|
+
// asymmetry was silent: `scripts/validate-wave-scope.mjs` graded these grants
|
|
458
|
+
// (system-root denylist, home depth + sensitive-subdirectory rule) and NOTHING
|
|
459
|
+
// called it from code — four prose steps in `skills/wave-executor/` were the
|
|
460
|
+
// only thing between a manifest and this gate, so a manifest that skipped them
|
|
461
|
+
// reached here ungraded and the operator never learned that it had.
|
|
462
|
+
//
|
|
463
|
+
// Now the same predicate runs HERE, on the grant that actually matched, and an
|
|
464
|
+
// `error` verdict surfaces as one operator-visible notice. Deliberately not a
|
|
465
|
+
// deny: this hook runs in every repo on the host, the grant is the
|
|
466
|
+
// coordinator's own artefact, and turning a live allow into a block on a
|
|
467
|
+
// pre-flight rule would break working sessions to enforce a policy whose place
|
|
468
|
+
// is before dispatch. `emitWarn` is an ALLOW that carries a notice (exit 0,
|
|
469
|
+
// `systemMessage` only) — see `scripts/lib/io.mjs`.
|
|
470
|
+
//
|
|
471
|
+
// THE SAME `resolve` THE VALIDATOR PASSES (#1398 cond. 4, closed 2026-09-21).
|
|
472
|
+
// It was omitted here on the assumption that canonicalisation is too expensive
|
|
473
|
+
// for a PreToolUse hot path; measuring it refuted that, and the omission was
|
|
474
|
+
// the last source of a verdict divergence between the two callers of one
|
|
475
|
+
// predicate (`/tmp/x/**`: `warn` here, `error/non-canonical` there — silent on
|
|
476
|
+
// exactly the grant that matches nothing at this gate).
|
|
477
|
+
//
|
|
478
|
+
// MEASURED 2026-09-21, A/B in one process, 200 repetitions, median:
|
|
479
|
+
// - this call site grades ONE grant per Gate 5b hit, never the manifest:
|
|
480
|
+
// +0.067 ms, 15 `realpathSync` — 75× under the 5 ms decision threshold;
|
|
481
|
+
// - a Gate 5b hit requires an out-of-repo write AND an absolute grant that
|
|
482
|
+
// matches it. The live 64-entry manifest of that session carried 0
|
|
483
|
+
// absolute entries and therefore cost 0 syscalls: an ordinary in-repo
|
|
484
|
+
// write never reaches this line at all.
|
|
485
|
+
// `canonicalizeGrantPrefix` memoizes per process and never throws (see its
|
|
486
|
+
// docblock); `gradeScopeEntry` additionally degrades a throwing resolver to
|
|
487
|
+
// the literal spelling, so the worst case is today's verdict.
|
|
488
|
+
const matchedGrant = matchedAbsoluteGrant(resolvedPath, allowedPaths);
|
|
489
|
+
if (matchedGrant !== null) {
|
|
490
|
+
const grade = gradeScopeEntry(matchedGrant, { resolve: canonicalizeGrantPrefix });
|
|
491
|
+
if (grade?.verdict === 'error') {
|
|
492
|
+
return emitWarn(
|
|
493
|
+
`Gate 5b honoured an out-of-repo grant that scripts/validate-wave-scope.mjs would REFUSE ` +
|
|
494
|
+
`before dispatch — allowedPaths ${grade.message}. The write to '${resolvedPath}' is ` +
|
|
495
|
+
`ALLOWED (this gate never denies on a grading verdict); re-validate the manifest.`,
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
return emitAllow();
|
|
499
|
+
}
|
|
437
500
|
|
|
438
501
|
// Gate 6: path must be inside the project root
|
|
439
502
|
if (!isPathInside(resolvedPath, projectRoot)) {
|
|
@@ -774,19 +837,54 @@ function isCoordinatorCarveout(normalizedRel, projectRoot, scopePath) {
|
|
|
774
837
|
* SECURITY (REQ-09): matches ONLY entries that are themselves absolute, against
|
|
775
838
|
* the fully realpath-resolved candidate — so RELATIVE entries (`**`, `../**`)
|
|
776
839
|
* can NEVER be used to escape the repo. When no absolute entry exists the helper
|
|
777
|
-
* returns
|
|
840
|
+
* returns null and the caller falls through to Gate 6 unchanged (inert pre-gate).
|
|
778
841
|
* An absolute entry matches only its own literal (canonical/realpath) subtree;
|
|
779
842
|
* the operator is responsible for supplying a canonical absolute path.
|
|
780
843
|
*
|
|
844
|
+
* MATCHING IS SHAPE-BLIND HERE; GRADING IS SHARED (#1398 cond. 4). This helper
|
|
845
|
+
* still decides nothing about whether a grant SHOULD exist: `/Users/<u>/**` and
|
|
846
|
+
* `/Users/<u>/.ssh/**` match exactly like `/Users/<u>/Projects/vault/**`. What
|
|
847
|
+
* changed is that the CALLER now runs the ONE grading predicate —
|
|
848
|
+
* `gradeScopeEntry` in `scripts/lib/scope-gate.mjs`, the same function
|
|
849
|
+
* `scripts/validate-wave-scope.mjs` turns into an exit-1 refusal — over the grant
|
|
850
|
+
* this helper returns, and WARNS when it grades `error`.
|
|
851
|
+
*
|
|
852
|
+
* ONE PREDICATE, ONE ARGUMENT SHAPE — THE VERDICTS NO LONGER DIFFER (#1398
|
|
853
|
+
* cond. 4, closed 2026-09-21). Both callers now pass the SAME fs-backed
|
|
854
|
+
* `resolve` (`canonicalizeGrantPrefix`, exported from
|
|
855
|
+
* `scripts/lib/scope-gate.mjs`), so only the CONSEQUENCE is asymmetric: the
|
|
856
|
+
* validator refuses an `error` before dispatch, this gate allows it with a
|
|
857
|
+
* notice at write time. The history, because the asymmetry was load-bearing for
|
|
858
|
+
* two fixes, measured 2026-09-20 @ 7e110a2a with the resolver-free call:
|
|
859
|
+
* - `/private/etc/**`, `/private/var/**` — the macOS realpaths of the
|
|
860
|
+
* denylisted `/etc` and `/var`, i.e. the spellings Gate 5b ACTUALLY matches
|
|
861
|
+
* (it matches the realpath-resolved candidate): hook `warn`, validator
|
|
862
|
+
* `error`. The notice fired on `/etc/**`, which reaches nothing here, and
|
|
863
|
+
* stayed silent on the spelling that reaches everything. CLOSED by listing
|
|
864
|
+
* the two aliases literally (`DENIED_ABSOLUTE_ALIAS_ROOTS`), at zero
|
|
865
|
+
* syscalls — they still cost nothing and stay.
|
|
866
|
+
* - `/tmp/x/**` — hook `warn`, validator `error/non-canonical`: the last
|
|
867
|
+
* divergence, 1 of 9 probes, and not closable by any literal list, because
|
|
868
|
+
* proving a literal prefix resolves elsewhere IS the realpath call. CLOSED
|
|
869
|
+
* by passing the resolver here too, once its cost was measured instead of
|
|
870
|
+
* assumed (+0.067 ms per Gate 5b hit; see the call site).
|
|
871
|
+
* The parity of the two verdict tables is pinned by
|
|
872
|
+
* `tests/lib/scope-gate.test.mjs` § "hook / validator grading parity", so a NEW
|
|
873
|
+
* divergence is a red test rather than a comment someone has to notice.
|
|
874
|
+
*
|
|
875
|
+
* Returns the MATCHED PATTERN rather than a boolean precisely so the caller has
|
|
876
|
+
* something to grade: with a bare `true` the grant that opened the gate is
|
|
877
|
+
* unknowable at the call site, and any notice would have to re-derive it.
|
|
878
|
+
*
|
|
781
879
|
* @param {string} resolvedPath — fully realpath-resolved candidate (absolute)
|
|
782
880
|
* @param {string[]} allowedPaths — raw allowedPaths array from wave-scope.json
|
|
783
|
-
* @returns {
|
|
881
|
+
* @returns {string|null} the first matching absolute entry, or null
|
|
784
882
|
*/
|
|
785
|
-
function
|
|
883
|
+
function matchedAbsoluteGrant(resolvedPath, allowedPaths) {
|
|
786
884
|
const abs = allowedPaths.filter((p) => typeof p === 'string' && path.isAbsolute(p));
|
|
787
|
-
if (abs.length === 0) return
|
|
885
|
+
if (abs.length === 0) return null;
|
|
788
886
|
const normalizedAbs = resolvedPath.split(path.sep).join('/');
|
|
789
|
-
return abs.
|
|
887
|
+
return abs.find((pat) => pathMatchesPattern(normalizedAbs, pat.split(path.sep).join('/'))) ?? null;
|
|
790
888
|
}
|
|
791
889
|
|
|
792
890
|
// ---------------------------------------------------------------------------
|
|
@@ -804,29 +902,41 @@ function matchesAbsoluteAllowlist(resolvedPath, allowedPaths) {
|
|
|
804
902
|
// guard armed and then tripped over a specific path; that fails CLOSED via
|
|
805
903
|
// emitDeny (SECURITY-REQ-01). The two paths MUST stay separate.
|
|
806
904
|
// ---------------------------------------------------------------------------
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
905
|
+
// Entry guard (#1393): run only when this file IS the script node was invoked
|
|
906
|
+
// with — every harness path execs it (`sh run-node.sh <this file>`). A bare
|
|
907
|
+
// `import()` (a probe, a test, a curious agent) must neither run main() nor
|
|
908
|
+
// tear the importing process down. The profile gate sits INSIDE the guard for
|
|
909
|
+
// that second reason: at module top level its `process.exit(0)` exited every
|
|
910
|
+
// process that merely imported this hook. bootstrap() is inside too — loading
|
|
911
|
+
// the guard sources is work a disabled hook and a bare importer must not do.
|
|
912
|
+
if (isMainModule(import.meta.url)) {
|
|
913
|
+
// #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
|
|
914
|
+
if (!shouldRunHook('enforce-scope')) process.exit(0);
|
|
915
|
+
|
|
810
916
|
try {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
917
|
+
await bootstrap();
|
|
918
|
+
} catch (loadError) {
|
|
919
|
+
try {
|
|
920
|
+
const { emitGuardInactiveBanner } = await import('./_lib/guard-source-loader.mjs');
|
|
921
|
+
// hookName is threaded explicitly (#993 — no hard-wired literal in the loader).
|
|
922
|
+
emitGuardInactiveBanner({ hookName: HOOK_NAME, error: loadError, consequence: GUARD_CONSEQUENCE });
|
|
923
|
+
} catch {
|
|
924
|
+
// Last resort: even the banner helper failed to load. Emit unconditionally —
|
|
925
|
+
// repeated noise beats a silent disarm.
|
|
926
|
+
process.stderr.write(
|
|
927
|
+
'🚨 enforce-scope: GUARD INACTIVE — module load failed ' +
|
|
928
|
+
`(${String(loadError?.message || loadError).split('\n')[0]}). ` +
|
|
929
|
+
'Edit/Write/MultiEdit scope enforcement is OFF. See issue #993.\n'
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
process.exit(0); // fail-open, but no longer fail-silent
|
|
822
933
|
}
|
|
823
|
-
process.exit(0); // fail-open, but no longer fail-silent
|
|
824
|
-
}
|
|
825
934
|
|
|
826
|
-
// SECURITY-REQ-01 (fail-closed): any unhandled rejection → structured deny, never bare exit 1
|
|
827
|
-
main().catch((e) => {
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
});
|
|
935
|
+
// SECURITY-REQ-01 (fail-closed): any unhandled rejection → structured deny, never bare exit 1
|
|
936
|
+
main().catch((e) => {
|
|
937
|
+
emitDeny(
|
|
938
|
+
'Internal hook error — request blocked for safety',
|
|
939
|
+
`${e?.message ?? String(e)}`,
|
|
940
|
+
);
|
|
941
|
+
});
|
|
942
|
+
}
|
package/hooks/hooks-codex.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
|
9
9
|
"type": "command",
|
|
10
|
-
"command": "echo '🎯 Session Orchestrator v5.
|
|
10
|
+
"command": "echo '🎯 Session Orchestrator v5.3.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
|
|
11
11
|
"async": false
|
|
12
12
|
},
|
|
13
13
|
{
|
package/hooks/hooks.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "echo '🎯 Session Orchestrator v5.
|
|
9
|
+
"command": "echo '🎯 Session Orchestrator v5.3.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
|
|
10
10
|
"async": false
|
|
11
11
|
},
|
|
12
12
|
{
|
package/hooks/loop-guard.mjs
CHANGED
|
@@ -25,8 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
28
|
-
|
|
29
|
-
if (!shouldRunHook('loop-guard')) process.exit(0);
|
|
28
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
30
29
|
|
|
31
30
|
import { promises as fs } from 'node:fs';
|
|
32
31
|
import os from 'node:os';
|
|
@@ -257,4 +256,13 @@ async function main() {
|
|
|
257
256
|
}
|
|
258
257
|
|
|
259
258
|
// Exit 0 always — informational hook must never block Claude (#619).
|
|
260
|
-
|
|
259
|
+
// Entry guard (#1393): run only when this file IS the script node was invoked
|
|
260
|
+
// with — every harness path execs it (`sh run-node.sh <this file>`). A bare
|
|
261
|
+
// `import()` (a probe, a test, a curious agent) must neither run main() nor
|
|
262
|
+
// tear the importing process down. The profile gate sits INSIDE the guard for
|
|
263
|
+
// that second reason: at module top level its `process.exit(0)` exited every
|
|
264
|
+
// process that merely imported this hook.
|
|
265
|
+
if (isMainModule(import.meta.url)) {
|
|
266
|
+
if (!shouldRunHook('loop-guard')) process.exit(0);
|
|
267
|
+
main().catch(() => {}).finally(() => process.exit(0));
|
|
268
|
+
}
|
package/hooks/on-session-end.mjs
CHANGED
|
@@ -31,8 +31,6 @@ import { promises as fs } from 'node:fs';
|
|
|
31
31
|
import path from 'node:path';
|
|
32
32
|
|
|
33
33
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
34
|
-
// #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
|
|
35
|
-
if (!shouldRunHook('on-session-end')) process.exit(0);
|
|
36
34
|
|
|
37
35
|
import { emitEvent } from '../scripts/lib/events.mjs';
|
|
38
36
|
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
@@ -47,6 +45,7 @@ import {
|
|
|
47
45
|
OWNER_PROOF_RELPATH,
|
|
48
46
|
} from '../scripts/lib/session-lock.mjs';
|
|
49
47
|
import { parseSessionId } from '../scripts/lib/session-id.mjs';
|
|
48
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
50
49
|
import { deregisterSelf, logSweepEvent } from '../scripts/lib/session-registry.mjs';
|
|
51
50
|
import { readConfigFile, parseSessionConfig } from '../scripts/lib/config.mjs';
|
|
52
51
|
import { flush } from '../scripts/lib/telemetry/sync.mjs';
|
|
@@ -255,21 +254,44 @@ async function readPersistence(projectRoot) {
|
|
|
255
254
|
}
|
|
256
255
|
|
|
257
256
|
/**
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
257
|
+
* Characters kept from `flush()`'s raw `reason` in the event record.
|
|
258
|
+
*
|
|
259
|
+
* NAMED CEILING (BV-004): `flush()` may return `build-error: <message>`, i.e.
|
|
260
|
+
* unbounded free text, into a stream whose purpose is aggregation. 200 mirrors
|
|
261
|
+
* the sibling bound in `scripts/lib/session-start-probes.mjs`; the banner
|
|
262
|
+
* re-bounds to 120 and strips control bytes on its own side, because the ledger
|
|
263
|
+
* is an untrusted string source for anything that renders it.
|
|
264
|
+
* REVISIT TRIGGER: a legitimate `flush()` reason longer than 200 characters.
|
|
265
|
+
*/
|
|
266
|
+
const FLUSH_REASON_MAX_CHARS = 200;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Reduce a `flush()` result to the three-field breadcrumb the event carries.
|
|
270
|
+
*
|
|
271
|
+
* `reason` is the FULL reason (length-bounded, see FLUSH_REASON_MAX_CHARS) and
|
|
272
|
+
* `reason_class` is its head token — the part before the first `:` — so an
|
|
273
|
+
* aggregation by class stays possible without destroying the detail.
|
|
274
|
+
*
|
|
275
|
+
* #1392, the defect this shape replaces: `reason` used to BE the head token
|
|
276
|
+
* (`String(res?.reason ?? 'unknown').split(':')[0]`). All five sandbox
|
|
277
|
+
* refusals `scripts/lib/telemetry/sync.mjs` produces (`sandbox:temp-root`,
|
|
278
|
+
* `sandbox:probe-failed`, …) were therefore written as bare `sandbox`, while
|
|
279
|
+
* the one reader — `scripts/lib/telemetry-flush-health-banner.mjs` — requires
|
|
280
|
+
* `reason.startsWith('sandbox:')`. The banner could not fire on any record this
|
|
281
|
+
* emitter ever wrote, and a mute instrument looks exactly like a healthy
|
|
282
|
+
* channel (HR-105). The cut is irrecoverable at the reader, so the fix belongs
|
|
283
|
+
* here: persist the full reason, carry the class beside it.
|
|
263
284
|
*
|
|
264
285
|
* @param {{sent?: boolean, queued?: boolean, reason?: string}|null|undefined} res
|
|
265
|
-
* @returns {{outcome: 'sent'|'queued'|'gated'|'skipped', reason: string}}
|
|
286
|
+
* @returns {{outcome: 'sent'|'queued'|'gated'|'skipped', reason: string, reason_class: string}}
|
|
266
287
|
*/
|
|
267
|
-
function classifyFlush(res) {
|
|
268
|
-
const reason = String(res?.reason ?? 'unknown').
|
|
269
|
-
|
|
270
|
-
if (res?.
|
|
271
|
-
if (
|
|
272
|
-
return { outcome: '
|
|
288
|
+
export function classifyFlush(res) {
|
|
289
|
+
const reason = String(res?.reason ?? 'unknown').slice(0, FLUSH_REASON_MAX_CHARS);
|
|
290
|
+
const reasonClass = reason.split(':')[0];
|
|
291
|
+
if (res?.sent === true) return { outcome: 'sent', reason, reason_class: reasonClass };
|
|
292
|
+
if (res?.queued === true) return { outcome: 'queued', reason, reason_class: reasonClass };
|
|
293
|
+
if (reasonClass === 'gated') return { outcome: 'gated', reason, reason_class: reasonClass };
|
|
294
|
+
return { outcome: 'skipped', reason, reason_class: reasonClass };
|
|
273
295
|
}
|
|
274
296
|
|
|
275
297
|
/**
|
|
@@ -287,8 +309,9 @@ function classifyFlush(res) {
|
|
|
287
309
|
* statement) — this function deliberately does not re-implement it, so there is
|
|
288
310
|
* exactly one place where "may we send?" is decided.
|
|
289
311
|
*
|
|
290
|
-
* Always emits `orchestrator.telemetry.flush` with `{ outcome, reason
|
|
291
|
-
* NOTHING else — no payload, no anon_id. Per
|
|
312
|
+
* Always emits `orchestrator.telemetry.flush` with `{ outcome, reason,
|
|
313
|
+
* reason_class }` and NOTHING else — no payload, no anon_id. Per
|
|
314
|
+
* `.claude/rules/host-resources.md`
|
|
292
315
|
* HR-105, a mechanism whose firing rate nothing records cannot be falsified;
|
|
293
316
|
* this event is what makes the flush rate measurable next time.
|
|
294
317
|
*
|
|
@@ -305,13 +328,17 @@ async function flushTelemetry(projectRoot) {
|
|
|
305
328
|
}))
|
|
306
329
|
// `persistence: false` means this session leaves no durable local trace;
|
|
307
330
|
// a telemetry ping is a durable record too, so it honours the same switch.
|
|
308
|
-
|
|
331
|
+
// `reason_class` is spelled out on BOTH non-flush() branches so every
|
|
332
|
+
// record of this event carries the same three keys — a consumer grouping
|
|
333
|
+
// by class must not have to treat "absent" as a fourth class.
|
|
334
|
+
: { outcome: 'skipped', reason: 'persistence-disabled', reason_class: 'persistence-disabled' };
|
|
309
335
|
} catch {
|
|
310
|
-
result = { outcome: 'skipped', reason: 'error' };
|
|
336
|
+
result = { outcome: 'skipped', reason: 'error', reason_class: 'error' };
|
|
311
337
|
}
|
|
312
338
|
|
|
313
|
-
// `result` is passed through verbatim: it holds EXACTLY
|
|
314
|
-
// no payload field can leak into the
|
|
339
|
+
// `result` is passed through verbatim: it holds EXACTLY
|
|
340
|
+
// {outcome, reason, reason_class}, so no payload field can leak into the
|
|
341
|
+
// event by accident.
|
|
315
342
|
try {
|
|
316
343
|
await emitEvent('orchestrator.telemetry.flush', result);
|
|
317
344
|
} catch { /* observability is best-effort */ }
|
|
@@ -830,6 +857,14 @@ async function main() {
|
|
|
830
857
|
}
|
|
831
858
|
|
|
832
859
|
// Exit 0 always — informational hook must never block session teardown.
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
860
|
+
// Entry guard (#1298 P7): run only as the node script every harness execs
|
|
861
|
+
// (`sh run-node.sh <this file>`); a bare `import()` must not tear down the
|
|
862
|
+
// live repo's session state.
|
|
863
|
+
if (isMainModule(import.meta.url)) {
|
|
864
|
+
// #1393: the profile gate sits INSIDE the entry guard — at module top level
|
|
865
|
+
// its `process.exit(0)` exited every process that merely imported this hook.
|
|
866
|
+
if (!shouldRunHook('on-session-end')) process.exit(0);
|
|
867
|
+
main()
|
|
868
|
+
.catch(() => {})
|
|
869
|
+
.finally(() => process.exit(0));
|
|
870
|
+
}
|