session-orchestrator 5.12.0 → 5.13.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +4 -1
- package/.codex-plugin/agents/explorer.toml +1 -1
- package/.codex-plugin/agents/session-reviewer.toml +2 -2
- package/.codex-plugin/agents/wave-worker.toml +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +8 -0
- package/.orchestrator/policy/public-export.json +30 -0
- package/CHANGELOG.md +162 -4
- package/README.md +126 -127
- package/agents/analyst.md +7 -2
- package/agents/architect-reviewer.md +7 -2
- package/agents/code-implementer.md +4 -2
- package/agents/db-specialist.md +9 -2
- package/agents/dialectic-deriver.md +9 -3
- package/agents/docs-writer.md +7 -2
- package/agents/eval-judge.md +7 -1
- package/agents/qa-strategist.md +7 -2
- package/agents/security-reviewer.md +9 -2
- package/agents/session-reviewer.md +18 -2
- package/agents/skill-applied-judge.md +7 -1
- package/agents/test-writer.md +9 -2
- package/agents/ui-developer.md +9 -2
- package/agents/ux-evaluator.md +6 -1
- package/commands/session.md +1 -1
- package/commands/templates-ack.md +89 -18
- package/docs/agent-authoring.md +30 -11
- package/docs/ci-setup.md +38 -2
- package/docs/codex-setup.md +33 -9
- package/docs/components.md +13 -2
- package/docs/cursor-setup.md +3 -3
- package/docs/events-schema.md +5 -4
- package/docs/evidence-candidates.md +63 -0
- package/docs/fleet-checkin-audit.md +7 -4
- package/docs/github-mirror-protection.md +150 -3
- package/docs/install.md +90 -17
- package/docs/instruction-delivery.md +1 -1
- package/docs/migration-v4.md +2 -2
- package/docs/mods.md +118 -0
- package/docs/owner-config-schema.md +1 -0
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/pm-skills-marketplace.md +4 -3
- package/docs/rule-authoring.md +24 -13
- package/docs/scope-collision-guard.md +2 -1
- package/docs/session-config-reference.md +49 -15
- package/docs/session-config-template.md +30 -22
- package/docs/sessions-ledger-repair.md +137 -0
- package/docs/vault-docs-architecture.md +1 -1
- package/hooks/_lib/hook-import-set.json +406 -73
- package/hooks/_lib/hook-root.mjs +301 -0
- package/hooks/_lib/lock-bootstrap.mjs +434 -4
- package/hooks/_lib/orchestrator-ignore.mjs +3 -0
- package/hooks/_lib/subagent-transcript.mjs +2 -2
- package/hooks/_lib/vcs-create-matcher.mjs +13 -17
- package/hooks/cwd-change-restore.mjs +5 -1
- package/hooks/enforce-commands.mjs +4 -3
- package/hooks/enforce-scope.mjs +11 -1
- package/hooks/hooks-codex.json +48 -2
- package/hooks/hooks.json +31 -31
- package/hooks/on-session-end.mjs +38 -15
- package/hooks/on-session-start.mjs +139 -49
- package/hooks/on-stop.mjs +11 -6
- package/hooks/post-bash-issue-budget-refund.mjs +6 -1
- package/hooks/post-bash-write-verify.mjs +8 -4
- package/hooks/post-edit-validate.mjs +45 -52
- package/hooks/post-subagent-discovery-validator.mjs +2 -2
- package/hooks/post-tool-batch-wave-signal.mjs +8 -4
- package/hooks/post-tool-failure-corrective-context.mjs +5 -1
- package/hooks/pre-bash-destructive-guard.mjs +220 -8
- package/hooks/pre-bash-issue-budget.mjs +6 -1
- package/hooks/pre-bash-sessions-ledger-guard.mjs +691 -123
- package/hooks/pre-bash-staging-fence.mjs +258 -14
- package/hooks/pre-bash-templates-first.mjs +28 -27
- package/hooks/run-node.sh +1 -1
- package/hooks/skill-invocation-telemetry.mjs +15 -8
- package/hooks/subagent-telemetry.mjs +77 -14
- package/hooks/wave-scope-commit-guard.mjs +143 -14
- package/mods/so-hud/.claude-plugin/plugin.json +11 -0
- package/mods/so-hud/README.md +57 -0
- package/mods/so-hud/hooks/beacon-io.mjs +101 -0
- package/mods/so-hud/hooks/heredoc-scan.ts +394 -0
- package/mods/so-hud/hooks/hooks.json +1 -0
- package/mods/so-hud/hooks/logic.ts +182 -0
- package/mods/so-hud/hooks/register.tsx +670 -0
- package/mods/so-hud/hooks/rules.ts +2998 -0
- package/mods/so-hud/tsconfig.json +8 -0
- package/mods/so-hud/types/index.d.ts +36 -0
- package/package.json +4 -2
- package/rules/_index.md +1 -1
- package/rules/always-on/ask-via-tool.md +1 -1
- package/rules/always-on/loop-and-monitor.md +17 -194
- package/rules/always-on/parallel-sessions.md +3 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +1 -1
- package/rules/opt-in-stack/security-web.md +5 -1
- package/scripts/agent-usage-census.mjs +79 -0
- package/scripts/apply-session-learnings.mjs +34 -6
- package/scripts/archive-closed-prds.mjs +21 -5
- package/scripts/autopilot.mjs +11 -5
- package/scripts/backfill-abandoned-sessions.mjs +13 -3
- package/scripts/backfill-evidence-digest.mjs +2 -1
- package/scripts/backfill-learnings-from-vault.mjs +61 -6
- package/scripts/backfill-sessions.mjs +16 -2
- package/scripts/check-package-manager.mjs +26 -8
- package/scripts/check-sessions-integrity.mjs +4 -1
- package/scripts/ci/assert-vitest-green.mjs +3 -0
- package/scripts/ci/windows-smoke.mjs +201 -0
- package/scripts/close-precheck.mjs +55 -0
- package/scripts/dialectic-deriver.mjs +20 -12
- package/scripts/emit-event.mjs +2 -2
- package/scripts/emit-session.mjs +239 -20
- package/scripts/eval-session.mjs +17 -3
- package/scripts/export-hw-learnings.mjs +29 -2
- package/scripts/fleet-checkin-audit.mjs +8 -4
- package/scripts/fleet-checkin.mjs +39 -27
- package/scripts/gc-stale-worktrees.mjs +6 -1
- package/scripts/github-export.mjs +128 -0
- package/scripts/github-protection-audit.mjs +3 -1
- package/scripts/handover-queue-check.mjs +209 -0
- package/scripts/harness-audit.mjs +6 -4
- package/scripts/lib/agent-frontmatter.mjs +57 -10
- package/scripts/lib/agent-status.mjs +125 -7
- package/scripts/lib/agent-usage-census.mjs +394 -0
- package/scripts/lib/auto-dialectic.mjs +17 -10
- package/scripts/lib/auto-dream.mjs +22 -14
- package/scripts/lib/autopilot/loop.mjs +3 -1
- package/scripts/lib/autopilot/recent-runs.mjs +4 -2
- package/scripts/lib/autopilot/stall-sampler.mjs +2 -1
- package/scripts/lib/autopilot/telemetry.mjs +5 -1
- package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
- package/scripts/lib/baseline-archetypes.mjs +10 -2
- package/scripts/lib/blocked-commands-policy.mjs +4 -3
- package/scripts/lib/build-live-signals.mjs +13 -9
- package/scripts/lib/ci-status-banner.mjs +329 -24
- package/scripts/lib/codex/plugin-contract.mjs +8 -0
- package/scripts/lib/cold-start-detector.mjs +5 -2
- package/scripts/lib/command-blocker.mjs +603 -43
- package/scripts/lib/common.mjs +3 -2
- package/scripts/lib/config/cross-repo.mjs +74 -7
- package/scripts/lib/config/host-paths.mjs +6 -2
- package/scripts/lib/config/section-extractor.mjs +65 -11
- package/scripts/lib/config/template-keys.mjs +153 -0
- package/scripts/lib/config/vault-integration.mjs +1 -0
- package/scripts/lib/config.mjs +2 -2
- package/scripts/lib/coordinator-snapshot.mjs +1 -1
- package/scripts/lib/discovery/triage-state.mjs +2 -1
- package/scripts/lib/dispatcher/rank.mjs +5 -7
- package/scripts/lib/ecosystem-health.mjs +2 -2
- package/scripts/lib/eval/engine.mjs +3 -2
- package/scripts/lib/eval/session-resolve.mjs +6 -2
- package/scripts/lib/eval/sink.mjs +4 -3
- package/scripts/lib/events-retention-banner.mjs +3 -2
- package/scripts/lib/events-schema.mjs +10 -0
- package/scripts/lib/events.mjs +23 -12
- package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
- package/scripts/lib/fleet-protocol.mjs +4 -0
- package/scripts/lib/gates/gate-full.mjs +172 -11
- package/scripts/lib/gates/gate-helpers.mjs +236 -23
- package/scripts/lib/git-config-drift.mjs +141 -7
- package/scripts/lib/github-export.mjs +447 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
- package/scripts/lib/handover-queue-marks.mjs +159 -0
- package/scripts/lib/hardware-pattern-detector.mjs +103 -13
- package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
- package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
- package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
- package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
- package/scripts/lib/host-identity.mjs +9 -5
- package/scripts/lib/http-request.mjs +64 -0
- package/scripts/lib/instruction-budget-guard.mjs +30 -22
- package/scripts/lib/io.mjs +6 -3
- package/scripts/lib/issue-budget-reconcile.mjs +3 -2
- package/scripts/lib/issue-budget.mjs +36 -7
- package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
- package/scripts/lib/learnings/io.mjs +289 -15
- package/scripts/lib/learnings/select.mjs +53 -4
- package/scripts/lib/learnings/store-role.mjs +209 -0
- package/scripts/lib/learnings/surface.mjs +46 -7
- package/scripts/lib/ledger-path.mjs +113 -0
- package/scripts/lib/maintenance-due-banner.mjs +63 -12
- package/scripts/lib/memory-banner.mjs +5 -6
- package/scripts/lib/memory-proposals/collector.mjs +5 -2
- package/scripts/lib/memory-proposals/sink.mjs +7 -4
- package/scripts/lib/memory-proposals/store.mjs +22 -5
- package/scripts/lib/mode-selector-accuracy.mjs +2 -1
- package/scripts/lib/no-follow-open.mjs +57 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
- package/scripts/lib/own-lock-root.mjs +95 -0
- package/scripts/lib/owner-interview.mjs +2 -1
- package/scripts/lib/owner-yaml.mjs +70 -7
- package/scripts/lib/platform.mjs +161 -9
- package/scripts/lib/plugin-update-banner.mjs +15 -11
- package/scripts/lib/pre-push-local-partial.mjs +117 -0
- package/scripts/lib/pre-push-remote-gate.mjs +151 -25
- package/scripts/lib/process-group.mjs +270 -34
- package/scripts/lib/project-hygiene.mjs +28 -0
- package/scripts/lib/quality-gate.mjs +33 -9
- package/scripts/lib/reconcile/backlog.mjs +29 -15
- package/scripts/lib/reconcile/engine.mjs +26 -2
- package/scripts/lib/reconcile/idempotency.mjs +65 -3
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
- package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
- package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
- package/scripts/lib/resource-probe/evaluate.mjs +32 -9
- package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
- package/scripts/lib/rule-loader.mjs +20 -10
- package/scripts/lib/scope-baseline.mjs +123 -8
- package/scripts/lib/scope-echo.mjs +5 -2
- package/scripts/lib/scope-gate.mjs +55 -20
- package/scripts/lib/session-close-backfill.mjs +75 -7
- package/scripts/lib/session-end/close-precheck.mjs +151 -0
- package/scripts/lib/session-end/phase-skip.mjs +8 -7
- package/scripts/lib/session-end/tail-runner.mjs +7 -2
- package/scripts/lib/session-id.mjs +9 -1
- package/scripts/lib/session-identity/own-session.mjs +63 -0
- package/scripts/lib/session-lock.mjs +114 -0
- package/scripts/lib/session-record-repair.mjs +245 -35
- package/scripts/lib/session-registry.mjs +72 -9
- package/scripts/lib/session-schema/constants.mjs +9 -0
- package/scripts/lib/session-schema/filters.mjs +56 -1
- package/scripts/lib/session-schema/validator.mjs +15 -5
- package/scripts/lib/session-start-probes.mjs +149 -22
- package/scripts/lib/session-token-rollup.mjs +2 -1
- package/scripts/lib/session-work-trees.mjs +166 -0
- package/scripts/lib/sessions-canonical.mjs +61 -3
- package/scripts/lib/sessions-integrity-banner.mjs +17 -39
- package/scripts/lib/sessions-staleness-banner.mjs +8 -5
- package/scripts/lib/sh-quote.mjs +1 -0
- package/scripts/lib/skill-evidence-window.mjs +5 -1
- package/scripts/lib/skill-health/join.mjs +10 -9
- package/scripts/lib/skill-invocations-schema.mjs +13 -0
- package/scripts/lib/skill-judgments-schema.mjs +6 -2
- package/scripts/lib/snapshot-probe.mjs +414 -0
- package/scripts/lib/so-hud/collect.mjs +49 -5
- package/scripts/lib/so-hud/runtime.mjs +15 -4
- package/scripts/lib/spiral-carryover.mjs +8 -1
- package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
- package/scripts/lib/state-md/idle-reset.mjs +14 -5
- package/scripts/lib/state-md/mission-status.mjs +5 -1
- package/scripts/lib/state-md-peer-guard.mjs +2 -0
- package/scripts/lib/state-md.mjs +31 -8
- package/scripts/lib/sunset/walker.mjs +4 -3
- package/scripts/lib/telemetry/consent.mjs +4 -2
- package/scripts/lib/telemetry/queue.mjs +3 -3
- package/scripts/lib/telemetry/sync.mjs +45 -9
- package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
- package/scripts/lib/tmux-layout/layouts.mjs +20 -3
- package/scripts/lib/ux-grill/paths.mjs +5 -1
- package/scripts/lib/validate/check-agents.mjs +37 -6
- package/scripts/lib/validate/check-component-paths.mjs +25 -1
- package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
- package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
- package/scripts/lib/validate/check-json-files.mjs +3 -1
- package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
- package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
- package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
- package/scripts/lib/validate/check-unwired-features.mjs +4 -2
- package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
- package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
- package/scripts/lib/validate/confidential-names.mjs +3 -1
- package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
- package/scripts/lib/vault-mirror/namespace.mjs +19 -7
- package/scripts/lib/vault-mirror/process.mjs +56 -27
- package/scripts/lib/vault-mirror/prune.mjs +5 -2
- package/scripts/lib/vault-mirror/render-learnings.mjs +3 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
- package/scripts/lib/vault-mirror/retention.mjs +2 -2
- package/scripts/lib/vault-mirror/utils.mjs +79 -0
- package/scripts/lib/vault-staleness-banner.mjs +4 -7
- package/scripts/lib/vault-status/board-writer.mjs +2 -1
- package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
- package/scripts/lib/win-process-tree.mjs +137 -0
- package/scripts/lib/workspace.mjs +1 -1
- package/scripts/lib/worktree/constants.mjs +1 -1
- package/scripts/lib/worktree/lifecycle.mjs +1 -1
- package/scripts/lib/worktree/listing.mjs +1 -1
- package/scripts/lib/zx.mjs +50 -0
- package/scripts/mcp-server.mjs +16 -7
- package/scripts/mcp-server.sh +13 -83
- package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
- package/scripts/memory-propose.mjs +10 -3
- package/scripts/migrate-cold-start-seed.mjs +7 -0
- package/scripts/migrate-legacy-learnings.sh +22 -0
- package/scripts/migrate-sessions-jsonl.mjs +16 -2
- package/scripts/migrate-vault-paths.mjs +29 -13
- package/scripts/parse-config.mjs +71 -14
- package/scripts/print-learnings-index.mjs +25 -2
- package/scripts/release-session-lock.mjs +94 -7
- package/scripts/release.mjs +451 -24
- package/scripts/relocate-vault-corpus.mjs +11 -7
- package/scripts/repair-invalid-sessions.mjs +68 -3
- package/scripts/resolve-session-invocation.mjs +10 -17
- package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
- package/scripts/run-quality-gate.mjs +33 -7
- package/scripts/self-update.mjs +56 -7
- package/scripts/session-commit-files.mjs +133 -0
- package/scripts/site-numbers.mjs +52 -9
- package/scripts/snapshot-probe.mjs +156 -0
- package/scripts/so-hud.mjs +29 -6
- package/scripts/state-md-idle-reset.mjs +2 -1
- package/scripts/sweep-expired-learnings.mjs +54 -6
- package/scripts/sweep-expired-rules.mjs +4 -1
- package/scripts/validate-plugin.mjs +22 -0
- package/scripts/vault-integration-watcher.mjs +54 -6
- package/scripts/vault-mirror.mjs +9 -0
- package/skills/_shared/fleet-protocol.md +142 -1
- package/skills/_shared/instruction-file-resolution.md +1 -1
- package/skills/_shared/monitor-patterns.md +44 -25
- package/skills/_shared/platform-tools.md +7 -0
- package/skills/_shared/state-ownership.md +3 -2
- package/skills/architecture/references/domain-model.md +1 -1
- package/skills/bootstrap/SKILL.md +3 -1
- package/skills/bootstrap/_shared-template.md +4 -4
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/bootstrap/private-contract.md +1 -1
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
- package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
- package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
- package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
- package/skills/claude-md-drift-check/SKILL.md +3 -3
- package/skills/claude-md-drift-check/checker.mjs +61 -101
- package/skills/close/SKILL.md +20 -7
- package/skills/discovery/probes/docs-staleness.mjs +7 -5
- package/skills/discovery/probes/frontend-slop.mjs +7 -3
- package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
- package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
- package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
- package/skills/discovery/probes/vault-staleness.mjs +5 -3
- package/skills/discovery/probes-session.md +3 -0
- package/skills/discovery/probes-vault.md +2 -0
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/eval/SKILL.md +3 -1
- package/skills/evolve/SKILL.md +2 -0
- package/skills/evolve/references/evolve-analyze-mode.md +3 -3
- package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
- package/skills/gitlab-ops/SKILL.md +7 -6
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- package/skills/go/SKILL.md +13 -1
- package/skills/npm-publish/SKILL.md +5 -2
- package/skills/plan/SKILL.md +23 -10
- package/skills/plan/mode-feature.md +13 -8
- package/skills/plan/mode-new.md +6 -14
- package/skills/plan/prd-feature-template.md +19 -0
- package/skills/plan/prd-full-template.md +12 -5
- package/skills/plan/prd-premortem-panel.md +48 -0
- package/skills/plan/prd-reviewer-prompt.md +11 -3
- package/skills/plan/soul.md +1 -1
- package/skills/portfolio/SKILL.md +5 -6
- package/skills/quality-gates/SKILL.md +2 -4
- package/skills/reconcile/SKILL.md +27 -4
- package/skills/release/SKILL.md +16 -8
- package/skills/session-end/SKILL.md +94 -11
- package/skills/session-end/phase-3-2-docs-verification.md +1 -0
- package/skills/session-end/phase-3-6-tail.md +11 -7
- package/skills/session-end/phase-3-7a-recommendations.md +9 -2
- package/skills/session-end/plan-verification.md +16 -1
- package/skills/session-end/references/phase-3-documentation-updates.md +14 -12
- package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
- package/skills/session-end/references/session-summary-template.md +4 -0
- package/skills/session-end/session-metrics-write.md +7 -4
- package/skills/session-plan/SKILL.md +5 -3
- package/skills/session-start/SKILL.md +9 -3
- package/skills/session-start/phase-4-5-resource-health.md +11 -5
- package/skills/session-start/phase-8-5-express-path.md +4 -3
- package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
- package/skills/session-start/references/phase-1-5-session-continuity.md +2 -2
- 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 +3 -2
- package/skills/tmux-layout/SKILL.md +2 -2
- package/skills/vault-sync/validator.mjs +3 -2
- package/skills/wave-executor/SKILL.md +5 -4
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
- package/skills/wave-executor/references/wave-loop-review.md +13 -5
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
- package/assets/og-card.png +0 -0
- package/assets/og-card.svg +0 -47
- package/assets/wave-lifecycle.svg +0 -98
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* G1 tool filter — Bash only
|
|
44
44
|
* G2 command must be a non-empty string
|
|
45
45
|
* G3 cheap regex PRE-FILTER — skip the tokenizer for plainly unrelated
|
|
46
|
-
* commands (see
|
|
46
|
+
* commands (see mayStage; it does not decide the question)
|
|
47
47
|
* G4 context gate — isWaveAgentContext(); exit 0 immediately for
|
|
48
48
|
* coordinator/manual commits (AC5 safe-default no-op)
|
|
49
49
|
* G4b tokenize + extract path operands; exit 0 when no `git add` statement
|
|
@@ -67,7 +67,8 @@
|
|
|
67
67
|
|
|
68
68
|
import { readStdin, emitAllow, writeJsonAtomicSync } from '../scripts/lib/io.mjs';
|
|
69
69
|
import { isWaveAgentContext } from '../scripts/lib/wave-context.mjs';
|
|
70
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
70
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
71
|
+
import { execFileSync } from 'node:child_process';
|
|
71
72
|
import path from 'node:path';
|
|
72
73
|
import os from 'node:os';
|
|
73
74
|
import crypto from 'node:crypto';
|
|
@@ -87,16 +88,38 @@ import { dotGitAncestor, launchDirFromEnv } from '../scripts/lib/platform.mjs';
|
|
|
87
88
|
* command?" — since #1404 that verdict belongs to {@link extractStagedPaths},
|
|
88
89
|
* which resolves the verb and subcommand properly.
|
|
89
90
|
*
|
|
90
|
-
* `git` and `add` must
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
91
|
+
* The words `git` and `add` must both occur — anywhere, in any order. Global
|
|
92
|
+
* flags between them are therefore tolerated at any length: `git -C sub add x`
|
|
93
|
+
* reaches the extractor, where `\bgit\s+add\b` used to drop it silently.
|
|
94
|
+
* `\badd\b` keeps `git addremote` out.
|
|
95
|
+
*
|
|
96
|
+
* Why not "same statement, within N characters" (#1670 item 6): the previous
|
|
97
|
+
* `\bgit\b[^;&|\n]{0,120}\badd\b` let `git -C <long path> -c k=v … add x`, a
|
|
98
|
+
* quoted `;`/`&`/`|` in a `-c` value, and a `\`-newline continuation pass
|
|
99
|
+
* UNFENCED — this pre-filter is the fence's only early exit, so every miss
|
|
100
|
+
* fails OPEN. Dropping the bound (`[^;&|\n]*`) is not an option either: it
|
|
101
|
+
* backtracks once per `git` occurrence, quadratic on attacker-influenced text
|
|
102
|
+
* (measured 2026-10-18: 200 kB of `git ` → 6220 ms, past the 5 s hook budget;
|
|
103
|
+
* two independent word tests → 0.16 ms). Two `\b` tests are linear.
|
|
95
104
|
*
|
|
96
105
|
* False positives are cheap AND inert: `git commit -m "add x"` matches here,
|
|
97
106
|
* resolves to no `git add` statement, and writes nothing (G4b).
|
|
107
|
+
*
|
|
108
|
+
* `stage` is git's built-in synonym for `add` (#1671 item 5).
|
|
109
|
+
*
|
|
110
|
+
* NAMED CEILING (BV-004, #1671 item 5): a subcommand spelled with shell quoting
|
|
111
|
+
* or escaping — `git a\dd x`, `git a''dd x` — misses both this pre-filter and
|
|
112
|
+
* the extractor's unquoted-subcommand check, so that staging is not recorded.
|
|
113
|
+
* The fence only RECORDS (it never blocks), so the loss is a collision entry,
|
|
114
|
+
* not a bypassed denial. REVISIT TRIGGER: a wave agent observed staging with a
|
|
115
|
+
* quoted/escaped subcommand, or the fence starting to block.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} command
|
|
118
|
+
* @returns {boolean}
|
|
98
119
|
*/
|
|
99
|
-
|
|
120
|
+
function mayStage(command) {
|
|
121
|
+
return /\b(?:add|stage)\b/.test(command) && /\bgit\b/.test(command);
|
|
122
|
+
}
|
|
100
123
|
|
|
101
124
|
/**
|
|
102
125
|
* Recorded in place of a path list when the staging command names NO explicit
|
|
@@ -114,6 +137,9 @@ const GIT_GLOBAL_VALUE_FLAGS = new Set([
|
|
|
114
137
|
'-C', '-c', '--git-dir', '--work-tree', '--namespace', '--exec-path', '--config-env',
|
|
115
138
|
]);
|
|
116
139
|
|
|
140
|
+
/** `git add` and its built-in synonym `git stage` (#1671 item 5). */
|
|
141
|
+
const GIT_ADD_SUBCOMMANDS = new Set(['add', 'stage']);
|
|
142
|
+
|
|
117
143
|
/** `git add` flags that consume the NEXT token as their value. */
|
|
118
144
|
const GIT_ADD_VALUE_FLAGS = new Set(['--chmod']);
|
|
119
145
|
|
|
@@ -211,7 +237,7 @@ function normalizeStagedPath(raw) {
|
|
|
211
237
|
* blocked commit, which the hook's fail-safe posture forbids. This is also
|
|
212
238
|
* why a blanket "pre-filter hit but nothing resolved → `paths: ['*']`"
|
|
213
239
|
* floor was REJECTED at the 2026-09-20 fix: `git commit -m "add x"` matches
|
|
214
|
-
* {@link
|
|
240
|
+
* {@link mayStage}, and the marker overlaps EVERY staged path, so that
|
|
215
241
|
* floor manufactures a collision for a command that stages nothing.
|
|
216
242
|
* - A staging command a shell never executes locally is not recorded, and
|
|
217
243
|
* should not be: `ssh host 'git add x'` stages a REMOTE index, and
|
|
@@ -311,7 +337,7 @@ function scanForStagedPaths(command, lib, paths, state, depth) {
|
|
|
311
337
|
// The G3 pre-filter again, on the payload: same cheap question, same
|
|
312
338
|
// tolerated false positives, and it keeps an unrelated `-c` operand
|
|
313
339
|
// from paying for a second tokenize.
|
|
314
|
-
if (typeof payload === 'string' &&
|
|
340
|
+
if (typeof payload === 'string' && mayStage(payload)) {
|
|
315
341
|
scanForStagedPaths(payload, lib, paths, state, depth + 1);
|
|
316
342
|
}
|
|
317
343
|
}
|
|
@@ -335,7 +361,7 @@ function scanForStagedPaths(command, lib, paths, state, depth) {
|
|
|
335
361
|
i += 1; // boolean global flag, or attached `--git-dir=<p>`
|
|
336
362
|
}
|
|
337
363
|
|
|
338
|
-
if (i >= segment.length || segment[i].quoted || segment[i].text
|
|
364
|
+
if (i >= segment.length || segment[i].quoted || !GIT_ADD_SUBCOMMANDS.has(segment[i].text)) continue;
|
|
339
365
|
state.recognised = true;
|
|
340
366
|
i += 1;
|
|
341
367
|
|
|
@@ -485,6 +511,218 @@ function appendIntent({ fenceFile, agentId, command, paths }) {
|
|
|
485
511
|
return writeJsonAtomicSync(fenceFile, body, { tmpPrefix: '.fence.tmp' });
|
|
486
512
|
}
|
|
487
513
|
|
|
514
|
+
// ---------------------------------------------------------------------------
|
|
515
|
+
// Work-tree recorder (#1659)
|
|
516
|
+
// ---------------------------------------------------------------------------
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Cheap PRE-FILTER for the work-tree recorder — same shape and same role as
|
|
520
|
+
* {@link mayStage}, for the same reasons (#1670 item 6: a bounded lookahead
|
|
521
|
+
* silently skipped `git -C <long path> -c … commit`): only a hit pays for the
|
|
522
|
+
* tokenizer and a git spawn. False positives (`echo "git commit"`) resolve to
|
|
523
|
+
* no commit statement and record nothing.
|
|
524
|
+
*
|
|
525
|
+
* @param {string} command
|
|
526
|
+
* @returns {boolean}
|
|
527
|
+
*/
|
|
528
|
+
function mayCommit(command) {
|
|
529
|
+
return /\bcommit\b/.test(command) && /\bgit\b/.test(command);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Timeout for the one `git rev-parse` per commit tree (hook budget is 5 s). */
|
|
533
|
+
const RECORDER_GIT_TIMEOUT_MS = 2000;
|
|
534
|
+
|
|
535
|
+
/** Directory-changing verbs whose effect the walker does not model (#1659). */
|
|
536
|
+
const DIR_UNKNOWN_VERBS = new Set(['pushd', 'popd', 'chdir']);
|
|
537
|
+
/** Builtins that change the directory when invoked as `builtin <name>` (#1659). */
|
|
538
|
+
const DIR_CHANGE_BUILTINS = new Set(['cd', 'pushd', 'popd', 'chdir']);
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Expand a `cd`/`-C` operand against the current directory. Returns null when
|
|
542
|
+
* the operand cannot be resolved without a shell (`$VAR`, `~user`, `-`), so
|
|
543
|
+
* the caller records nothing rather than a guessed tree.
|
|
544
|
+
*
|
|
545
|
+
* @param {string} base - absolute current directory
|
|
546
|
+
* @param {string} operand
|
|
547
|
+
* @returns {string|null}
|
|
548
|
+
*/
|
|
549
|
+
function resolveDirOperand(base, operand) {
|
|
550
|
+
if (operand === '~') return os.homedir();
|
|
551
|
+
if (operand.startsWith('~/')) return path.join(os.homedir(), operand.slice(2));
|
|
552
|
+
if (operand === '-' || operand.startsWith('~') || operand.includes('$') || operand.includes('`')) return null;
|
|
553
|
+
return path.resolve(base, operand);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Effective directories of every `git commit` statement in a command.
|
|
558
|
+
*
|
|
559
|
+
* Walks the chain segments in order, tracking the directory the shell would be
|
|
560
|
+
* in: a `cd <dir>` statement moves it (relative to the previous one), and each
|
|
561
|
+
* `-C <dir>` global flag of the commit statement composes on top, as git does.
|
|
562
|
+
* A `cd` whose operand is unresolvable (`cd "$WT"`) makes the directory
|
|
563
|
+
* UNKNOWN for the rest of the command — later commits are not recorded rather
|
|
564
|
+
* than misattributed to the launch tree. So does any directory change the
|
|
565
|
+
* walker does not model (`pushd`, `popd`, `chdir`, `builtin cd`).
|
|
566
|
+
*
|
|
567
|
+
* NAMED CEILINGS (BV-004): subshell scope is not modelled — `(cd x && git
|
|
568
|
+
* commit); git commit` attributes the second commit to `x` too; a commit nested
|
|
569
|
+
* in `bash -c "…"` is not recorded. REVISIT if a session's commits are found
|
|
570
|
+
* missing from `orchestrator.session.work_tree` for one of these shapes.
|
|
571
|
+
*
|
|
572
|
+
* @param {string} command
|
|
573
|
+
* @param {string} startDir - absolute directory the command starts in
|
|
574
|
+
* @param {{ tokenizeCommand: Function, splitChainSegments: Function,
|
|
575
|
+
* resolveSegmentVerb: Function }} lib
|
|
576
|
+
* @returns {string[]} distinct absolute directories
|
|
577
|
+
*/
|
|
578
|
+
function commitDirs(command, startDir, lib) {
|
|
579
|
+
const out = new Set();
|
|
580
|
+
let cwd = startDir;
|
|
581
|
+
const tokens = lib.tokenizeCommand(command);
|
|
582
|
+
// No paren stripping here: since #1667 the shared tokenizer turns subshell
|
|
583
|
+
// parens into statement breaks and appends `$( )`/backtick bodies as their
|
|
584
|
+
// own statements. A hand strip on top only ever changed `((git commit))`
|
|
585
|
+
// — arithmetic, not a commit — into a recorded commit (#1670).
|
|
586
|
+
//
|
|
587
|
+
// Those hoisted bodies sit AFTER every outer statement, behind the first
|
|
588
|
+
// `operator: 'substitution'` separator, so walking them in stream order would
|
|
589
|
+
// judge `cd sub && echo $(git commit); cd ../other` against `other`. Walk the
|
|
590
|
+
// outer statements first; the bodies then run in the start directory only
|
|
591
|
+
// when no outer statement moved it, and in an UNKNOWN directory otherwise
|
|
592
|
+
// (not recorded rather than misattributed).
|
|
593
|
+
const cut = tokens.findIndex((t) => t.operator === 'substitution');
|
|
594
|
+
const outer = cut < 0 ? tokens : tokens.slice(0, cut);
|
|
595
|
+
const hoistedSegments = cut < 0 ? [] : lib.splitChainSegments(tokens.slice(cut));
|
|
596
|
+
const outerSegments = lib.splitChainSegments(outer);
|
|
597
|
+
let moved = false;
|
|
598
|
+
const segments = [...outerSegments, null, ...hoistedSegments];
|
|
599
|
+
for (const segment of segments) {
|
|
600
|
+
if (segment === null) {
|
|
601
|
+
// Boundary between the outer statements and the hoisted bodies.
|
|
602
|
+
cwd = moved ? null : startDir;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (segment.length === 0) continue;
|
|
606
|
+
const { verb, index } = lib.resolveSegmentVerb(segment);
|
|
607
|
+
if (index < 0) continue;
|
|
608
|
+
|
|
609
|
+
// A directory change the walker does not model (`pushd`, `popd`, zsh's
|
|
610
|
+
// `chdir`, `builtin cd …`) makes the directory UNKNOWN, like an
|
|
611
|
+
// unresolvable `cd` operand — never misattributed to the old tree (#1659).
|
|
612
|
+
if (!segment[index].quoted && (DIR_UNKNOWN_VERBS.has(verb)
|
|
613
|
+
|| (verb === 'builtin' && index + 1 < segment.length && DIR_CHANGE_BUILTINS.has(segment[index + 1].text)))) {
|
|
614
|
+
cwd = null;
|
|
615
|
+
moved = true;
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (verb === 'cd' && !segment[index].quoted) {
|
|
620
|
+
moved = true;
|
|
621
|
+
if (cwd === null) continue;
|
|
622
|
+
let operand = null;
|
|
623
|
+
for (let j = index + 1; j < segment.length; j++) {
|
|
624
|
+
const t = segment[j];
|
|
625
|
+
if (!t.quoted && t.text.startsWith('-') && t.text !== '-') continue; // -P / -L
|
|
626
|
+
operand = t.text;
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
cwd = operand === null ? os.homedir() : resolveDirOperand(cwd, operand);
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (verb !== 'git' || cwd === null) continue;
|
|
634
|
+
let dir = cwd;
|
|
635
|
+
let i = index + 1;
|
|
636
|
+
while (i < segment.length) {
|
|
637
|
+
const tok = segment[i];
|
|
638
|
+
if (tok.quoted || !tok.text.startsWith('-') || tok.text === '-') break;
|
|
639
|
+
if (GIT_GLOBAL_VALUE_FLAGS.has(tok.text)) {
|
|
640
|
+
if (tok.text === '-C' && i + 1 < segment.length && dir !== null) {
|
|
641
|
+
dir = resolveDirOperand(dir, segment[i + 1].text);
|
|
642
|
+
}
|
|
643
|
+
i += 2;
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
i += 1;
|
|
647
|
+
}
|
|
648
|
+
if (i < segment.length && !segment[i].quoted && segment[i].text === 'commit' && dir !== null) {
|
|
649
|
+
out.add(dir);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return [...out];
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Record `(tree, start_ref)` for every tree a `git commit` in this command
|
|
657
|
+
* targets, as an `orchestrator.session.work_tree` event (#1659).
|
|
658
|
+
*
|
|
659
|
+
* Runs for EVERY session context (coordinator and wave agents alike), before
|
|
660
|
+
* the fence gates. Best-effort and side-effect-only: it never throws, never
|
|
661
|
+
* blocks, and never touches the hook's decision — the caller emits the same
|
|
662
|
+
* allow either way. One record per commit; the READER dedupes (first record
|
|
663
|
+
* per tree wins), so no ledger read happens on this path.
|
|
664
|
+
*
|
|
665
|
+
* Cost on a command without a commit: one or two linear word tests.
|
|
666
|
+
*
|
|
667
|
+
* @param {object} input - the hook payload
|
|
668
|
+
* @param {string} command
|
|
669
|
+
* @returns {Promise<void>}
|
|
670
|
+
*/
|
|
671
|
+
async function recordWorkTrees(input, command) {
|
|
672
|
+
try {
|
|
673
|
+
if (!mayCommit(command)) return;
|
|
674
|
+
const sessionId = typeof input?.session_id === 'string' ? input.session_id.trim() : '';
|
|
675
|
+
if (sessionId === '') return;
|
|
676
|
+
const payloadCwd = typeof input?.cwd === 'string' ? input.cwd.trim() : '';
|
|
677
|
+
const startDir = path.isAbsolute(payloadCwd) ? payloadCwd : process.cwd();
|
|
678
|
+
|
|
679
|
+
const lib = await import('../scripts/lib/command-blocker.mjs');
|
|
680
|
+
const dirs = commitDirs(command, startDir, lib);
|
|
681
|
+
if (dirs.length === 0) return;
|
|
682
|
+
|
|
683
|
+
const pairs = new Map();
|
|
684
|
+
for (const dir of dirs) {
|
|
685
|
+
let lines;
|
|
686
|
+
try {
|
|
687
|
+
lines = execFileSync('git', ['-C', dir, 'rev-parse', '--show-toplevel', 'HEAD'], {
|
|
688
|
+
encoding: 'utf8',
|
|
689
|
+
timeout: RECORDER_GIT_TIMEOUT_MS,
|
|
690
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
691
|
+
}).split('\n');
|
|
692
|
+
} catch {
|
|
693
|
+
continue; // not a repo, dir gone, or unborn HEAD — nothing to anchor on
|
|
694
|
+
}
|
|
695
|
+
const top = (lines[0] || '').trim();
|
|
696
|
+
const head = (lines[1] || '').trim();
|
|
697
|
+
if (top === '' || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(head)) continue;
|
|
698
|
+
let tree = top;
|
|
699
|
+
try { tree = realpathSync(top); } catch { /* keep git's spelling */ }
|
|
700
|
+
if (!pairs.has(tree)) pairs.set(tree, head);
|
|
701
|
+
}
|
|
702
|
+
if (pairs.size === 0) return;
|
|
703
|
+
|
|
704
|
+
const { emitEvent } = await import('../scripts/lib/events.mjs');
|
|
705
|
+
const { SESSION_WORK_TREE_EVENT } = await import('../scripts/lib/events-schema.mjs');
|
|
706
|
+
for (const [tree, startRef] of pairs) {
|
|
707
|
+
try {
|
|
708
|
+
await emitEvent(SESSION_WORK_TREE_EVENT, {
|
|
709
|
+
session_id: sessionId,
|
|
710
|
+
tree,
|
|
711
|
+
start_ref: startRef,
|
|
712
|
+
});
|
|
713
|
+
} catch (err) {
|
|
714
|
+
process.stderr.write(
|
|
715
|
+
`⚠ pre-bash-staging-fence: work-tree record not written — ${err?.message || err}\n`,
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
} catch (err) {
|
|
720
|
+
process.stderr.write(
|
|
721
|
+
`⚠ pre-bash-staging-fence: work-tree recorder failed — ${err?.message || err}\n`,
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
488
726
|
// ---------------------------------------------------------------------------
|
|
489
727
|
// Main
|
|
490
728
|
// ---------------------------------------------------------------------------
|
|
@@ -500,9 +738,13 @@ async function main() {
|
|
|
500
738
|
const command = input?.tool_input?.command;
|
|
501
739
|
if (typeof command !== 'string' || command.length === 0) return emitAllow();
|
|
502
740
|
|
|
741
|
+
// #1659 — record (tree, start_ref) before any `git commit`, in every session
|
|
742
|
+
// context. Side effect only: it cannot change the decision below.
|
|
743
|
+
await recordWorkTrees(input, command);
|
|
744
|
+
|
|
503
745
|
// G3 — cheap regex PRE-FILTER. Commands that cannot possibly be a staging
|
|
504
746
|
// command pass through without loading the tokenizer.
|
|
505
|
-
if (!
|
|
747
|
+
if (!mayStage(command)) return emitAllow();
|
|
506
748
|
|
|
507
749
|
// G4 — context gate. Coordinator/manual commits are not fenced.
|
|
508
750
|
if (!isWaveAgentContext()) return emitAllow();
|
|
@@ -541,8 +783,10 @@ if (isMainModule(import.meta.url)) {
|
|
|
541
783
|
if (!shouldRunHook('pre-bash-staging-fence')) process.exit(0);
|
|
542
784
|
|
|
543
785
|
// Top-level error handler — never let exit 1 leak. Fail-open on internal
|
|
544
|
-
// errors
|
|
545
|
-
//
|
|
786
|
+
// errors on purpose (#1679 point 3): this hook only LOGS staging intent and
|
|
787
|
+
// never blocks (G6); the commit guard reconciles. Unlike the destructive
|
|
788
|
+
// guard, which denies on an internal error since #1671, a deny here would turn
|
|
789
|
+
// the logger into a gate. Revisit if a fence record is missed because of one.
|
|
546
790
|
main().catch((e) => {
|
|
547
791
|
process.stderr.write(
|
|
548
792
|
`⚠ pre-bash-staging-fence: internal error — ${e?.message || e}\n`,
|
|
@@ -38,8 +38,9 @@
|
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
40
|
import { readStdin, emitAllow, emitDeny } from '../scripts/lib/io.mjs';
|
|
41
|
-
import { resolveProjectDir, resolvePluginRoot } from '../scripts/lib/platform.mjs';
|
|
41
|
+
import { resolveProjectDir, resolvePluginRoot, resolveEventsLedgerRoot } from '../scripts/lib/platform.mjs';
|
|
42
42
|
import { readJson } from '../scripts/lib/common.mjs';
|
|
43
|
+
import { readProcessLocalSessionIds } from '../scripts/lib/session-identity/own-session.mjs';
|
|
43
44
|
import { hasReadInSession } from './_lib/transcript-history.mjs';
|
|
44
45
|
import { resolveHost, matchesBypass } from './_lib/vcs-create-matcher.mjs';
|
|
45
46
|
|
|
@@ -219,7 +220,8 @@ function resolvePolicyPath(projectDir) {
|
|
|
219
220
|
*
|
|
220
221
|
* { "<session_id>": { "acknowledgedAt": "<ISO timestamp>" } }
|
|
221
222
|
*
|
|
222
|
-
*
|
|
223
|
+
* The `/templates-ack` command (commands/templates-ack.md, #1589/#1591) writes
|
|
224
|
+
* this file; the hook only reads it.
|
|
223
225
|
*
|
|
224
226
|
* @param {string} ackFilePath
|
|
225
227
|
* @param {string|null} sessionId
|
|
@@ -239,29 +241,25 @@ async function isAcknowledged(ackFilePath, sessionId) {
|
|
|
239
241
|
}
|
|
240
242
|
|
|
241
243
|
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
244
|
+
* Require this invocation's ID for acknowledgement identity. Missing, malformed
|
|
245
|
+
* or conflicting IDs cannot fall back to env. Parent IDs and shared checkout
|
|
246
|
+
* state never establish an acknowledgement identity.
|
|
245
247
|
*
|
|
246
248
|
* @param {object|null} input
|
|
247
|
-
* @
|
|
248
|
-
* @returns {Promise<string|null>}
|
|
249
|
+
* @returns {string|null}
|
|
249
250
|
*/
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
} catch {
|
|
264
|
-
// ignore — null below
|
|
251
|
+
function resolveSessionId(input) {
|
|
252
|
+
const keys = ['session_id', 'sessionId'].filter((key) =>
|
|
253
|
+
Object.hasOwn(input ?? {}, key),
|
|
254
|
+
);
|
|
255
|
+
if (keys.length > 0) {
|
|
256
|
+
const ids = keys.map((key) => input[key]);
|
|
257
|
+
if (ids.some((id) => typeof id !== 'string' || !id.trim())) return null;
|
|
258
|
+
const trimmed = ids.map((id) => id.trim());
|
|
259
|
+
if (new Set(trimmed).size !== 1) return null;
|
|
260
|
+
// Pass only the invocation ID: the helper's parent/env union is not an
|
|
261
|
+
// acknowledgement identity and must not grant a peer's bypass.
|
|
262
|
+
return readProcessLocalSessionIds({ env: {}, hookInput: { session_id: trimmed[0] } })[0] ?? null;
|
|
265
263
|
}
|
|
266
264
|
return null;
|
|
267
265
|
}
|
|
@@ -330,12 +328,15 @@ async function main() {
|
|
|
330
328
|
typeof policy.acknowledgement_file === 'string' && policy.acknowledgement_file.length > 0
|
|
331
329
|
? policy.acknowledgement_file
|
|
332
330
|
: DEFAULT_ACK_PATH;
|
|
331
|
+
// A relative ack file is runtime state, not a tracked file: resolve it under
|
|
332
|
+
// the MAIN checkout for a linked worktree, the same root `/templates-ack`
|
|
333
|
+
// writes to — otherwise the two sides split across roots (sweep of #1729).
|
|
333
334
|
const ackFile = path.isAbsolute(ackRel)
|
|
334
335
|
? ackRel
|
|
335
|
-
: path.join(projectDir || process.cwd(), ackRel);
|
|
336
|
+
: path.join(resolveEventsLedgerRoot(projectDir || process.cwd()), ackRel);
|
|
336
337
|
|
|
337
338
|
// G6 — acknowledgement-file check.
|
|
338
|
-
const sessionId =
|
|
339
|
+
const sessionId = resolveSessionId(input);
|
|
339
340
|
if (await isAcknowledged(ackFile, sessionId)) {
|
|
340
341
|
return emitAllow();
|
|
341
342
|
}
|
|
@@ -430,9 +431,9 @@ if (isMainModule(import.meta.url)) {
|
|
|
430
431
|
// per session.
|
|
431
432
|
if (!shouldRunHook('pre-bash-templates-first')) process.exit(0);
|
|
432
433
|
|
|
433
|
-
// Top-level error handler — never let exit 1 leak.
|
|
434
|
-
//
|
|
435
|
-
//
|
|
434
|
+
// Top-level error handler — never let exit 1 leak. This hook is advisory, so
|
|
435
|
+
// it fails open on internal errors to avoid blocking legitimate work (unlike
|
|
436
|
+
// pre-bash-destructive-guard.mjs, which denies on a main() error, #1671).
|
|
436
437
|
main().catch((e) => {
|
|
437
438
|
process.stderr.write(
|
|
438
439
|
`⚠ pre-bash-templates-first: internal error — ${e?.message || e}\n`,
|
package/hooks/run-node.sh
CHANGED
|
@@ -66,7 +66,7 @@ if [ -n "$expired" ]; then
|
|
|
66
66
|
echo "session-orchestrator: 'node' not found on the hook PATH — plugin hooks are skipped."
|
|
67
67
|
echo " Fix: install Node.js 24+, expose it on the harness PATH, or set SO_NODE_BIN=/abs/path/to/node."
|
|
68
68
|
echo " (Hook shells do not source ~/.zshrc — Homebrew/nvm/volta installs can be invisible here."
|
|
69
|
-
echo " Rate-limited to once per 6h. See
|
|
69
|
+
echo " Rate-limited to once per 6h. See docs/USER-GUIDE.md § Node is missing from the hook PATH.)"
|
|
70
70
|
} >&2
|
|
71
71
|
fi
|
|
72
72
|
exit 0
|
|
@@ -33,7 +33,8 @@ import { spawn } from 'node:child_process';
|
|
|
33
33
|
|
|
34
34
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
35
35
|
import { appendSkillInvocation } from '../scripts/lib/skill-invocations-schema.mjs';
|
|
36
|
-
import {
|
|
36
|
+
import { resolveEventsLedgerRoot } from '../scripts/lib/platform.mjs';
|
|
37
|
+
import { resolveLedgerPath } from '../scripts/lib/ledger-path.mjs';
|
|
37
38
|
import { shouldDailyFlush } from '../scripts/lib/telemetry/sync.mjs';
|
|
38
39
|
import { resolveConsent, readTelemetryState } from '../scripts/lib/telemetry/consent.mjs';
|
|
39
40
|
import { loadOwnerConfig } from '../scripts/lib/owner-yaml.mjs';
|
|
@@ -44,19 +45,25 @@ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
|
44
45
|
// ---------------------------------------------------------------------------
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
48
|
+
* Ledger paths. FUNCTIONS, not module-level consts: resolving the project dir
|
|
49
|
+
* walks the filesystem on first call, and this hook must not pay that at
|
|
50
|
+
* import time (#1153 P5).
|
|
51
|
+
*
|
|
52
|
+
* The two resolve independently (#1722): `skill-invocations.jsonl` goes
|
|
53
|
+
* through {@link resolveLedgerPath} (the main checkout's copy only when the
|
|
54
|
+
* launch dir is a linked worktree that gitignores it), while the metrics dir
|
|
55
|
+
* holding `sessions.jsonl` maps unconditionally via resolveEventsLedgerRoot
|
|
56
|
+
* (#1514/#1584).
|
|
50
57
|
*
|
|
51
58
|
* @returns {string}
|
|
52
59
|
*/
|
|
53
|
-
function
|
|
54
|
-
return path.join(
|
|
60
|
+
function sessionsMetricsDirPath() {
|
|
61
|
+
return path.join(resolveEventsLedgerRoot(), '.orchestrator', 'metrics');
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
/** @returns {string} */
|
|
58
65
|
function jsonlPath() {
|
|
59
|
-
return
|
|
66
|
+
return resolveLedgerPath('.orchestrator/metrics/skill-invocations.jsonl');
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
/** Absolute path to the telemetry CLI (carries the hidden `_flush` subcommand). */
|
|
@@ -136,7 +143,7 @@ export function maybeSpawnDailyFlush({
|
|
|
136
143
|
now = Date.now(),
|
|
137
144
|
statePath,
|
|
138
145
|
queuePath,
|
|
139
|
-
metricsDir =
|
|
146
|
+
metricsDir = sessionsMetricsDirPath(),
|
|
140
147
|
} = {}) {
|
|
141
148
|
try {
|
|
142
149
|
// 1. Cheapest gate: env kill-switches, no file I/O.
|
|
@@ -108,12 +108,14 @@ import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
|
108
108
|
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
109
109
|
|
|
110
110
|
import fs from 'node:fs';
|
|
111
|
-
import path from 'node:path';
|
|
112
111
|
import { appendSubagent } from '../scripts/lib/subagents-schema.mjs';
|
|
113
112
|
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
113
|
+
import { resolveLedgerPath } from '../scripts/lib/ledger-path.mjs';
|
|
114
114
|
import { resolveSubagentSidecar } from './_lib/subagent-paths.mjs';
|
|
115
115
|
import { readTailWindow } from '../scripts/lib/tail-window.mjs';
|
|
116
116
|
import { COST_BASIS } from '../scripts/lib/telemetry/pricing.mjs';
|
|
117
|
+
import { setLifecycle } from '../scripts/lib/agent-status.mjs';
|
|
118
|
+
import { rawPayloadSessionId, resolveOwnLockRoot } from './_lib/hook-root.mjs';
|
|
117
119
|
|
|
118
120
|
// ---------------------------------------------------------------------------
|
|
119
121
|
// Constants
|
|
@@ -127,7 +129,9 @@ import { COST_BASIS } from '../scripts/lib/telemetry/pricing.mjs';
|
|
|
127
129
|
* @returns {string}
|
|
128
130
|
*/
|
|
129
131
|
function jsonlPath() {
|
|
130
|
-
|
|
132
|
+
// #1722: the main checkout's copy when the launch dir is a linked worktree
|
|
133
|
+
// that gitignores the ledger (emit-session.mjs reads it there).
|
|
134
|
+
return resolveLedgerPath('.orchestrator/metrics/subagents.jsonl');
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
/**
|
|
@@ -650,7 +654,9 @@ function resolveSubagentTranscriptPath(parentTranscriptPath, agentId) {
|
|
|
650
654
|
|
|
651
655
|
/**
|
|
652
656
|
* Scan the tail of the ledger backwards for the most recent 'start' record with
|
|
653
|
-
* the given agent_id and return its timestamp in epoch-ms (#917)
|
|
657
|
+
* the given agent_id and return its timestamp in epoch-ms (#917) plus its
|
|
658
|
+
* `agent_type` (#1617), so a stop whose payload carries no type can inherit it
|
|
659
|
+
* from the same single tail read — no second scan on the hot path.
|
|
654
660
|
*
|
|
655
661
|
* Reads only the last START_JOIN_TAIL_BYTES rather than the whole file — see that
|
|
656
662
|
* constant for the measured sizing rationale. Scanning backwards means the FIRST
|
|
@@ -658,20 +664,22 @@ function resolveSubagentTranscriptPath(parentTranscriptPath, agentId) {
|
|
|
658
664
|
* reused across sessions (measured 2026-07-30: 22 of 1420 distinct start ids
|
|
659
665
|
* appeared more than once).
|
|
660
666
|
*
|
|
661
|
-
* NEVER throws. Any failure (missing file, unreadable, no match
|
|
662
|
-
*
|
|
667
|
+
* NEVER throws. Any failure (missing file, unreadable, no match) yields
|
|
668
|
+
* `{ startedAtMs: null, agentType: null }` so the caller falls back to an honest
|
|
669
|
+
* "unknown". An unparseable start timestamp nulls only `startedAtMs`.
|
|
663
670
|
*
|
|
664
671
|
* @param {string} filePath — ledger path
|
|
665
672
|
* @param {string} agentId — the stopping agent's id
|
|
666
|
-
* @returns {number|null
|
|
673
|
+
* @returns {{ startedAtMs: number|null, agentType: string|null }}
|
|
667
674
|
*/
|
|
668
|
-
function
|
|
675
|
+
function findStartRecord(filePath, agentId) {
|
|
676
|
+
const none = { startedAtMs: null, agentType: null };
|
|
669
677
|
try {
|
|
670
|
-
if (typeof agentId !== 'string' || !agentId.trim()) return
|
|
671
|
-
if (!fs.existsSync(filePath)) return
|
|
678
|
+
if (typeof agentId !== 'string' || !agentId.trim()) return none;
|
|
679
|
+
if (!fs.existsSync(filePath)) return none;
|
|
672
680
|
|
|
673
681
|
const window = readTailWindow(filePath, START_JOIN_TAIL_BYTES);
|
|
674
|
-
if (window.size === 0) return
|
|
682
|
+
if (window.size === 0) return none;
|
|
675
683
|
|
|
676
684
|
const lines = window.text.split('\n');
|
|
677
685
|
// When the window does not cover the whole file, the first element is a
|
|
@@ -692,11 +700,13 @@ function findStartTimestampMs(filePath, agentId) {
|
|
|
692
700
|
}
|
|
693
701
|
if (!obj || obj.event !== 'start' || obj.agent_id !== agentId) continue;
|
|
694
702
|
const ms = Date.parse(obj.timestamp);
|
|
695
|
-
|
|
703
|
+
const agentType =
|
|
704
|
+
typeof obj.agent_type === 'string' && obj.agent_type.trim() ? obj.agent_type : null;
|
|
705
|
+
return { startedAtMs: Number.isFinite(ms) ? ms : null, agentType };
|
|
696
706
|
}
|
|
697
|
-
return
|
|
707
|
+
return none;
|
|
698
708
|
} catch {
|
|
699
|
-
return
|
|
709
|
+
return none;
|
|
700
710
|
}
|
|
701
711
|
}
|
|
702
712
|
|
|
@@ -798,7 +808,23 @@ async function main() {
|
|
|
798
808
|
// duration measurement and the explicit orphan discriminator below.
|
|
799
809
|
// 'unknown' is the agent-id fallback for a payload with no usable id —
|
|
800
810
|
// joining on it would collide across unrelated agents, so never scan for it.
|
|
801
|
-
const
|
|
811
|
+
const startJoin = agentId === 'unknown'
|
|
812
|
+
? { startedAtMs: null, agentType: null }
|
|
813
|
+
: findStartRecord(jsonlPath(), agentId);
|
|
814
|
+
const { startedAtMs } = startJoin;
|
|
815
|
+
|
|
816
|
+
// agent_type inheritance (#1617): the stop payload does not always carry the
|
|
817
|
+
// type, while the start record for the same agent_id does. Payload wins when
|
|
818
|
+
// present; otherwise take it from the start record found by the join above.
|
|
819
|
+
// CEILING: this cannot type the phantom class (#939/#949) — those stops have
|
|
820
|
+
// no start record at all. Almost all type-less stop records are phantom
|
|
821
|
+
// stops (subagent_transcript_found: false), which this join cannot type;
|
|
822
|
+
// records from before #949 carry no flag at all. It closes the class for
|
|
823
|
+
// real agents whose stop payload omits the type.
|
|
824
|
+
// Readers counting by type must still filter on subagent_transcript_found.
|
|
825
|
+
if (agentType === null && startJoin.agentType !== null) {
|
|
826
|
+
record.agent_type = startJoin.agentType;
|
|
827
|
+
}
|
|
802
828
|
|
|
803
829
|
// duration_ms (#917): prefer the harness value, else join backwards to this
|
|
804
830
|
// agent's own start record, else null. Never 0 — see resolveDurationMs().
|
|
@@ -884,6 +910,43 @@ async function main() {
|
|
|
884
910
|
}
|
|
885
911
|
|
|
886
912
|
await appendSubagent(jsonlPath(), record);
|
|
913
|
+
await writeLiveMap(input, record);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Keep the HUD's agent live-map current from the lifecycle hooks (#1580).
|
|
918
|
+
* Before this, only wave-executor self-reports reached
|
|
919
|
+
* `.orchestrator/runtime/agent-status-current.json`, and those carry no session
|
|
920
|
+
* binding, so so-hud showed 0 agents while subagents ran.
|
|
921
|
+
*
|
|
922
|
+
* Writes into the root that holds THIS session's lock (#1605), so a session
|
|
923
|
+
* working in a worktree feeds the map the HUD reads beside that lock. A stop is
|
|
924
|
+
* written only when its start record was found: the phantom-stop class
|
|
925
|
+
* (#939/#949) has no start and a fresh agent_id per firing, and would otherwise
|
|
926
|
+
* add one `done` entry per firing. Best-effort: a short lock timeout and every
|
|
927
|
+
* failure swallowed, so agent dispatch is never delayed beyond it.
|
|
928
|
+
*
|
|
929
|
+
* @param {object} input Hook payload.
|
|
930
|
+
* @param {object} record The ledger record just written.
|
|
931
|
+
* @returns {Promise<void>}
|
|
932
|
+
*/
|
|
933
|
+
async function writeLiveMap(input, record) {
|
|
934
|
+
try {
|
|
935
|
+
if (record.agent_id === 'unknown') return;
|
|
936
|
+
if (record.event === 'stop' && record.start_record_found !== true) return;
|
|
937
|
+
const sessionId = rawPayloadSessionId({ session_id: record.parent_session_id });
|
|
938
|
+
if (sessionId === null) return;
|
|
939
|
+
const repoRoot = resolveOwnLockRoot({ session_id: sessionId, cwd: input?.cwd }, getProjectDir());
|
|
940
|
+
await setLifecycle(
|
|
941
|
+
record.agent_id,
|
|
942
|
+
{ status: record.event === 'start' ? 'running' : 'done', sessionId, agentType: record.agent_type ?? null },
|
|
943
|
+
// The event's own time, not the time after transcript parsing: a stop stamped late can overtake a
|
|
944
|
+
// quickly resumed agent's next start and show it as done.
|
|
945
|
+
{ repoRoot, timeoutMs: 1000, now: Number.isFinite(Date.parse(record.timestamp)) ? Date.parse(record.timestamp) : Date.now() },
|
|
946
|
+
);
|
|
947
|
+
} catch {
|
|
948
|
+
/* best-effort: the live-map never blocks or fails the hook */
|
|
949
|
+
}
|
|
887
950
|
}
|
|
888
951
|
|
|
889
952
|
// Entry guard (#1393): run only as the node script the harness execs — a bare
|