session-orchestrator 5.11.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 +12 -1
- 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 +180 -3
- package/README.md +126 -117
- 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 +4 -2
- 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 +102 -25
- 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 +55 -21
- package/docs/session-config-template.md +32 -24
- 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 +158 -59
- 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 -3
- 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 +73 -3
- 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/remote-hosts.mjs +2 -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 +3 -3
- 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 +21 -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-end/vault-orphans.mjs +205 -0
- package/scripts/lib/session-id.mjs +9 -1
- package/scripts/lib/session-identity/own-session.mjs +63 -0
- package/scripts/lib/session-invocation.mjs +14 -3
- package/scripts/lib/session-lock.mjs +114 -0
- package/scripts/lib/session-record-repair.mjs +245 -35
- package/scripts/lib/session-registry.mjs +85 -12
- 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 +174 -0
- package/scripts/lib/so-hud/runtime.mjs +87 -0
- 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 +248 -0
- package/scripts/lib/state-md/mission-status.mjs +5 -1
- package/scripts/lib/state-md/recommendations.mjs +26 -4
- 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 +22 -9
- package/scripts/lib/vault-mirror/process.mjs +197 -65
- package/scripts/lib/vault-mirror/prune.mjs +5 -2
- package/scripts/lib/vault-mirror/render-learnings.mjs +8 -7
- 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 +106 -13
- package/scripts/lib/vault-staleness-banner.mjs +4 -7
- package/scripts/lib/vault-status/board-writer.mjs +34 -5
- 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 +57 -8
- 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 +68 -0
- package/scripts/state-md-idle-reset.mjs +82 -0
- 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/scripts/vault-orphans.mjs +171 -0
- package/skills/_shared/fleet-protocol.md +166 -3
- 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 +5 -4
- package/skills/architecture/references/domain-model.md +1 -1
- package/skills/autopilot/SKILL.md +3 -2
- 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 +97 -13
- 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-7e-vault-orphans.md +63 -0
- package/skills/session-end/references/phase-3-documentation-updates.md +25 -13
- package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
- package/skills/session-end/references/session-summary-template.md +8 -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 +10 -4
- package/skills/session-start/phase-4-5-resource-health.md +11 -5
- package/skills/session-start/phase-7-5-mode-selector.md +3 -1
- 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 +13 -5
- 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/AGENTS.md +0 -207
- package/assets/og-card.png +0 -0
- package/assets/og-card.svg +0 -47
- package/assets/wave-lifecycle.svg +0 -98
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { discoverActiveSessions } from '../session-discovery.mjs';
|
|
6
|
+
import { readRegistry, activeDir } from '../session-registry.mjs';
|
|
7
|
+
import { readCurrentStatus } from '../agent-status.mjs';
|
|
8
|
+
import { readLockDetailed, isLockLive } from '../session-lock.mjs';
|
|
9
|
+
import { readTailWindow } from '../tail-window.mjs';
|
|
10
|
+
import { parseSessionConfig } from '../config.mjs';
|
|
11
|
+
import { resolveStateMdPath } from '../state-md.mjs';
|
|
12
|
+
import { resolveOwnLockRootFor } from '../own-lock-root.mjs';
|
|
13
|
+
import { collectSnapshot } from './collect.mjs';
|
|
14
|
+
import { resolveEventsLedgerRoot } from '../platform.mjs';
|
|
15
|
+
const exec = promisify(execFile);
|
|
16
|
+
export const MAX_EVENT_BYTES = 512 * 1024;
|
|
17
|
+
|
|
18
|
+
export function readEventTail(repoRoot) {
|
|
19
|
+
try {
|
|
20
|
+
const tail = readTailWindow(path.join(resolveEventsLedgerRoot(repoRoot), '.orchestrator/metrics/events.jsonl'), MAX_EVENT_BYTES);
|
|
21
|
+
return { text: tail.cut ? tail.text.slice(tail.text.indexOf('\n') + 1) : tail.text, degraded: tail.cut ? ['events:tail-only'] : [] };
|
|
22
|
+
} catch (e) { return { text: '', degraded: [e.code === 'ENOENT' ? 'events:absent' : 'events:unreadable'] }; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function registryWithHealth() {
|
|
26
|
+
// The canonical reader intentionally swallows failures; distinguish its directory first.
|
|
27
|
+
try { await fs.readdir(activeDir()); }
|
|
28
|
+
catch (e) { if (e.code === 'ENOENT') return []; throw e; }
|
|
29
|
+
return readRegistry();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Capture fallback at the injected boundaries, without intercepting global stderr. */
|
|
33
|
+
export async function discover(repoRoot, { run = exec, registry = registryWithHealth, discovery = discoverActiveSessions, now = Date.now() } = {}) {
|
|
34
|
+
const degraded = [];
|
|
35
|
+
let registryEntries = [];
|
|
36
|
+
const sessions = await discovery(repoRoot, {
|
|
37
|
+
now, timeoutMs: 1000,
|
|
38
|
+
listWorktreesImpl: async () => {
|
|
39
|
+
try {
|
|
40
|
+
const { stdout } = await run('git', ['-C', repoRoot, 'worktree', 'list', '--porcelain'], { timeout: 700, maxBuffer: 512 * 1024 });
|
|
41
|
+
return stdout.trim().split('\n\n').filter(Boolean).map(block => {
|
|
42
|
+
const lines = block.split('\n');
|
|
43
|
+
return { path: lines.find(l => l.startsWith('worktree '))?.slice(9), branch: lines.find(l => l.startsWith('branch '))?.slice(7).replace(/^refs\/heads\//, '') ?? '' };
|
|
44
|
+
}).filter(w => w.path);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
degraded.push(e.killed || e.code === 'ETIMEDOUT' ? 'discovery-timeout' : 'discovery-fallback');
|
|
47
|
+
return [{ path: repoRoot, branch: '' }];
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
registryReader: async () => {
|
|
51
|
+
try { registryEntries = await registry(); return registryEntries; } catch { degraded.push('registry-unreadable'); return []; }
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
if (sessions.some(s => s.registryOnly)) degraded.push('discovery:registry-fallback');
|
|
55
|
+
return { sessions: sessions.map(s => ({ ...s, last_heartbeat: registryEntries.find(r => r.session_id === s.sessionId)?.last_heartbeat ?? null })), degraded };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function config(repoRoot) {
|
|
59
|
+
for (const name of ['CLAUDE.md', 'AGENTS.md']) {
|
|
60
|
+
try {
|
|
61
|
+
const source = await fs.readFile(path.join(repoRoot, name), 'utf8');
|
|
62
|
+
if (!/^vault-integration:/m.test(source)) continue;
|
|
63
|
+
// No host-local owner.yaml reads and no vault path resolution outside this repo.
|
|
64
|
+
const parsed = parseSessionConfig(source, { hostPaths: { env: {}, ownerConfig: {} } });
|
|
65
|
+
const value = parsed['vault-integration'];
|
|
66
|
+
return { enabled: value?.enabled ?? null, mode: value?.mode ?? null };
|
|
67
|
+
} catch (e) { if (e.code !== 'ENOENT') return { degraded: ['config:unreadable'] }; }
|
|
68
|
+
}
|
|
69
|
+
return { enabled: null, mode: null };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* @param {string} repoRoot
|
|
73
|
+
* @param {{ sessionId?: string|null }} [opts] `sessionId` = the mod's raw session id (#1730). The agent-status
|
|
74
|
+
* writer (`hooks/subagent-telemetry.mjs`) writes beside the session's own lock, which is not always `repoRoot`:
|
|
75
|
+
* a session working in a worktree by `cd` keeps its lock in the main checkout and the other way round. With an
|
|
76
|
+
* id, agents and the lock that dates them are read there. Without one, everything is read at `repoRoot`.
|
|
77
|
+
*/
|
|
78
|
+
export async function measure(repoRoot, { sessionId = null } = {}) {
|
|
79
|
+
const ownRoot = sessionId ? resolveOwnLockRootFor({ sessionId, cwd: repoRoot }).root : null;
|
|
80
|
+
return collectSnapshot(repoRoot, {
|
|
81
|
+
now: Date.now, readText: file => fs.readFile(file, 'utf8'), lock: root => readLockDetailed({ repoRoot: root }),
|
|
82
|
+
isLockLive, agents: root => readCurrentStatus({ repoRoot: ownRoot ?? root }), discover, events: readEventTail, config,
|
|
83
|
+
...(ownRoot ? { agentLock: () => readLockDetailed({ repoRoot: ownRoot }) } : {}),
|
|
84
|
+
statePath: path.relative(repoRoot, resolveStateMdPath(repoRoot)),
|
|
85
|
+
sessionId: sessionId ?? process.env.CLAUDE_SESSION_ID ?? process.env.SO_SESSION_ID ?? null,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
resolveIssueBudgetSessionId,
|
|
45
45
|
} from './issue-budget.mjs';
|
|
46
46
|
import { resolveProjectDir } from './platform.mjs';
|
|
47
|
+
import { resolveOwnLockRoot } from '../../hooks/_lib/hook-root.mjs';
|
|
47
48
|
import { resolveRepoSpec } from './vcs-repo-spec.mjs';
|
|
48
49
|
|
|
49
50
|
/**
|
|
@@ -167,8 +168,14 @@ function runCli(cmd, args, repoRoot) {
|
|
|
167
168
|
let currentSession = null;
|
|
168
169
|
if (typeof nativeRawId === 'string' && nativeRawId.length > 0) {
|
|
169
170
|
try {
|
|
171
|
+
// #1605: after /clear in an entered worktree the session's lock and
|
|
172
|
+
// current-session.json live in that worktree, not at the launch root.
|
|
173
|
+
// Read the record beside the own lock — the same lookup
|
|
174
|
+
// `hooks/pre-bash-issue-budget.mjs` uses — so both producers charge
|
|
175
|
+
// the ledger (which stays at `budgetRoot`) under the same session key.
|
|
176
|
+
const ownRoot = resolveOwnLockRoot({ session_id: nativeRawId, cwd: process.cwd() }, budgetRoot);
|
|
170
177
|
const parsed = JSON.parse(
|
|
171
|
-
readFileSync(path.join(
|
|
178
|
+
readFileSync(path.join(ownRoot, '.orchestrator', 'current-session.json'), 'utf8'),
|
|
172
179
|
);
|
|
173
180
|
// SESSION BINDING (#1058). `current-session.json` is repo-global —
|
|
174
181
|
// every session sharing this working copy writes the same path — so
|
|
@@ -15,6 +15,9 @@ import { dirname, resolve as resolvePath } from 'node:path';
|
|
|
15
15
|
import { parseStateMd, serializeStateMd } from './yaml-parser.mjs';
|
|
16
16
|
import { withStateMdLock } from '../session-lock.mjs';
|
|
17
17
|
import { resolveStateDir } from '../platform.mjs';
|
|
18
|
+
// Hook-closure note: own-session.mjs imports only node:* and the import-free
|
|
19
|
+
// session-lock-shape.mjs, both already in the hook import set.
|
|
20
|
+
import { readProcessLocalSessionIds } from '../session-identity/own-session.mjs';
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* Sets frontmatter.updated to the given ISO 8601 timestamp and returns the
|
|
@@ -85,20 +88,84 @@ const STATE_DIR_CANDIDATES = ['.claude', '.codex', '.cursor', '.pi'];
|
|
|
85
88
|
* @returns {string} Absolute artifact path.
|
|
86
89
|
*/
|
|
87
90
|
export function resolveStateArtifactPath(repoRoot, filename) {
|
|
91
|
+
const { candidates } = stateArtifactCandidates(repoRoot, filename);
|
|
92
|
+
return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Shared candidate list: SO_STATE_DIR override (if set), the active harness's
|
|
97
|
+
* state dir, then the legacy harness dirs. `preferredCount` is how many of the
|
|
98
|
+
* leading entries are override/active (as opposed to legacy).
|
|
99
|
+
*
|
|
100
|
+
* @param {string|undefined} repoRoot
|
|
101
|
+
* @param {string} filename
|
|
102
|
+
* @returns {{ candidates: string[], preferredCount: number }}
|
|
103
|
+
*/
|
|
104
|
+
function stateArtifactCandidates(repoRoot, filename) {
|
|
88
105
|
const root = repoRoot ?? process.cwd();
|
|
89
106
|
const override = (process.env.SO_STATE_DIR ?? '').trim();
|
|
90
107
|
const active = resolveStateDir();
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
return
|
|
108
|
+
const preferred = [...new Set([override || active, active])];
|
|
109
|
+
const directories = [...new Set([...preferred, ...STATE_DIR_CANDIDATES])];
|
|
110
|
+
return {
|
|
111
|
+
candidates: directories.map((directory) => resolvePath(root, directory, filename)),
|
|
112
|
+
preferredCount: preferred.length,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* May THIS process write into the existing STATE.md at `candidate`?
|
|
118
|
+
* Always yes for a placeholder (no frontmatter or no `session-id`), a
|
|
119
|
+
* `status: idle` record, or a `session-id` naming this process
|
|
120
|
+
* (`readProcessLocalSessionIds()`). An unreadable file is never a target.
|
|
121
|
+
*
|
|
122
|
+
* Otherwise the answer depends on where the candidate sits:
|
|
123
|
+
* - Legacy (other-platform) dir: no — #1618, unchanged.
|
|
124
|
+
* - SO_STATE_DIR / active dir (`preferred`): refused ONLY when it is
|
|
125
|
+
* `status: completed` AND this process has a measurable own id (#1640 —
|
|
126
|
+
* the measured damage: session-end Phase 3.7a wrote into a FINISHED
|
|
127
|
+
* foreign session's record). A non-completed record keeps the pre-#1640
|
|
128
|
+
* behaviour: one checkout has one session.lock owner, and after `/clear`
|
|
129
|
+
* the process carries a NEW raw id while its own active STATE.md still
|
|
130
|
+
* names the old one — refusing that file made `/close` unable to finish.
|
|
131
|
+
* `completedHandoff` (Idle Reset) may take a completed record too: that
|
|
132
|
+
* rewrite IS the hand-over to the next session.
|
|
133
|
+
*
|
|
134
|
+
* CEILING (BV-004): a `/clear`ed session's active file is still identified by
|
|
135
|
+
* its OLD id until close, where {@link markStateMdCompletedOnDisk} re-stamps
|
|
136
|
+
* it; a live parallel session in the same checkout is PSA territory, not
|
|
137
|
+
* this guard's. Revisit when the raw id survives `/clear` (resume linkage).
|
|
138
|
+
*
|
|
139
|
+
* @param {string} candidate
|
|
140
|
+
* @param {string[]} ownIds
|
|
141
|
+
* @param {{ preferred: boolean, completedHandoff?: boolean }} opts
|
|
142
|
+
* @returns {boolean}
|
|
143
|
+
*/
|
|
144
|
+
function isWritableStateMd(candidate, ownIds, { preferred, completedHandoff = false }) {
|
|
145
|
+
let frontmatter;
|
|
146
|
+
try {
|
|
147
|
+
frontmatter = parseStateMd(readFileSync(candidate, 'utf8'))?.frontmatter ?? null;
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
const raw = frontmatter?.['session-id'];
|
|
152
|
+
const sessionId = typeof raw === 'string' ? raw.trim() : '';
|
|
153
|
+
if (!sessionId) return true;
|
|
154
|
+
const status = frontmatter.status;
|
|
155
|
+
if (status === 'idle') return true;
|
|
156
|
+
if (ownIds.includes(sessionId)) return true;
|
|
157
|
+
if (!preferred) return false;
|
|
158
|
+
return status !== 'completed' || completedHandoff || ownIds.length === 0;
|
|
94
159
|
}
|
|
95
160
|
|
|
96
161
|
/**
|
|
97
|
-
* Resolve the active STATE.md path under a given repo root.
|
|
162
|
+
* Resolve the active STATE.md path under a given repo root — READ side.
|
|
98
163
|
*
|
|
99
164
|
* Returns the first existing candidate, checking the active platform first.
|
|
100
165
|
* Falls back to the active platform's state dir when none exist (matches the
|
|
101
|
-
* create-on-first-write semantics expected by callers).
|
|
166
|
+
* create-on-first-write semantics expected by callers). The legacy fallback to
|
|
167
|
+
* another platform's STATE.md is kept for readers (#890); writers must use
|
|
168
|
+
* {@link resolveStateMdWritePath}.
|
|
102
169
|
*
|
|
103
170
|
* @param {string|undefined} repoRoot
|
|
104
171
|
* @returns {string} Absolute path to STATE.md.
|
|
@@ -107,6 +174,83 @@ export function resolveStateMdPath(repoRoot) {
|
|
|
107
174
|
return resolveStateArtifactPath(repoRoot, 'STATE.md');
|
|
108
175
|
}
|
|
109
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Resolve the STATE.md path a WRITE may target (#1618, #1640).
|
|
179
|
+
*
|
|
180
|
+
* Every existing candidate — SO_STATE_DIR, the active platform's dir, the
|
|
181
|
+
* legacy dirs — must pass the ownership rule ({@link isWritableStateMd}); the
|
|
182
|
+
* first one that does wins. Before #1640 the override/active candidates were
|
|
183
|
+
* taken unchecked, so session-end Phase 3.7a wrote its recommendation fields
|
|
184
|
+
* into a FOREIGN completed `.claude/STATE.md` while this session's own record
|
|
185
|
+
* lived elsewhere. Without a qualifying candidate the preferred path is
|
|
186
|
+
* returned for create-on-first-write — unless that path itself holds a foreign
|
|
187
|
+
* file, in which case `blocked` is true and writers refuse.
|
|
188
|
+
*
|
|
189
|
+
* @param {string|undefined} repoRoot
|
|
190
|
+
* @returns {string} Absolute path to STATE.md.
|
|
191
|
+
*/
|
|
192
|
+
export function resolveStateMdWritePath(repoRoot) {
|
|
193
|
+
return resolveStateMdWriteTarget(repoRoot).path;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Like {@link resolveStateMdWritePath}, but also names the first STATE.md
|
|
198
|
+
* that was REJECTED on the way (#1618 review): `rejected` is the absolute path
|
|
199
|
+
* of an existing STATE.md not proven to be this process's, set only when the
|
|
200
|
+
* returned `path` is NOT a qualifying existing file (absent → create, or
|
|
201
|
+
* `blocked`). Callers use it to say why nothing was written instead of
|
|
202
|
+
* returning a silent no-op. `blocked` means `path` exists and is foreign:
|
|
203
|
+
* nothing may be written there (#1640).
|
|
204
|
+
*
|
|
205
|
+
* `opts.statePath` (#1640) pins the target explicitly — session-end passes the
|
|
206
|
+
* same path to Phase 3.7a and Phase 3.4 so both land in one file — and is
|
|
207
|
+
* subject to the same ownership rule (treated as a preferred candidate).
|
|
208
|
+
* `opts.completedHandoff` lets the Idle Reset take a preferred
|
|
209
|
+
* `status: completed` record (see {@link isWritableStateMd}).
|
|
210
|
+
*
|
|
211
|
+
* @param {string|undefined} repoRoot
|
|
212
|
+
* @param {{ statePath?: string, completedHandoff?: boolean }} [opts]
|
|
213
|
+
* @returns {{ path: string, rejected: string|null, blocked: boolean }}
|
|
214
|
+
*/
|
|
215
|
+
export function resolveStateMdWriteTarget(repoRoot, opts = {}) {
|
|
216
|
+
const { statePath, completedHandoff = false } = opts;
|
|
217
|
+
const ownIds = readProcessLocalSessionIds();
|
|
218
|
+
const explicit = typeof statePath === 'string' && statePath.trim() !== '';
|
|
219
|
+
const { candidates, preferredCount } = explicit
|
|
220
|
+
? { candidates: [resolvePath(repoRoot ?? process.cwd(), statePath)], preferredCount: 1 }
|
|
221
|
+
: stateArtifactCandidates(repoRoot, 'STATE.md');
|
|
222
|
+
let rejected = null;
|
|
223
|
+
for (const [index, candidate] of candidates.entries()) {
|
|
224
|
+
if (!existsSync(candidate)) continue;
|
|
225
|
+
if (isWritableStateMd(candidate, ownIds, { preferred: index < preferredCount, completedHandoff })) {
|
|
226
|
+
return { path: candidate, rejected: null, blocked: false };
|
|
227
|
+
}
|
|
228
|
+
rejected ??= candidate;
|
|
229
|
+
}
|
|
230
|
+
const path = candidates[0];
|
|
231
|
+
return { path, rejected, blocked: existsSync(path) };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* One stderr line naming a rejected foreign STATE.md, so a fail-closed
|
|
236
|
+
* write target never degrades into a silent no-op (#1618 review). When the
|
|
237
|
+
* rejected file IS the target (#1640 `blocked`), the line says nothing is
|
|
238
|
+
* written instead of naming a different target.
|
|
239
|
+
*
|
|
240
|
+
* @param {string} caller
|
|
241
|
+
* @param {string} rejected
|
|
242
|
+
* @param {string} target
|
|
243
|
+
*/
|
|
244
|
+
export function warnForeignStateMd(caller, rejected, target) {
|
|
245
|
+
const outcome = rejected === target
|
|
246
|
+
? 'nothing written — it is no session-own write target'
|
|
247
|
+
: `write target is ${target}`;
|
|
248
|
+
process.stderr.write(
|
|
249
|
+
`⚠ ${caller}: ignoring ${rejected} — its session-id is not this process's ` +
|
|
250
|
+
`(no matching process-local session id); ${outcome} (#1618/#1640)\n`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
110
254
|
// ---------------------------------------------------------------------------
|
|
111
255
|
// Size-ceiling guard (issue #739)
|
|
112
256
|
// ---------------------------------------------------------------------------
|
|
@@ -351,27 +495,40 @@ function writeFileAtomic(filePath, contents) {
|
|
|
351
495
|
* frontmatter-unsafe breach throws an `Error`
|
|
352
496
|
* (`.code === 'STATE_MD_FRONTMATTER_UNSAFE'`) instead of returning a no-op
|
|
353
497
|
* result. Default `false`.
|
|
498
|
+
* @param {string} [opts.statePath] — explicit STATE.md target (#1640), absolute
|
|
499
|
+
* or repo-relative; still ownership-checked. A foreign existing target is
|
|
500
|
+
* refused with `reason: 'foreign-state-md'`, never overwritten.
|
|
501
|
+
* @param {boolean} [opts.completedHandoff] — Idle Reset only: a preferred
|
|
502
|
+
* `status: completed` record qualifies as write target (#1640).
|
|
354
503
|
* @returns {Promise<{ written: boolean, path: string, contents: string|null, reason?: string }>}
|
|
355
504
|
*/
|
|
356
505
|
export async function writeStateMd(repoRoot, transformer, opts = {}) {
|
|
357
506
|
if (typeof transformer !== 'function') {
|
|
358
507
|
throw new TypeError('writeStateMd: transformer must be a function');
|
|
359
508
|
}
|
|
360
|
-
const statePath =
|
|
509
|
+
const { path: statePath, rejected, blocked } = resolveStateMdWriteTarget(repoRoot, {
|
|
510
|
+
statePath: opts.statePath,
|
|
511
|
+
completedHandoff: opts.completedHandoff,
|
|
512
|
+
});
|
|
513
|
+
if (rejected) warnForeignStateMd('writeStateMd', rejected, statePath);
|
|
514
|
+
const foreign = rejected ? { reason: 'foreign-state-md' } : {};
|
|
515
|
+
// #1640: the only existing file at the target belongs to another session —
|
|
516
|
+
// never overwrite it, never call the transformer on its contents.
|
|
517
|
+
if (blocked) return { written: false, path: statePath, contents: null, ...foreign };
|
|
361
518
|
return withStateMdLock(
|
|
362
519
|
repoRoot,
|
|
363
520
|
async () => {
|
|
364
521
|
const before = existsSync(statePath) ? readFileSync(statePath, 'utf8') : '';
|
|
365
522
|
const after = await transformer(before);
|
|
366
523
|
if (after === null || after === undefined) {
|
|
367
|
-
return { written: false, path: statePath, contents: null };
|
|
524
|
+
return { written: false, path: statePath, contents: null, ...foreign };
|
|
368
525
|
}
|
|
369
526
|
if (typeof after !== 'string') {
|
|
370
527
|
throw new TypeError('writeStateMd: transformer must return a string or null');
|
|
371
528
|
}
|
|
372
529
|
if (after === before) {
|
|
373
530
|
// No textual change — skip the write to keep mtime stable.
|
|
374
|
-
return { written: false, path: statePath, contents: before };
|
|
531
|
+
return { written: false, path: statePath, contents: before, ...foreign };
|
|
375
532
|
}
|
|
376
533
|
|
|
377
534
|
const ceilingBytes = typeof opts._ceilingBytes === 'number' && opts._ceilingBytes > 0
|
|
@@ -456,6 +613,107 @@ export async function updateFrontmatterFieldsOnDisk(repoRoot, fields, opts = {})
|
|
|
456
613
|
return writeStateMd(repoRoot, (contents) => updateFrontmatterFields(contents, fields), opts);
|
|
457
614
|
}
|
|
458
615
|
|
|
616
|
+
/**
|
|
617
|
+
* Why the STATE.md `session-id` `sessionId` may NOT be taken over by this
|
|
618
|
+
* process (#1640 review MED-1), or `null` when it may. A non-completed record
|
|
619
|
+
* passes the write rule even with a foreign id (a `/clear`ed session's own
|
|
620
|
+
* file still names its pre-`/clear` id), so before re-stamping the id and
|
|
621
|
+
* completing the file we ask the host session registry whether that id is
|
|
622
|
+
* still REGISTERED at all (`readRegistry()`; the caller already excludes our
|
|
623
|
+
* own id). A registered session owns the file: completing it would lock that
|
|
624
|
+
* session out of its own record. No freshness window: the heartbeat is only
|
|
625
|
+
* written at turn end (hooks/on-stop.mjs), and 92 of 954 consecutive
|
|
626
|
+
* `orchestrator.turn.stopped` gaps (48 of 64 sessions) exceeded 15 min —
|
|
627
|
+
* an idle-but-live peer would look dead. Measured 2026-10-15: 13 of 13
|
|
628
|
+
* sessions that ended with SessionEnd `reason: clear` were already
|
|
629
|
+
* deregistered (on-session-end.mjs deregisters on every reason), so a
|
|
630
|
+
* `/clear` predecessor does not block its successor's close.
|
|
631
|
+
*
|
|
632
|
+
* Fail-closed: a registry directory that exists but cannot be read refuses.
|
|
633
|
+
* An absent registry (ENOENT) means no live peers.
|
|
634
|
+
*
|
|
635
|
+
* CEILING (BV-004): the registry is host-wide, not per checkout, and the
|
|
636
|
+
* check runs inside the STATE.md lock but not under the registry's. A session
|
|
637
|
+
* that crashed without SessionEnd (or whose deregistration failed) stays
|
|
638
|
+
* registered until `sweepZombies()` removes it after 60 min of inactivity at
|
|
639
|
+
* the next SessionStart of ANY session — until then no other session can
|
|
640
|
+
* complete its STATE.md (fail-closed direction, refused loudly). Revisit if
|
|
641
|
+
* that refusal shows up in close transcripts.
|
|
642
|
+
*
|
|
643
|
+
* Lazy import: this module is in the hook import closure; the registry is
|
|
644
|
+
* only needed on this async session-end path.
|
|
645
|
+
*
|
|
646
|
+
* @param {string} sessionId
|
|
647
|
+
* @param {string|undefined} ownId
|
|
648
|
+
* @returns {Promise<string|null>}
|
|
649
|
+
*/
|
|
650
|
+
async function liveForeignOwner(sessionId) {
|
|
651
|
+
try {
|
|
652
|
+
const { activeDir, readRegistry } = await import('../session-registry.mjs');
|
|
653
|
+
const { readdir } = await import('node:fs/promises');
|
|
654
|
+
try {
|
|
655
|
+
await readdir(activeDir());
|
|
656
|
+
} catch (err) {
|
|
657
|
+
if (err?.code === 'ENOENT') return null;
|
|
658
|
+
return `session registry unreadable (${err?.code ?? 'error'})`;
|
|
659
|
+
}
|
|
660
|
+
const entries = await readRegistry();
|
|
661
|
+
return entries.some((entry) => entry.session_id === sessionId)
|
|
662
|
+
? 'its session-id belongs to a session still registered on this host'
|
|
663
|
+
: null;
|
|
664
|
+
} catch (err) {
|
|
665
|
+
return `session registry unreadable (${err?.code ?? err?.message ?? 'error'})`;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* session-end Phase 3.4 (#1640): finalize THIS session's STATE.md in one
|
|
671
|
+
* locked write — `status: completed`, `updated`, optionally `current-wave`,
|
|
672
|
+
* and `session-id` re-stamped to the closing process's own raw id when one is
|
|
673
|
+
* measurable. The re-stamp keeps a `/clear`ed session's record its own after
|
|
674
|
+
* 3.4: the writers that run LATER in the close — Phase 5's
|
|
675
|
+
* `markOpenQuestionAnsweredOnDisk` — would otherwise find a completed record
|
|
676
|
+
* naming the pre-`/clear` id and be refused as foreign. Without a measurable
|
|
677
|
+
* id the existing `session-id` is left as is.
|
|
678
|
+
*
|
|
679
|
+
* Refused (`reason: 'foreign-state-md'`, one stderr line, file byte-identical)
|
|
680
|
+
* when the record's `session-id` is neither this process's nor free to take
|
|
681
|
+
* over — see {@link liveForeignOwner}. Placeholder (no `session-id`), idle and
|
|
682
|
+
* own records are never checked against the registry.
|
|
683
|
+
*
|
|
684
|
+
* @param {string} repoRoot — absolute path to the repository root (required)
|
|
685
|
+
* @param {object} [opts] — `writeStateMd` options; pass the SAME `statePath` as Phase 3.7a.
|
|
686
|
+
* @param {number} [opts.currentWave] — final wave count, written as `current-wave`.
|
|
687
|
+
* @returns {Promise<{ written: boolean, path: string, contents: string|null, reason?: string }>}
|
|
688
|
+
* @throws {Error} when repoRoot is undefined, null, or empty
|
|
689
|
+
*/
|
|
690
|
+
export async function markStateMdCompletedOnDisk(repoRoot, opts = {}) {
|
|
691
|
+
requireRepoRoot(repoRoot, 'markStateMdCompletedOnDisk');
|
|
692
|
+
const { currentWave, ...writeOpts } = opts;
|
|
693
|
+
const fields = { status: 'completed', updated: new Date().toISOString() };
|
|
694
|
+
if (Number.isInteger(currentWave)) fields['current-wave'] = currentWave;
|
|
695
|
+
const [ownId] = readProcessLocalSessionIds();
|
|
696
|
+
if (ownId) fields['session-id'] = ownId;
|
|
697
|
+
let refusal = null;
|
|
698
|
+
const result = await writeStateMd(
|
|
699
|
+
repoRoot,
|
|
700
|
+
async (contents) => {
|
|
701
|
+
const frontmatter = parseStateMd(contents)?.frontmatter;
|
|
702
|
+
const raw = frontmatter?.['session-id'];
|
|
703
|
+
const sessionId = typeof raw === 'string' ? raw.trim() : '';
|
|
704
|
+
if (sessionId && sessionId !== ownId && frontmatter.status !== 'idle') {
|
|
705
|
+
refusal = await liveForeignOwner(sessionId);
|
|
706
|
+
if (refusal) return null;
|
|
707
|
+
}
|
|
708
|
+
return updateFrontmatterFields(contents, fields);
|
|
709
|
+
},
|
|
710
|
+
writeOpts,
|
|
711
|
+
);
|
|
712
|
+
if (!refusal) return result;
|
|
713
|
+
process.stderr.write(`⚠ markStateMdCompletedOnDisk: refusing ${result.path} — ${refusal}; nothing written (#1640)\n`);
|
|
714
|
+
return { ...result, written: false, reason: 'foreign-state-md' };
|
|
715
|
+
}
|
|
716
|
+
|
|
459
717
|
/**
|
|
460
718
|
* Convenience wrapper: apply `touchUpdatedField` under the state-lock.
|
|
461
719
|
*
|