session-orchestrator 3.23.0 → 4.0.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 +18 -0
- package/.agents/skills/autopilot/SKILL.md +17 -0
- package/.agents/skills/bootstrap/SKILL.md +20 -0
- package/.agents/skills/brainstorm/SKILL.md +22 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
- package/.agents/skills/debug/SKILL.md +22 -0
- package/.agents/skills/discovery/SKILL.md +20 -0
- package/.agents/skills/dispatcher/SKILL.md +15 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
- package/.agents/skills/ecosystem-health/SKILL.md +20 -0
- package/.agents/skills/eli5/SKILL.md +20 -0
- package/.agents/skills/eval/SKILL.md +21 -0
- package/.agents/skills/evolve/SKILL.md +21 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
- package/.agents/skills/gitlab-ops/SKILL.md +20 -0
- package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
- package/.agents/skills/grill/SKILL.md +22 -0
- package/.agents/skills/hook-development/SKILL.md +15 -0
- package/.agents/skills/mcp-builder/SKILL.md +15 -0
- package/.agents/skills/memory-cleanup/SKILL.md +21 -0
- package/.agents/skills/mode-selector/SKILL.md +17 -0
- package/.agents/skills/npm-publish/SKILL.md +16 -0
- package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
- package/.agents/skills/persona-panel/SKILL.md +17 -0
- package/.agents/skills/plan/SKILL.md +20 -0
- package/.agents/skills/playwright-driver/SKILL.md +20 -0
- package/.agents/skills/quality-gates/SKILL.md +20 -0
- package/.agents/skills/reconcile/SKILL.md +21 -0
- package/.agents/skills/remote-offload/SKILL.md +20 -0
- package/.agents/skills/repo-audit/SKILL.md +16 -0
- package/.agents/skills/session-end/SKILL.md +20 -0
- package/.agents/skills/session-plan/SKILL.md +20 -0
- package/.agents/skills/session-start/SKILL.md +20 -0
- package/.agents/skills/spinout/SKILL.md +16 -0
- package/.agents/skills/sunset-review/SKILL.md +16 -0
- package/.agents/skills/test-runner/SKILL.md +20 -0
- package/.agents/skills/tmux-layout/SKILL.md +21 -0
- package/.agents/skills/using-orchestrator/SKILL.md +17 -0
- package/.agents/skills/vault-mirror/SKILL.md +15 -0
- package/.agents/skills/vault-sync/SKILL.md +15 -0
- package/.agents/skills/wave-executor/SKILL.md +20 -0
- package/.agents/skills/write-executable-plan/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot.md +2 -2
- package/.cursor/commands/bootstrap.md +1 -1
- package/.cursor/commands/brainstorm.md +1 -1
- package/.cursor/commands/debug.md +1 -1
- package/.cursor/commands/discovery.md +1 -1
- package/.cursor/commands/dispatcher.md +2 -2
- package/.cursor/commands/eli5.md +2 -2
- package/.cursor/commands/eval.md +2 -2
- package/.cursor/commands/evolve.md +1 -1
- package/.cursor/commands/go.md +1 -1
- package/.cursor/commands/grill.md +2 -2
- package/.cursor/commands/memory-cleanup.md +2 -2
- package/.cursor/commands/persona-panel.md +1 -1
- package/.cursor/commands/plan.md +1 -1
- package/.cursor/commands/portfolio.md +1 -1
- package/.cursor/commands/reconcile.md +2 -2
- package/.cursor/commands/release.md +2 -2
- package/.cursor/commands/session.md +2 -2
- package/.cursor/commands/spinout.md +2 -2
- package/.cursor/commands/sunset-review.md +2 -2
- package/.cursor/commands/templates-ack.md +2 -2
- package/.cursor/commands/test.md +2 -2
- package/.cursor/skills/brainstorm/SKILL.md +1 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/quality-gates/SKILL.md +1 -1
- package/.cursor/skills/remote-offload/SKILL.md +13 -0
- package/.orchestrator/policy/blocked-commands.json +121 -0
- package/.orchestrator/policy/ecosystem.schema.json +66 -0
- package/.orchestrator/policy/quality-gates.example.json +16 -0
- package/.orchestrator/policy/quality-gates.schema.json +38 -0
- package/.orchestrator/policy/templates-policy.json +27 -0
- package/.orchestrator/policy/test-profiles.json +47 -0
- package/AGENTS.md +225 -0
- package/CHANGELOG.md +1401 -0
- package/NOTICE +11 -6
- package/README.md +127 -92
- package/agents/db-specialist.md +0 -1
- package/agents/eval-judge.md +1 -1
- package/agents/skill-applied-judge.md +1 -1
- package/assets/wave-lifecycle.svg +98 -0
- package/commands/release.md +6 -3
- package/commands/session.md +18 -3
- package/docs/README.md +4 -0
- package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
- package/docs/baseline.md +67 -0
- package/docs/ci-setup.md +249 -48
- package/docs/codex-setup.md +66 -22
- package/docs/components.md +37 -16
- package/docs/cursor-setup.md +6 -2
- package/docs/events-schema.md +51 -10
- package/docs/instruction-delivery.md +62 -0
- package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
- package/docs/migration-v4.md +341 -0
- package/docs/pi-setup.md +6 -1
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +85 -19
- package/docs/scope-collision-guard.md +8 -8
- package/docs/session-config-reference.md +120 -61
- package/docs/session-config-template.md +40 -33
- package/docs/telemetry/telemetry-claims.md +11 -10
- package/docs/telemetry.md +187 -4
- package/docs/vault-docs-architecture.md +50 -11
- package/hooks/_lib/atomic-json.mjs +111 -0
- package/hooks/_lib/hook-import-set.json +1487 -0
- package/hooks/_lib/subagent-paths.mjs +143 -0
- package/hooks/_lib/subagent-transcript.mjs +562 -0
- package/hooks/config-protection.mjs +2 -2
- package/hooks/cwd-change-restore.mjs +11 -31
- package/hooks/enforce-commands.mjs +69 -0
- package/hooks/enforce-scope.mjs +35 -6
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +10 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/loop-guard.mjs +3 -3
- package/hooks/on-session-end.mjs +280 -14
- package/hooks/on-session-start.mjs +153 -4
- package/hooks/on-stop.mjs +371 -17
- package/hooks/operator-steer.mjs +2 -2
- package/hooks/post-bash-write-verify.mjs +189 -4
- package/hooks/post-edit-import-probe.mjs +344 -0
- package/hooks/post-subagent-discovery-validator.mjs +278 -392
- package/hooks/post-tool-batch-wave-signal.mjs +272 -44
- package/hooks/post-tool-failure-corrective-context.mjs +11 -34
- package/hooks/post-tooluse-frontend-slop.mjs +3 -3
- package/hooks/pre-bash-destructive-guard.mjs +39 -13
- package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
- package/hooks/skill-invocation-telemetry.mjs +17 -5
- package/hooks/subagent-telemetry.mjs +24 -30
- package/monitors/monitors.json +3 -3
- package/package.json +9 -1
- package/pi/prompts/session.md +2 -2
- package/plugin.json +27 -0
- package/scripts/autopilot.mjs +26 -12
- package/scripts/backfill-abandoned-sessions.mjs +130 -15
- package/scripts/backfill-learnings-from-vault.mjs +9 -3
- package/scripts/dialectic-deriver.mjs +73 -8
- package/scripts/emit-event.mjs +10 -2
- package/scripts/export-hw-learnings.mjs +113 -1
- package/scripts/generate-agents-skills.mjs +378 -0
- package/scripts/generate-cursor-adapter.mjs +45 -8
- package/scripts/generate-hook-import-set.mjs +249 -0
- package/scripts/lib/agent-status.mjs +13 -2
- package/scripts/lib/auq/parse.mjs +5 -29
- package/scripts/lib/auto-dialectic.mjs +68 -0
- package/scripts/lib/auto-dream.mjs +38 -36
- package/scripts/lib/autonomy/suitability.mjs +6 -0
- package/scripts/lib/autopilot/loop.mjs +2 -2
- package/scripts/lib/autopilot/worktree-pipeline.mjs +82 -6
- package/scripts/lib/build-live-signals.mjs +25 -22
- package/scripts/lib/ci-status-banner.mjs +220 -75
- package/scripts/lib/codex/plugin-contract.mjs +82 -6
- package/scripts/lib/cold-start-detector.mjs +23 -14
- package/scripts/lib/config/auto-dream.mjs +2 -1
- package/scripts/lib/config/block-header.mjs +63 -0
- package/scripts/lib/config/block-preprocess.mjs +177 -0
- package/scripts/lib/config/broken-window.mjs +2 -1
- package/scripts/lib/config/cold-start.mjs +2 -1
- package/scripts/lib/config/config-protection.mjs +22 -2
- package/scripts/lib/config/context-coverage.mjs +2 -1
- package/scripts/lib/config/cross-repo.mjs +2 -1
- package/scripts/lib/config/custom-phases.mjs +2 -1
- package/scripts/lib/config/dialectic.mjs +2 -1
- package/scripts/lib/config/discovery-validator.mjs +9 -3
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
- package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
- package/scripts/lib/config/docs-orchestrator.mjs +2 -1
- package/scripts/lib/config/docs-staleness.mjs +2 -1
- package/scripts/lib/config/drift-check.mjs +2 -1
- package/scripts/lib/config/eval.mjs +2 -1
- package/scripts/lib/config/events-rotation.mjs +2 -1
- package/scripts/lib/config/evolve.mjs +8 -2
- package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
- package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
- package/scripts/lib/config/handover-gate.mjs +2 -1
- package/scripts/lib/config/health-endpoints.mjs +388 -0
- package/scripts/lib/config/issue-budget.mjs +2 -1
- package/scripts/lib/config/loop-guard.mjs +2 -1
- package/scripts/lib/config/memory.mjs +2 -1
- package/scripts/lib/config/moc-staleness.mjs +2 -1
- package/scripts/lib/config/persona-gate-wave.mjs +2 -1
- package/scripts/lib/config/private-config-dir.mjs +67 -0
- package/scripts/lib/config/reconcile.mjs +2 -1
- package/scripts/lib/config/remote-hosts.mjs +234 -0
- package/scripts/lib/config/section-extractor.mjs +7 -1
- package/scripts/lib/config/skill-evolution.mjs +2 -1
- package/scripts/lib/config/slopcheck.mjs +2 -1
- package/scripts/lib/config/state-md-lock.mjs +2 -1
- package/scripts/lib/config/templates-first.mjs +2 -1
- package/scripts/lib/config/test.mjs +2 -1
- package/scripts/lib/config/vault-integration.mjs +7 -1
- package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
- package/scripts/lib/config/vault-staleness.mjs +2 -1
- package/scripts/lib/config/vault-sync.mjs +2 -1
- package/scripts/lib/config/verification-auto-fix.mjs +2 -1
- package/scripts/lib/config/wave-reviewers.mjs +2 -1
- package/scripts/lib/config/worktree-orphans.mjs +2 -1
- package/scripts/lib/config.mjs +31 -3
- package/scripts/lib/convergence-monitor.mjs +82 -16
- package/scripts/lib/dispatcher/enumerate.mjs +2 -17
- package/scripts/lib/dispatcher/rank.mjs +124 -48
- package/scripts/lib/ecosystem-health.mjs +16 -2
- package/scripts/lib/eval/engine.mjs +9 -1
- package/scripts/lib/eval/session-resolve.mjs +23 -4
- package/scripts/lib/events-schema.mjs +48 -0
- package/scripts/lib/events.mjs +256 -7
- package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
- package/scripts/lib/frontmatter-guard.mjs +131 -13
- package/scripts/lib/gates/gate-full.mjs +26 -0
- package/scripts/lib/gates/gate-helpers.mjs +76 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
- package/scripts/lib/hardware-pattern-detector.mjs +18 -1
- package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
- package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
- package/scripts/lib/host-identity.mjs +50 -11
- package/scripts/lib/instruction-budget-guard.mjs +171 -5
- package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
- package/scripts/lib/learnings/io.mjs +60 -6
- package/scripts/lib/memory-banner.mjs +20 -8
- package/scripts/lib/memory-proposals/store.mjs +30 -22
- package/scripts/lib/owner-config-banner.mjs +43 -6
- package/scripts/lib/owner-config-loader.mjs +21 -10
- package/scripts/lib/owner-interview.mjs +3 -3
- package/scripts/lib/owner-yaml.mjs +207 -14
- package/scripts/lib/peer-discovery.mjs +20 -2
- package/scripts/lib/platform.mjs +108 -15
- package/scripts/lib/plugin-update-banner.mjs +406 -0
- package/scripts/lib/project-hygiene.mjs +38 -2
- package/scripts/lib/qg-command-drift-banner.mjs +50 -12
- package/scripts/lib/quality-gate.mjs +133 -44
- package/scripts/lib/reconcile/emitter.mjs +68 -6
- package/scripts/lib/reconcile/engine.mjs +249 -9
- package/scripts/lib/reconcile/idempotency.mjs +37 -4
- package/scripts/lib/reconcile/writer.mjs +40 -18
- package/scripts/lib/scope-gate.mjs +36 -0
- package/scripts/lib/session-close-backfill.mjs +125 -18
- package/scripts/lib/session-discovery.mjs +57 -3
- package/scripts/lib/session-end/phase-skip.mjs +2 -2
- package/scripts/lib/session-id.mjs +12 -23
- package/scripts/lib/session-identity/own-session.mjs +187 -11
- package/scripts/lib/session-lock-shape.mjs +43 -0
- package/scripts/lib/session-lock.mjs +5 -10
- package/scripts/lib/session-registry.mjs +25 -9
- package/scripts/lib/session-schema/constants.mjs +36 -2
- package/scripts/lib/session-schema/validator.mjs +38 -4
- package/scripts/lib/session-start-probes.mjs +18 -1
- package/scripts/lib/session-transition.mjs +1 -1
- package/scripts/lib/sessions-canonical.mjs +446 -0
- package/scripts/lib/sessions-staleness-banner.mjs +18 -11
- package/scripts/lib/skill-health/join.mjs +17 -4
- package/scripts/lib/state-md.mjs +78 -0
- package/scripts/lib/sunset/walker.mjs +6 -0
- package/scripts/lib/telemetry/schema.mjs +255 -17
- package/scripts/lib/telemetry/sync.mjs +417 -24
- package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
- package/scripts/lib/validate/check-agents-skills.mjs +327 -0
- package/scripts/lib/validate/check-agents.mjs +3 -3
- package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
- package/scripts/lib/validate/check-doc-cli-commands.mjs +9 -33
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
- package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
- package/scripts/lib/validate/check-skill-links.mjs +163 -0
- package/scripts/lib/validate/check-skill-script-paths.mjs +455 -0
- package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
- package/scripts/lib/validate/check-unwired-features.mjs +0 -9
- package/scripts/lib/validate/check-validator-registration.mjs +254 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
- package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
- package/scripts/lib/validate/markdown-fences.mjs +196 -0
- package/scripts/lib/vault-backfill/template.mjs +63 -6
- package/scripts/lib/vault-mirror/process.mjs +165 -42
- package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
- package/scripts/lib/vault-status/board-lock.mjs +185 -0
- package/scripts/lib/vault-status/board-writer.mjs +174 -135
- package/scripts/lib/vault-status/narrative-mirror.mjs +129 -37
- package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
- package/scripts/lib/wave-executor/remote-dispatch.mjs +502 -0
- package/scripts/lib/wave-resource-gate.mjs +133 -7
- package/scripts/lib/wave-sizing.mjs +4 -1
- package/scripts/lib/wave-transcript-tail.mjs +142 -8
- package/scripts/materialize-wave-scope.mjs +32 -9
- package/scripts/memory-propose.mjs +146 -8
- package/scripts/migrate-cold-start-seed.mjs +4 -1
- package/scripts/parse-config.mjs +60 -3
- package/scripts/promote-vault-strict.mjs +4 -15
- package/scripts/release.mjs +337 -29
- package/scripts/repair-invalid-sessions.mjs +3 -3
- package/scripts/run-quality-gate.mjs +128 -11
- package/scripts/site-numbers.mjs +36 -4
- package/scripts/sweep-expired-learnings.mjs +90 -0
- package/scripts/sync-vault-schema.mjs +3 -1
- package/scripts/telemetry.mjs +2 -2
- package/scripts/validate-plugin.mjs +187 -0
- package/scripts/validate-wave-scope.mjs +28 -8
- package/scripts/vault-consolidate.mjs +3 -11
- package/scripts/vault-integration-watcher.mjs +2 -4
- package/scripts/vault-mirror.mjs +111 -26
- package/scripts/wave-scope-binding.mjs +215 -0
- package/skills/_shared/instruction-file-resolution.md +10 -0
- package/skills/_shared/parallel-aware-auq.md +31 -2
- package/skills/_shared/parallel-aware-preamble.md +18 -4
- package/skills/_shared/platform-tools.md +1 -1
- package/skills/_shared/state-ownership.md +1 -1
- package/skills/architecture/SKILL.md +7 -5
- package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
- package/skills/autopilot/SKILL.md +4 -18
- package/skills/claude-md-drift-check/SKILL.md +5 -1
- package/skills/claude-md-drift-check/checker.mjs +62 -2
- package/skills/convergence-monitoring/SIGNALS.md +55 -0
- package/skills/discovery/probes/vault-staleness.mjs +37 -13
- package/skills/discovery/probes-arch.md +20 -18
- package/skills/dispatcher/SKILL.md +3 -2
- package/skills/ecosystem-health/SKILL.md +4 -1
- package/skills/ecosystem-health/wizard.md +5 -0
- package/skills/evolve/SKILL.md +87 -11
- package/skills/frontmatter-guard/SKILL.md +11 -5
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +38 -2
- package/skills/remote-offload/SKILL.md +89 -0
- package/skills/session-end/SKILL.md +18 -905
- package/skills/session-end/phase-3-6-tail.md +19 -9
- package/skills/session-end/plan-verification.md +221 -155
- package/skills/session-end/references/phase-2-quality-gate.md +93 -0
- package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
- package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
- package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
- package/skills/session-end/references/session-summary-template.md +62 -0
- package/skills/session-plan/SKILL.md +49 -0
- package/skills/session-start/SKILL.md +41 -900
- package/skills/session-start/phase-8-5-express-path.md +1 -1
- package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
- package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
- package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
- package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
- package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
- package/skills/vault-sync/validator.mjs +21 -27
- package/skills/wave-executor/SKILL.md +16 -2
- package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
- package/skills/wave-executor/references/wave-loop-review.md +570 -0
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
- package/skills/wave-executor/wave-loop.md +14 -1271
- package/templates/_shared/journey-manifest.md +10 -6
- package/.cursor/commands/autopilot-multi.md +0 -14
- package/.cursor/commands/contract-version-bump.md +0 -14
- package/.cursor/commands/journey-audit.md +0 -14
- package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
- package/.cursor/skills/daily/SKILL.md +0 -12
- package/.cursor/skills/domain-model/SKILL.md +0 -13
- package/.cursor/skills/journey-audit/SKILL.md +0 -13
- package/.cursor/skills/skill-creator/SKILL.md +0 -13
- package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
- package/commands/autopilot-multi.md +0 -74
- package/commands/contract-version-bump.md +0 -28
- package/commands/journey-audit.md +0 -43
- package/pi/prompts/autopilot-multi.md +0 -12
- package/pi/prompts/contract-version-bump.md +0 -12
- package/pi/prompts/journey-audit.md +0 -12
- package/scripts/autopilot-multi.mjs +0 -885
- package/scripts/backfill-learnings-expires.mjs +0 -196
- package/scripts/backfill-learnings.mjs +0 -203
- package/scripts/fleet-instruction-scan.mjs +0 -141
- package/scripts/lib/autopilot/dep-graph.mjs +0 -417
- package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
- package/scripts/lib/webhook-url.mjs +0 -105
- package/scripts/lifecycle-sim-v6.mjs +0 -347
- package/scripts/migrate-learnings-jsonl.mjs +0 -189
- package/scripts/migrate-subagents-jsonl.mjs +0 -196
- package/scripts/upload-social-preview.mjs +0 -316
- package/skills/_shared/model-selection.md +0 -64
- package/skills/contract-version-bump/SKILL.md +0 -219
- package/skills/daily/SKILL.md +0 -222
- package/skills/daily/generate.sh +0 -92
- package/skills/daily/templates/daily.md.tpl +0 -36
- package/skills/journey-audit/SKILL.md +0 -269
- package/skills/skill-creator/SKILL.md +0 -168
- package/skills/ubiquitous-language/SKILL.md +0 -97
- package/skills/vault-sync/package-lock.json +0 -40
- /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
- /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
package/hooks/on-stop.mjs
CHANGED
|
@@ -16,23 +16,42 @@
|
|
|
16
16
|
* JSONL format (`.orchestrator/metrics/events.jsonl`) — emitted via the canonical
|
|
17
17
|
* `emitEvent()` so the JSONL record and the optional Clank webhook always carry the
|
|
18
18
|
* SAME dotted event name (was: bare `stop`/`subagent_stop` in JSONL vs dotted in webhook):
|
|
19
|
-
* Stop: {"timestamp":<ISO>,"event":"orchestrator.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* Stop: {"timestamp":<ISO>,"event":"orchestrator.turn.stopped","session_id":"...","semantic_session_id":"...","wave":<int>,"branch":"...","commit":"...","duration_ms":<int>,"duration_source":"session-lock"}
|
|
20
|
+
* + the SAME payload under the DEPRECATED legacy name (one generation, removal 2027-03-06):
|
|
21
|
+
* {"timestamp":<ISO>,"event":"orchestrator.session.stopped",…same fields…,"deprecated":true}
|
|
22
|
+
* (`session_id` / `semantic_session_id` are omitted when unresolvable — #1068 AC1.
|
|
23
|
+
* `duration_ms` + `duration_source` are omitted TOGETHER when no OWNED
|
|
24
|
+
* session.lock is readable — never a fabricated 0, see K5 below.)
|
|
25
|
+
* SubagentStop: {"timestamp":<ISO>,"event":"orchestrator.agent.stopped","agent":"<name>","agent_id":"...",
|
|
26
|
+
* "agent_type_meta":"...","tool_use_id":"...","transcript_found":true,
|
|
27
|
+
* "duration_ms":<int>,"duration_source":"meta-birthtime","status":"done"}
|
|
28
|
+
* (#1190 — every key after `event` is OPTIONAL and OMITTED when the
|
|
29
|
+
* measurement could not be made; `agent` too. See docs/events-schema.md.)
|
|
22
30
|
*/
|
|
23
31
|
|
|
24
32
|
import path from 'node:path';
|
|
25
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
promises as fs,
|
|
35
|
+
closeSync,
|
|
36
|
+
existsSync,
|
|
37
|
+
fstatSync,
|
|
38
|
+
openSync,
|
|
39
|
+
readFileSync,
|
|
40
|
+
readSync,
|
|
41
|
+
statSync,
|
|
42
|
+
writeFileSync,
|
|
43
|
+
} from 'node:fs';
|
|
26
44
|
|
|
27
45
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
46
|
+
import { AGENT_ID_RE, resolveSubagentSidecar } from './_lib/subagent-paths.mjs';
|
|
28
47
|
// #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
|
|
29
48
|
if (!shouldRunHook('on-stop')) process.exit(0);
|
|
30
49
|
|
|
31
50
|
import { emitEvent } from '../scripts/lib/events.mjs';
|
|
32
|
-
import {
|
|
51
|
+
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
33
52
|
import { parseSessionId } from '../scripts/lib/session-id.mjs';
|
|
34
53
|
import { heartbeat, logSweepEvent } from '../scripts/lib/session-registry.mjs';
|
|
35
|
-
import { updateHeartbeat } from '../scripts/lib/session-lock.mjs';
|
|
54
|
+
import { readLock, updateHeartbeat } from '../scripts/lib/session-lock.mjs';
|
|
36
55
|
|
|
37
56
|
// ---------------------------------------------------------------------------
|
|
38
57
|
// stdin reading (inline — no io.mjs because Stop hooks exit 0 always, never deny)
|
|
@@ -247,7 +266,8 @@ async function readWaveNumber(projectRoot) {
|
|
|
247
266
|
*/
|
|
248
267
|
async function resolveSessionId(input, projectRoot) {
|
|
249
268
|
const fromStdin = input?.session_id ?? input?.sessionId ?? null;
|
|
250
|
-
|
|
269
|
+
const rawStdinId = parseSessionId(fromStdin)?.format === 'uuid' ? fromStdin : null;
|
|
270
|
+
let sessionId = rawStdinId;
|
|
251
271
|
|
|
252
272
|
let recordedId = null;
|
|
253
273
|
let semanticSessionId = null;
|
|
@@ -265,21 +285,90 @@ async function resolveSessionId(input, projectRoot) {
|
|
|
265
285
|
}
|
|
266
286
|
} catch { /* missing or unparseable is fine */ }
|
|
267
287
|
|
|
288
|
+
// Actor-identity fallback ONLY — `sessionId` drives the registry heartbeat and
|
|
289
|
+
// the lock heartbeat, both of which need an id even when stdin carried none.
|
|
268
290
|
if (sessionId === null) sessionId = recordedId;
|
|
269
291
|
|
|
270
|
-
|
|
292
|
+
// W4a review F-A, second site of the same defect (census: exactly 2, both in
|
|
293
|
+
// hooks/, `grep -rn "= recordedId;" hooks/ scripts/` 2026-09-02). Comparing
|
|
294
|
+
// the RESOLVED id against `recordedId` is self-fulfilling: the line above may
|
|
295
|
+
// have just copied one into the other, so an id-less or non-UUID turn-end
|
|
296
|
+
// always read as "I am the recorded session" and inherited a live PEER's
|
|
297
|
+
// semantic identity — the exact #863 defect (c) contamination this gate
|
|
298
|
+
// exists to refuse. Decided on the RAW stdin UUID instead.
|
|
299
|
+
const isRecordedSession = rawStdinId !== null && rawStdinId === recordedId;
|
|
271
300
|
return {
|
|
272
301
|
sessionId,
|
|
273
302
|
semanticSessionId: isRecordedSession ? semanticSessionId : null,
|
|
274
303
|
};
|
|
275
304
|
}
|
|
276
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Resolve an elapsed span for this Stop event — and WHICH span depends on the
|
|
308
|
+
* source, which is why `duration_source` ships beside the number (W4a F-C).
|
|
309
|
+
*
|
|
310
|
+
* `session-lock` — SESSION-elapsed, measured at this turn's end. Stop fires
|
|
311
|
+
* once per TURN, and `started_at` is stamped once per
|
|
312
|
+
* SESSION, so this number GROWS MONOTONICALLY across the
|
|
313
|
+
* turns of one session: the last Stop of a 3-hour session
|
|
314
|
+
* reports ~3 hours, not the length of its final turn.
|
|
315
|
+
* `stdin-start-ms` — TURN-elapsed, the only first-party measurement of the
|
|
316
|
+
* turn itself. Measured today: the harness never sends it,
|
|
317
|
+
* so this source has never appeared in the fleet stream.
|
|
318
|
+
*
|
|
319
|
+
* Consumers must therefore not aggregate the two: summing `session-lock` spans
|
|
320
|
+
* over a session double-counts, where summing `stdin-start-ms` spans would not.
|
|
321
|
+
*
|
|
322
|
+
* OWNERSHIP-GATED, on the raw stdin `session_id` only. A lock in this working
|
|
323
|
+
* copy may name a PEER session (two windows share one checkout routinely), and
|
|
324
|
+
* a working-copy artefact is not a process-local identity witness — so the
|
|
325
|
+
* lock's `session_id` must match the id the STOPPING session was handed on
|
|
326
|
+
* stdin. `resolveSessionId()`'s resolved id is deliberately NOT used here: it
|
|
327
|
+
* falls back to `current-session.json`, which is exactly the foreign-identity
|
|
328
|
+
* inheritance this guard exists to refuse.
|
|
329
|
+
*
|
|
330
|
+
* Returns an object to SPREAD into the payload: `{}` when nothing is
|
|
331
|
+
* attestable, so `duration_ms` and `duration_source` are omitted together
|
|
332
|
+
* rather than written as a fabricated 0 (K5).
|
|
333
|
+
*
|
|
334
|
+
* `start_ms` is honoured first when the harness ever starts sending it — it is
|
|
335
|
+
* the only first-party measurement of the turn itself; measured today it is
|
|
336
|
+
* never present.
|
|
337
|
+
*
|
|
338
|
+
* @param {object|null} input parsed stdin payload
|
|
339
|
+
* @param {string} projectRoot
|
|
340
|
+
* @returns {{duration_ms?: number, duration_source?: string}}
|
|
341
|
+
*/
|
|
342
|
+
function resolveStopDuration(input, projectRoot) {
|
|
343
|
+
if (typeof input?.start_ms === 'number' && Number.isFinite(input.start_ms)) {
|
|
344
|
+
const ms = Date.now() - input.start_ms;
|
|
345
|
+
if (Number.isFinite(ms) && ms >= 0) {
|
|
346
|
+
return { duration_ms: Math.round(ms), duration_source: 'stdin-start-ms' };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const stdinId = input?.session_id ?? input?.sessionId ?? null;
|
|
351
|
+
if (typeof stdinId !== 'string' || stdinId.length === 0) return {};
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const lock = readLock({ repoRoot: projectRoot });
|
|
355
|
+
if (lock === null || lock.session_id !== stdinId) return {};
|
|
356
|
+
const startedAt = Date.parse(lock.started_at);
|
|
357
|
+
if (Number.isNaN(startedAt)) return {};
|
|
358
|
+
const ms = Date.now() - startedAt;
|
|
359
|
+
if (!Number.isFinite(ms) || ms < 0) return {};
|
|
360
|
+
return { duration_ms: Math.round(ms), duration_source: 'session-lock' };
|
|
361
|
+
} catch {
|
|
362
|
+
return {}; // best-effort — an unreadable lock measures nothing
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
277
366
|
/**
|
|
278
367
|
* Handle a Stop event. Reads wave from scope file + git info, appends JSONL.
|
|
279
368
|
* @param {object|null} input
|
|
280
369
|
*/
|
|
281
370
|
async function handleStop(input) {
|
|
282
|
-
const projectRoot =
|
|
371
|
+
const projectRoot = getProjectDir();
|
|
283
372
|
|
|
284
373
|
const wave = await readWaveNumber(projectRoot);
|
|
285
374
|
const { commit, branch } = await gitInfo(projectRoot);
|
|
@@ -350,9 +439,15 @@ async function handleStop(input) {
|
|
|
350
439
|
} catch { /* best-effort */ }
|
|
351
440
|
}
|
|
352
441
|
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
442
|
+
// K5 — `duration_ms` was literally 0 in 8.127 of 8.127 fleet
|
|
443
|
+
// `orchestrator.session.stopped` records (measured 2026-09-02): the old
|
|
444
|
+
// expression fell back to a hard 0 because the harness never sends
|
|
445
|
+
// `start_ms`. A fabricated 0 is the exact failure class
|
|
446
|
+
// `scripts/lib/telemetry/subagents-schema.mjs:20-35` documents — it reads as
|
|
447
|
+
// a MEASURED zero-length session and is indistinguishable from one. So the
|
|
448
|
+
// span now comes from the session.lock's `started_at`, and BOTH keys are
|
|
449
|
+
// omitted when it cannot be measured.
|
|
450
|
+
const duration = resolveStopDuration(input, projectRoot);
|
|
356
451
|
|
|
357
452
|
// Single emission path: emitEvent writes the canonical {timestamp, event, ...payload}
|
|
358
453
|
// JSONL record AND fires the optional Clank webhook with the SAME event name — no
|
|
@@ -361,14 +456,42 @@ async function handleStop(input) {
|
|
|
361
456
|
// turn-end outcome is joinable by identity from events.jsonl alone. Omitted
|
|
362
457
|
// (never `""`/`null`) when unattested: an unresolved identity stays visibly
|
|
363
458
|
// unresolved rather than becoming a guessed id.
|
|
364
|
-
|
|
459
|
+
// #1183 — a malformed record throws EventValidationError BEFORE any side
|
|
460
|
+
// effect (scripts/lib/events.mjs); this hook must never abort on that, so
|
|
461
|
+
// the emit is wrapped rather than left to propagate.
|
|
462
|
+
//
|
|
463
|
+
// ── THE RENAME (GitLab #1234) ──────────────────────────────────────────────
|
|
464
|
+
// `orchestrator.session.stopped` is a TURN counter with a session's name. The
|
|
465
|
+
// Stop hook fires once per ASSISTANT TURN, not once per session: measured
|
|
466
|
+
// 2026-09-06 over 90 days, 15.538 records against 2.016 distinct
|
|
467
|
+
// `session.started` ids = 7,7 per session, with 184 for a single id. Six
|
|
468
|
+
// consumers read it as a session-lifecycle signal and are wrong by that factor.
|
|
469
|
+
//
|
|
470
|
+
// So the same payload now leaves under BOTH names for one generation:
|
|
471
|
+
// - `orchestrator.turn.stopped` — the correct name, what new readers use.
|
|
472
|
+
// - `orchestrator.session.stopped` — unchanged shape PLUS `deprecated: true`,
|
|
473
|
+
// so an existing reader keeps working and a NEW reader can tell at a glance
|
|
474
|
+
// that it picked the legacy name.
|
|
475
|
+
// Removal of the legacy name: 2027-03-06 (docs/events-schema.md).
|
|
476
|
+
//
|
|
477
|
+
// The SubagentStop branch (`orchestrator.agent.stopped`) is deliberately NOT
|
|
478
|
+
// renamed — it is a different event and its per-agent cardinality is correct.
|
|
479
|
+
const stoppedPayload = {
|
|
365
480
|
...(sessionId !== null ? { session_id: sessionId } : {}),
|
|
366
481
|
...(semanticSessionId !== null ? { semantic_session_id: semanticSessionId } : {}),
|
|
367
482
|
wave,
|
|
368
483
|
...(branch !== null ? { branch } : {}),
|
|
369
484
|
...(commit !== null ? { commit } : {}),
|
|
370
|
-
|
|
371
|
-
}
|
|
485
|
+
...duration,
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
await emitEvent('orchestrator.turn.stopped', stoppedPayload);
|
|
490
|
+
} catch { /* telemetry never blocks the hook (#1183) */ }
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
await emitEvent('orchestrator.session.stopped', { ...stoppedPayload, deprecated: true });
|
|
494
|
+
} catch { /* telemetry never blocks the hook (#1183) */ }
|
|
372
495
|
}
|
|
373
496
|
|
|
374
497
|
/**
|
|
@@ -380,11 +503,242 @@ async function handleStop(input) {
|
|
|
380
503
|
* @returns {Promise<string|null>} additionalContext or null
|
|
381
504
|
*/
|
|
382
505
|
async function handleSubagentStop(input) {
|
|
383
|
-
|
|
384
|
-
|
|
506
|
+
/** @type {Record<string, unknown>} */
|
|
507
|
+
const payload = {};
|
|
508
|
+
|
|
509
|
+
// `agent` — the #1190 headline fix. The harness sends the EMPTY STRING, which
|
|
510
|
+
// `?? 'unknown'` never caught: 89.991 of 103.763 fleet records (86,7%,
|
|
511
|
+
// measured 2026-09-02) carry `agent: ""`. Trim, and emit the key ONLY when a
|
|
512
|
+
// real value remains — an unmeasured type stays visibly unmeasured rather
|
|
513
|
+
// than becoming `""` or a guessed `'unknown'`.
|
|
514
|
+
//
|
|
515
|
+
// Clamped with the SAME regex as `agent_type_meta` below (Q1-LOW-F3): this is
|
|
516
|
+
// the identical value class from a different source, it lands in the ledger
|
|
517
|
+
// and travels the optional Clank webhook, and it was the only unclamped
|
|
518
|
+
// string in this payload. A mismatch OMITS the key — never a truncation,
|
|
519
|
+
// which would look like a measured (but wrong) agent type.
|
|
520
|
+
const rawAgent = firstNonEmptyString(input, ['agent_type', 'subagent_type']);
|
|
521
|
+
const agent = rawAgent !== null && AGENT_TYPE_META_RE.test(rawAgent) ? rawAgent : null;
|
|
522
|
+
if (agent !== null) payload.agent = agent;
|
|
523
|
+
|
|
524
|
+
// `agent_id` — reader shape copied from hooks/subagent-telemetry.mjs (both
|
|
525
|
+
// naming conventions the harness may use). Charset-guarded before it is ever
|
|
526
|
+
// interpolated into a path (same precedent, resolveSubagentTranscriptPath)
|
|
527
|
+
// AND before it is emitted: an unbounded, unvalidated id (`../../etc/passwd`,
|
|
528
|
+
// or a 10 KB blob) would otherwise land verbatim in the ledger and travel
|
|
529
|
+
// over the optional Clank webhook unredacted. A rejected id is OMITTED — the
|
|
530
|
+
// same omission contract every other field in this payload uses — never
|
|
531
|
+
// sanitised into a different id that looks measured.
|
|
532
|
+
const rawAgentId = firstNonEmptyString(input, ['agent_id', 'subagent_id']);
|
|
533
|
+
const agentId = rawAgentId !== null && AGENT_ID_RE.test(rawAgentId) ? rawAgentId : null;
|
|
534
|
+
if (agentId !== null) payload.agent_id = agentId;
|
|
535
|
+
|
|
536
|
+
// Sidecar-derived fields. Every derivation is individually wrapped: a failure
|
|
537
|
+
// omits its own field and nothing else, and this function must never throw —
|
|
538
|
+
// a throw here would skip the emit and lose the record that already works.
|
|
539
|
+
// Consolidated derivation (#1196): hooks/_lib/subagent-paths.mjs now also
|
|
540
|
+
// rejects the literal 'unknown' agentId — this file's own AGENT_ID_RE-only
|
|
541
|
+
// check (above, for the emitted `agent_id` field) never did.
|
|
542
|
+
const sidecar = resolveSubagentSidecar({ transcriptPath: input?.transcript_path, agentId });
|
|
543
|
+
if (sidecar !== null) {
|
|
544
|
+
const transcriptPath = sidecar.transcript;
|
|
545
|
+
const metaPath = sidecar.meta;
|
|
546
|
+
|
|
547
|
+
let transcriptFound = false;
|
|
548
|
+
try {
|
|
549
|
+
transcriptFound = existsSync(transcriptPath);
|
|
550
|
+
payload.transcript_found = transcriptFound;
|
|
551
|
+
} catch { /* probe failed — omit rather than assert `false` */ }
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
// One small read, one parse, two fields. `description` is operator prose
|
|
555
|
+
// and is deliberately NOT carried: this payload also travels over the
|
|
556
|
+
// optional Clank webhook unredacted.
|
|
557
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf8'));
|
|
558
|
+
// Both fields are CHARSET+LENGTH CLAMPED before they enter the payload,
|
|
559
|
+
// for the same reason `agent_id` above is: this record is appended to the
|
|
560
|
+
// ledger AND travels over the optional Clank webhook UNREDACTED, and
|
|
561
|
+
// meta.json is written by the harness, not by us. An oversized or
|
|
562
|
+
// structured value would land verbatim in both. A rejected value is
|
|
563
|
+
// OMITTED, never truncated into a shorter value that still looks measured.
|
|
564
|
+
if (typeof meta?.toolUseId === 'string' && TOOL_USE_ID_RE.test(meta.toolUseId.trim())) {
|
|
565
|
+
payload.tool_use_id = meta.toolUseId.trim();
|
|
566
|
+
}
|
|
567
|
+
// A SECOND witness for the type — never merged into `agent`, so the
|
|
568
|
+
// empty-`agent_type` rate stays measurable.
|
|
569
|
+
if (typeof meta?.agentType === 'string' && AGENT_TYPE_META_RE.test(meta.agentType.trim())) {
|
|
570
|
+
payload.agent_type_meta = meta.agentType.trim();
|
|
571
|
+
}
|
|
572
|
+
} catch { /* absent or corrupt sidecar meta — omit both fields */ }
|
|
573
|
+
|
|
574
|
+
try {
|
|
575
|
+
// meta.json carries no spawn timestamp; the harness writes the file AT
|
|
576
|
+
// spawn, so its birthtime IS the spawn moment. `duration_source` puts
|
|
577
|
+
// that provenance in the record instead of implying a measured span.
|
|
578
|
+
const span = spanFromBirthtime(Date.now(), statSync(metaPath).birthtimeMs);
|
|
579
|
+
if (span !== null) {
|
|
580
|
+
payload.duration_ms = span;
|
|
581
|
+
payload.duration_source = 'meta-birthtime';
|
|
582
|
+
}
|
|
583
|
+
} catch { /* stat failed — omit both keys (never a fabricated 0) */ }
|
|
584
|
+
|
|
585
|
+
if (transcriptFound) {
|
|
586
|
+
const status = readStatusFromTranscriptTail(transcriptPath);
|
|
587
|
+
if (status !== null) payload.status = status;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// #1183 — a malformed record throws EventValidationError BEFORE any side
|
|
592
|
+
// effect (scripts/lib/events.mjs); this hook must never abort on that, so
|
|
593
|
+
// the emit is wrapped rather than left to propagate.
|
|
594
|
+
try {
|
|
595
|
+
await emitEvent('orchestrator.agent.stopped', payload);
|
|
596
|
+
} catch { /* telemetry never blocks the hook (#1183) */ }
|
|
385
597
|
return null;
|
|
386
598
|
}
|
|
387
599
|
|
|
600
|
+
// ---------------------------------------------------------------------------
|
|
601
|
+
// SubagentStop payload derivations (#1190)
|
|
602
|
+
// ---------------------------------------------------------------------------
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Harness tool-use ids, measured on-disk 2026-09-02 in a real sidecar meta.json:
|
|
606
|
+
* `toolu_01Xj6qZ3ApsVfomhByNftmxj` (25 chars, alphanumeric + one underscore).
|
|
607
|
+
*/
|
|
608
|
+
const TOOL_USE_ID_RE = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Agent types carry a plugin qualifier, so the COLON is part of the real shape —
|
|
612
|
+
* measured in the same file: `session-orchestrator:code-implementer` (37 chars).
|
|
613
|
+
* A `tool_use_id`-identical charset would have rejected every plugin-qualified
|
|
614
|
+
* agent type in this repo, i.e. silently removed the field it clamps.
|
|
615
|
+
*/
|
|
616
|
+
const AGENT_TYPE_META_RE = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Upper bound on a `meta-birthtime` span, in ms. NAMED CEILING (BV-004): 7 days.
|
|
620
|
+
*
|
|
621
|
+
* No subagent runs for a week — the longest recorded here is minutes — so a
|
|
622
|
+
* span above this is not a long agent, it is a birthtime the filesystem did not
|
|
623
|
+
* supply. Node documents `birthtimeMs` as unavailable on filesystems that do
|
|
624
|
+
* not record it (overlayfs and some CI images among them), where it surfaces as
|
|
625
|
+
* 0 / 1970-01-01; the old `>= 0` lower bound accepted that verbatim and would
|
|
626
|
+
* ship a ~55-YEAR span stamped `duration_source: 'meta-birthtime'` — a
|
|
627
|
+
* fabricated measurement of exactly the class K5 removed.
|
|
628
|
+
*
|
|
629
|
+
* REVISIT TRIGGER: if a legitimately long-running agent class ever appears
|
|
630
|
+
* (a background agent measured in hours-to-days), raise this — but raise it to
|
|
631
|
+
* a measured bound, never remove it.
|
|
632
|
+
*/
|
|
633
|
+
const META_BIRTHTIME_MAX_SPAN_MS = 7 * 24 * 60 * 60 * 1000;
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Span between a birthtime and now, or `null` when the birthtime is not a
|
|
637
|
+
* usable measurement.
|
|
638
|
+
*
|
|
639
|
+
* Rejects a non-positive `birthtimeMs` (the "filesystem records no birthtime"
|
|
640
|
+
* signal) and any span outside `(0, META_BIRTHTIME_MAX_SPAN_MS)`. `null` means
|
|
641
|
+
* "omit both keys" at the call site — never a fabricated number.
|
|
642
|
+
*
|
|
643
|
+
* Pure, and separate from the call site so the two out-of-band cases are
|
|
644
|
+
* testable without a filesystem that can forge a birthtime.
|
|
645
|
+
*
|
|
646
|
+
* @param {number} nowMs
|
|
647
|
+
* @param {number} birthtimeMs
|
|
648
|
+
* @returns {number|null} rounded span in ms, or null
|
|
649
|
+
*/
|
|
650
|
+
function spanFromBirthtime(nowMs, birthtimeMs) {
|
|
651
|
+
if (!Number.isFinite(birthtimeMs) || birthtimeMs <= 0) return null;
|
|
652
|
+
const span = nowMs - birthtimeMs;
|
|
653
|
+
if (!Number.isFinite(span) || span < 0 || span > META_BIRTHTIME_MAX_SPAN_MS) return null;
|
|
654
|
+
return Math.round(span);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** Tail window for the STATUS scan — sidecars measured at 113–528 KB. */
|
|
658
|
+
const STATUS_TAIL_BYTES = 64 * 1024;
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* STATUS literals an agent reports at the START of a line (optionally bold).
|
|
662
|
+
*
|
|
663
|
+
* LINE-ANCHORED on purpose — the rationale and the measurement are at
|
|
664
|
+
* `scripts/lib/wave-transcript-tail.mjs:105-112`: a free-floating
|
|
665
|
+
* `/STATUS: partial/` fires on any agent that merely QUOTES the marker
|
|
666
|
+
* (2 of 2 live hits in that measurement were quotations, 0 were reports).
|
|
667
|
+
* NOT imported from that module because the two alternations are DIFFERENT
|
|
668
|
+
* literal sets, not two copies of one: the tailer's `STATUS_RE` matches only
|
|
669
|
+
* `partial|blocked|failed` (it classifies a single `status-partial` attention
|
|
670
|
+
* pattern), while this hook records the agent's SELF-REPORTED status and must
|
|
671
|
+
* therefore also match `done` and `no-tests-needed`. Importing its constant
|
|
672
|
+
* would silently drop the two success literals. (Import cost is NOT the reason —
|
|
673
|
+
* measured 2026-09-02 as equal.)
|
|
674
|
+
*/
|
|
675
|
+
const AGENT_STATUS_RE = /^[ \t*_]*STATUS:\s*(done|partial|blocked|failed|no-tests-needed)\b/im;
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Pick the first non-empty trimmed string among `keys` on `input`.
|
|
679
|
+
* @param {object|null|undefined} input
|
|
680
|
+
* @param {string[]} keys
|
|
681
|
+
* @returns {string|null}
|
|
682
|
+
*/
|
|
683
|
+
function firstNonEmptyString(input, keys) {
|
|
684
|
+
for (const k of keys) {
|
|
685
|
+
const v = input?.[k];
|
|
686
|
+
if (typeof v === 'string' && v.trim()) return v.trim();
|
|
687
|
+
}
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Read the LAST `STATUS_TAIL_BYTES` of an agent transcript and return the most
|
|
693
|
+
* recent line-anchored STATUS literal, or null when none is present.
|
|
694
|
+
*
|
|
695
|
+
* Never reads the whole file: measured sidecar sizes are 113–528 KB and this
|
|
696
|
+
* hook fires on every SubagentStop. Absence means NOT FOUND, never success —
|
|
697
|
+
* measured 2026-09-02, only 1 of 4 live sidecars carried a STATUS token in its
|
|
698
|
+
* last 64 KiB, so the miss is the common path.
|
|
699
|
+
*
|
|
700
|
+
* @param {string} transcriptPath
|
|
701
|
+
* @returns {string|null}
|
|
702
|
+
*/
|
|
703
|
+
function readStatusFromTranscriptTail(transcriptPath) {
|
|
704
|
+
let fd = null;
|
|
705
|
+
try {
|
|
706
|
+
fd = openSync(transcriptPath, 'r');
|
|
707
|
+
const size = fstatSync(fd).size;
|
|
708
|
+
const length = Math.min(size, STATUS_TAIL_BYTES);
|
|
709
|
+
const start = size - length;
|
|
710
|
+
const buf = Buffer.alloc(length);
|
|
711
|
+
const read = readSync(fd, buf, 0, length, start);
|
|
712
|
+
let lines = buf.subarray(0, read).toString('utf8').split('\n');
|
|
713
|
+
// Drop the leading partial line when the window did not start at byte 0.
|
|
714
|
+
if (start > 0) lines = lines.slice(1);
|
|
715
|
+
|
|
716
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
717
|
+
const line = lines[i].trim();
|
|
718
|
+
if (!line) continue;
|
|
719
|
+
let rec;
|
|
720
|
+
try {
|
|
721
|
+
rec = JSON.parse(line);
|
|
722
|
+
} catch { continue; }
|
|
723
|
+
const content = rec?.message?.content;
|
|
724
|
+
if (!Array.isArray(content)) continue;
|
|
725
|
+
for (let j = content.length - 1; j >= 0; j -= 1) {
|
|
726
|
+
const block = content[j];
|
|
727
|
+
if (typeof block?.text !== 'string') continue;
|
|
728
|
+
const m = AGENT_STATUS_RE.exec(block.text);
|
|
729
|
+
if (m) return m[1].toLowerCase();
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return null;
|
|
733
|
+
} catch {
|
|
734
|
+
return null;
|
|
735
|
+
} finally {
|
|
736
|
+
if (fd !== null) {
|
|
737
|
+
try { closeSync(fd); } catch { /* best-effort */ }
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
388
742
|
// ---------------------------------------------------------------------------
|
|
389
743
|
// main
|
|
390
744
|
// ---------------------------------------------------------------------------
|
package/hooks/operator-steer.mjs
CHANGED
|
@@ -37,14 +37,14 @@ import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
|
37
37
|
// Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
|
|
38
38
|
if (!shouldRunHook('operator-steer')) process.exit(0);
|
|
39
39
|
|
|
40
|
-
import {
|
|
40
|
+
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
41
41
|
|
|
42
42
|
// ---------------------------------------------------------------------------
|
|
43
43
|
// Main
|
|
44
44
|
// ---------------------------------------------------------------------------
|
|
45
45
|
|
|
46
46
|
async function main() {
|
|
47
|
-
const steerPath = path.join(
|
|
47
|
+
const steerPath = path.join(getProjectDir(), '.orchestrator', 'STEER.md');
|
|
48
48
|
|
|
49
49
|
// File absent — nothing to do.
|
|
50
50
|
if (!existsSync(steerPath)) return;
|