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,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* close-precheck.mjs — the `/close` pre-check as code (#1661).
|
|
3
|
+
*
|
|
4
|
+
* The pre-check used to be prose that read `<state-dir>/STATE.md` and acted on
|
|
5
|
+
* its `status` alone. It never asked whose STATE.md that is. Two failures
|
|
6
|
+
* followed (2026-10-17, three sessions in one afternoon):
|
|
7
|
+
* - After `/clear` the tab runs under a NEW native id while STATE.md still
|
|
8
|
+
* carries the previous one. A completed foreign STATE.md read as "already
|
|
9
|
+
* finalized", the session answered with the wait sentence although its own
|
|
10
|
+
* handover was written, and the inbox kept nudging.
|
|
11
|
+
* - A foreign ACTIVE STATE.md (a peer working in the same checkout) passed
|
|
12
|
+
* condition 3, so session-end would have closed the peer's session.
|
|
13
|
+
*
|
|
14
|
+
* Ownership is decided on the native id only: STATE.md `session-id` (#1368)
|
|
15
|
+
* against `readProcessLocalSessionIds()`. The session lock is no proof here —
|
|
16
|
+
* it records the pid of the short-lived hook process, and in a shared checkout
|
|
17
|
+
* it may name a peer.
|
|
18
|
+
*
|
|
19
|
+
* Unproven ownership (#1718): a STATE.md without `session-id` (written before
|
|
20
|
+
* #1368, in practice a foreign old file) or a process that cannot name its own
|
|
21
|
+
* id never runs session-end on status alone. A would-be session-end verdict
|
|
22
|
+
* becomes `ownership-unproven` with a `confirmToken` bound to the STATE.md path
|
|
23
|
+
* and its raw bytes; only that token (`--confirm-own`) turns it back, and only while the
|
|
24
|
+
* file is unchanged. Field incident 2026-10-21: a legacy STATE.md of an older,
|
|
25
|
+
* already closed plan read `complete-missing-record`.
|
|
26
|
+
*
|
|
27
|
+
* Ceiling (BV-004): on platforms without a native id (Cursor, Pi, Codex
|
|
28
|
+
* without a thread id) that is one operator confirmation per `/close`.
|
|
29
|
+
* Revisit when those platforms expose a native session id.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { createHash } from 'node:crypto';
|
|
33
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
34
|
+
import path from 'node:path';
|
|
35
|
+
|
|
36
|
+
import { parseStateMd } from '../state-md.mjs';
|
|
37
|
+
import { findRecordedSession } from '../session-close-backfill.mjs';
|
|
38
|
+
import { readLock } from '../session-lock.mjs';
|
|
39
|
+
import { readProcessLocalSessionIds } from '../session-identity/own-session.mjs';
|
|
40
|
+
import { readJsonlFile } from '../io.mjs';
|
|
41
|
+
import { resolveEventsLedgerRoot, resolveStateDir } from '../platform.mjs';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Confirmation token for an `ownership-unproven` STATE.md (#1718): bound to the
|
|
45
|
+
* STATE.md path and its raw bytes, so a byte-identical file in another checkout
|
|
46
|
+
* yields a different token.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} statePath path of the STATE.md (resolved to absolute)
|
|
49
|
+
* @param {Buffer|string} raw the raw STATE.md bytes
|
|
50
|
+
* @returns {string} 16 lowercase hex characters
|
|
51
|
+
*/
|
|
52
|
+
export function computeConfirmToken(statePath, raw) {
|
|
53
|
+
return createHash('sha256').update(path.resolve(statePath)).update('\0').update(raw).digest('hex').slice(0, 16);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Verdicts that hand off to session-end; every other verdict stops `/close`. */
|
|
57
|
+
export const RUNS_SESSION_END = new Set(['run-session-end', 'complete-missing-record']);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Classify `<state-dir>/STATE.md` for `/close`.
|
|
61
|
+
*
|
|
62
|
+
* @param {object} opts
|
|
63
|
+
* @param {string} opts.repoRoot project root (the working tree `/close` runs in)
|
|
64
|
+
* @param {string} [opts.stateDir] `.claude` | `.codex` | `.cursor` | `.pi`; default from the platform
|
|
65
|
+
* @param {object} [opts.env=process.env] injectable for tests
|
|
66
|
+
* @param {string} [opts.confirmToken] token from an earlier `ownership-unproven`
|
|
67
|
+
* result; bound to the STATE.md path and its raw bytes, confirms a legacy/unprovable
|
|
68
|
+
* STATE.md while that file is unchanged (#1718)
|
|
69
|
+
* @returns {{
|
|
70
|
+
* verdict: 'no-state'|'foreign'|'run-session-end'|'complete-missing-record'|'already-finalized'|'unexpected-status'|'inconclusive'|'ownership-unproven',
|
|
71
|
+
* ownership: 'own'|'foreign'|'legacy'|'unprovable'|'confirmed'|null,
|
|
72
|
+
* status: string|null, stateSessionId: string|null, ownSessionIds: string[],
|
|
73
|
+
* statePath: string, reason?: string,
|
|
74
|
+
* wouldRun?: 'run-session-end'|'complete-missing-record', confirmToken?: string,
|
|
75
|
+
* session?: string|null, startedAt?: string|null
|
|
76
|
+
* }} the last four only with `ownership-unproven`
|
|
77
|
+
*/
|
|
78
|
+
export function classifyCloseState({ repoRoot, stateDir, env = process.env, confirmToken } = {}) {
|
|
79
|
+
const dir = stateDir ?? resolveStateDir();
|
|
80
|
+
const statePath = path.join(repoRoot, dir, 'STATE.md');
|
|
81
|
+
const ownSessionIds = readProcessLocalSessionIds({ env });
|
|
82
|
+
const base = { ownership: null, status: null, stateSessionId: null, ownSessionIds, statePath };
|
|
83
|
+
|
|
84
|
+
if (!existsSync(statePath)) return { ...base, verdict: 'no-state' };
|
|
85
|
+
|
|
86
|
+
const raw = readFileSync(statePath);
|
|
87
|
+
const parsed = parseStateMd(raw.toString('utf8'));
|
|
88
|
+
if (!parsed) return { ...base, verdict: 'inconclusive', reason: 'STATE.md has no readable frontmatter' };
|
|
89
|
+
const fm = parsed.frontmatter;
|
|
90
|
+
const status = typeof fm.status === 'string' ? fm.status : null;
|
|
91
|
+
const rawId = typeof fm['session-id'] === 'string' ? fm['session-id'].trim() : '';
|
|
92
|
+
const stateSessionId = rawId || null;
|
|
93
|
+
|
|
94
|
+
const ownership = !stateSessionId ? 'legacy'
|
|
95
|
+
: ownSessionIds.length === 0 ? 'unprovable'
|
|
96
|
+
: ownSessionIds.includes(stateSessionId) ? 'own' : 'foreign';
|
|
97
|
+
const known = { ...base, ownership, status, stateSessionId };
|
|
98
|
+
|
|
99
|
+
// A foreign STATE.md is never this session's to close, whatever its status:
|
|
100
|
+
// completed belongs to an earlier tab incarnation or a peer, active/paused to a
|
|
101
|
+
// peer (or to this tab before a /clear, which this process cannot prove).
|
|
102
|
+
if (ownership === 'foreign') return { ...known, verdict: 'foreign' };
|
|
103
|
+
|
|
104
|
+
// #1718: legacy/unprovable never reaches session-end without a token bound
|
|
105
|
+
// to this path and the bytes read here; every non-session-end verdict passes unchanged.
|
|
106
|
+
const gate = (wouldRun) => {
|
|
107
|
+
if (ownership === 'own') return { ...known, verdict: wouldRun };
|
|
108
|
+
const token = computeConfirmToken(statePath, raw);
|
|
109
|
+
if (confirmToken === token) return { ...known, ownership: 'confirmed', verdict: wouldRun };
|
|
110
|
+
const unproven = {
|
|
111
|
+
...known, verdict: 'ownership-unproven', wouldRun, confirmToken: token,
|
|
112
|
+
session: typeof fm.session === 'string' ? fm.session : null,
|
|
113
|
+
startedAt: typeof fm.started_at === 'string' ? fm.started_at : null,
|
|
114
|
+
};
|
|
115
|
+
if (confirmToken === undefined) return unproven;
|
|
116
|
+
return { ...unproven, reason: 'confirm token does not match STATE.md (changed since the pre-check, or the token belongs to another file)' };
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if (status === 'active' || status === 'paused') return gate('run-session-end');
|
|
120
|
+
if (status !== 'completed') return { ...known, verdict: 'unexpected-status' };
|
|
121
|
+
|
|
122
|
+
// #429: completed alone does not prove the Phase 3.7 writer ran.
|
|
123
|
+
let records;
|
|
124
|
+
try {
|
|
125
|
+
records = readJsonlFile(path.join(resolveEventsLedgerRoot(repoRoot), '.orchestrator', 'metrics', 'sessions.jsonl'));
|
|
126
|
+
} catch (err) {
|
|
127
|
+
return { ...known, verdict: 'inconclusive', reason: `sessions.jsonl unreadable: ${err.message}` };
|
|
128
|
+
}
|
|
129
|
+
// The lock fallback only for STATE.md without session-id (pre-#1368), and
|
|
130
|
+
// only when the lock names THIS process (#1663). A peer's lock UUID would
|
|
131
|
+
// become the native key in findRecordedSession, skip this session's own
|
|
132
|
+
// record (different UUID) before the label match is reached, and send
|
|
133
|
+
// session-end a second time. Foreign or unprovable lock → no fallback, the
|
|
134
|
+
// label + started_at join decides.
|
|
135
|
+
let lockId = null;
|
|
136
|
+
if (!stateSessionId) {
|
|
137
|
+
let lockRaw;
|
|
138
|
+
try { lockRaw = readLock({ repoRoot })?.session_id ?? null; } catch { lockRaw = null; }
|
|
139
|
+
const trimmed = typeof lockRaw === 'string' ? lockRaw.trim() : '';
|
|
140
|
+
if (trimmed && ownSessionIds.includes(trimmed)) lockId = trimmed;
|
|
141
|
+
}
|
|
142
|
+
const record = findRecordedSession(records, {
|
|
143
|
+
sessionId: stateSessionId ?? lockId,
|
|
144
|
+
semanticSessionId: fm.session ?? null,
|
|
145
|
+
startedAt: fm.started_at ?? null,
|
|
146
|
+
});
|
|
147
|
+
// An abandoned backfill stub is no completion; a legacy record without
|
|
148
|
+
// `status` (written before the field existed) is authoritative.
|
|
149
|
+
const finalized = record && (record.status === undefined || record.status === 'completed');
|
|
150
|
+
return finalized ? { ...known, verdict: 'already-finalized' } : gate('complete-missing-record');
|
|
151
|
+
}
|
|
@@ -61,11 +61,11 @@
|
|
|
61
61
|
*/
|
|
62
62
|
|
|
63
63
|
import { existsSync } from 'node:fs';
|
|
64
|
-
import path from 'node:path';
|
|
65
64
|
|
|
66
65
|
import { collectProposals } from '../memory-proposals/collector.mjs';
|
|
67
66
|
import { sweepExpiredLearnings } from '../learnings/expiry-sweep.mjs';
|
|
68
|
-
import { readSkillInvocations } from '../skill-invocations-schema.mjs';
|
|
67
|
+
import { readSkillInvocations, defaultSkillInvocationsPath } from '../skill-invocations-schema.mjs';
|
|
68
|
+
import { resolveLedgerPath } from '../ledger-path.mjs';
|
|
69
69
|
import { runReconcileFromPhaseSkip, resolveEffectiveTargets } from '../reconcile/engine.mjs';
|
|
70
70
|
|
|
71
71
|
// NOTE (2026-09-09): `shouldDispatchAutoDream` / `shouldDispatchAutoDialectic`
|
|
@@ -91,10 +91,11 @@ import { runReconcileFromPhaseSkip, resolveEffectiveTargets } from '../reconcile
|
|
|
91
91
|
* @returns {{filePath: string, archivePath: string}}
|
|
92
92
|
*/
|
|
93
93
|
export function resolveLearningsPaths(repoRoot) {
|
|
94
|
-
|
|
94
|
+
// #1722: each store resolves on its own — a linked worktree of a repo that
|
|
95
|
+
// ignores them maps to the main checkout, a versioned one stays local.
|
|
95
96
|
return {
|
|
96
|
-
filePath:
|
|
97
|
-
archivePath:
|
|
97
|
+
filePath: resolveLedgerPath('.orchestrator/metrics/learnings.jsonl', repoRoot),
|
|
98
|
+
archivePath: resolveLedgerPath('.orchestrator/metrics/learnings-archive.jsonl', repoRoot),
|
|
98
99
|
};
|
|
99
100
|
}
|
|
100
101
|
|
|
@@ -232,7 +233,7 @@ async function decideSkillJudge({ repoRoot, cfg, sessionId }) {
|
|
|
232
233
|
return mkSkip(phase, 'disabled (skill-evolution.judge=false)', 'config-gate');
|
|
233
234
|
}
|
|
234
235
|
if (isPersistenceOff(cfg)) return mkSkip(phase, 'persistence=false', 'config-gate');
|
|
235
|
-
const invPath =
|
|
236
|
+
const invPath = defaultSkillInvocationsPath(repoRoot);
|
|
236
237
|
const entries = await readSkillInvocations(invPath);
|
|
237
238
|
const noSessionFilter = sessionId === null || sessionId === undefined;
|
|
238
239
|
const judged = new Set(
|
|
@@ -272,7 +273,7 @@ async function decideReconcile({ repoRoot, cfg }) {
|
|
|
272
273
|
if (cfg?.reconcile?.enabled !== true) {
|
|
273
274
|
return mkSkip(phase, 'disabled (reconcile.enabled=false)', 'config-gate');
|
|
274
275
|
}
|
|
275
|
-
const learningsPath =
|
|
276
|
+
const learningsPath = resolveLearningsPaths(repoRoot).filePath;
|
|
276
277
|
if (!existsSync(learningsPath)) {
|
|
277
278
|
return mkSkip(phase, 'learnings.jsonl absent', 'learnings.jsonl');
|
|
278
279
|
}
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
*
|
|
42
42
|
* @typedef {Object} SweepResult
|
|
43
43
|
* @property {boolean} ran - true only when the store was actually rewritten.
|
|
44
|
-
* @property {string} [reason] - why it did not run ('plan-skip' | 'no-plan' |
|
|
44
|
+
* @property {string} [reason] - why it did not run ('plan-skip' | 'no-plan' |
|
|
45
|
+
* 'store-role-inbox' | 'error').
|
|
45
46
|
* @property {string} [error] - error message, `reason === 'error'` only.
|
|
46
47
|
* @property {number} [scanned] - entries read from the active store (ran only).
|
|
47
48
|
* @property {number} [archived] - entries moved to the archive sidecar (ran only).
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
* @property {string} [archivePath] - the archive sidecar written to (ran only).
|
|
51
52
|
*/
|
|
52
53
|
|
|
53
|
-
import { sweepExpiredLearnings } from '../learnings/expiry-sweep.mjs';
|
|
54
|
+
import { SKIPPED_STORE_ROLE_INBOX, sweepExpiredLearnings } from '../learnings/expiry-sweep.mjs';
|
|
54
55
|
import { resolveLearningsPaths } from './phase-skip.mjs';
|
|
55
56
|
|
|
56
57
|
/** Event name — a plain string literal so the events-schema census can see it. */
|
|
@@ -151,6 +152,10 @@ export async function runExpiredSweep({ repoRoot, plan, now, graceDays, emit = t
|
|
|
151
152
|
...(graceDays === undefined ? {} : { graceDays }),
|
|
152
153
|
});
|
|
153
154
|
|
|
155
|
+
// Inbox role (#1516): the sweep wrote nothing, so this is not a run and
|
|
156
|
+
// emits no sweep_applied — that record's presence claims a rewrite.
|
|
157
|
+
if (res?.skipped === SKIPPED_STORE_ROLE_INBOX) return { ran: false, reason: SKIPPED_STORE_ROLE_INBOX };
|
|
158
|
+
|
|
154
159
|
const scanned = res?.scanned ?? 0;
|
|
155
160
|
const archived = res?.archived ?? 0;
|
|
156
161
|
// The sweep result omits `malformed` when the read found none, so absent IS
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vault-orphans.mjs - Phase 3.7e Vault Orphans (#1563).
|
|
3
|
+
*
|
|
4
|
+
* Lists the untracked files in the Meta-Vault that were modified since this
|
|
5
|
+
* session started, so the closing session can NAME what it may have left
|
|
6
|
+
* behind there.
|
|
7
|
+
*
|
|
8
|
+
* Why this exists (measured 2026-10-11): one vault checkout had accumulated 268
|
|
9
|
+
* untracked files over five days - website audits, session reports, reviews,
|
|
10
|
+
* lead handovers - written by sessions of OTHER repos into their configured
|
|
11
|
+
* `vault-dir`. None of them was ever committed: a session's close commits only
|
|
12
|
+
* in its own repo, and vault-mirror auto-commits only its own generator-marked
|
|
13
|
+
* files (`scripts/lib/vault-mirror/auto-commit.mjs`). The writing session is the
|
|
14
|
+
* only one that knows it wrote them, and nothing made it look.
|
|
15
|
+
*
|
|
16
|
+
* THE IRON RULE: this module PROPOSES, it never DISPOSES. Its only git
|
|
17
|
+
* invocation is the read-only `ls-files --others --exclude-standard -z`. No
|
|
18
|
+
* add, commit, rm, mv, clean, stash; no file is written, moved or deleted. The
|
|
19
|
+
* vault is another repo with its own rules (PSA-003).
|
|
20
|
+
*
|
|
21
|
+
* The return field is named `candidates`, not `orphans`: mtime is a HINT, not
|
|
22
|
+
* proof of ownership. A concurrent session of another repo may have written the
|
|
23
|
+
* same file in the same window. The coordinator names them and records one Open
|
|
24
|
+
* Question; it never acts on them.
|
|
25
|
+
*
|
|
26
|
+
* Three states, never two (a missing measurement must not look like zero):
|
|
27
|
+
* - `{ status: 'ok', total: 0 }` - measured, nothing found
|
|
28
|
+
* - `{ status: 'ok', total: N }` - measured, N candidates
|
|
29
|
+
* - `{ status: 'unavailable' }` - could not measure (no vault dir, not a
|
|
30
|
+
* git repo, git failed). Never reported as
|
|
31
|
+
* "no orphans".
|
|
32
|
+
*
|
|
33
|
+
* Exclusion - what another writer already owns: any file whose first 4 KB
|
|
34
|
+
* carries a `_generator: session-orchestrator-` frontmatter line. Census
|
|
35
|
+
* 2026-10-11 @ 686e2c12 (`rg -n "GENERATOR_MARKER\s*=" scripts/`): four marker
|
|
36
|
+
* values, all with that prefix - `vault-mirror@1` (40-learnings/, 50-sessions/,
|
|
37
|
+
* monthly rollups), `vault-status-narrative@1` (01-projects/<slug>/
|
|
38
|
+
* _session-narrative.md), `active-sessions@1` (01-projects/_active-sessions.md),
|
|
39
|
+
* `gitlab-portfolio@1`. The match is line-anchored so a hand-written note that
|
|
40
|
+
* merely QUOTES the marker in prose is still reported.
|
|
41
|
+
*
|
|
42
|
+
* There is deliberately NO blanket exclusion of `40-learnings/` / `50-sessions/`:
|
|
43
|
+
* vault-mirror owns only the MARKED files there. `autoCommitVaultMirror` treats
|
|
44
|
+
* an unmarked staged file in those dirs as an offender and skips its whole
|
|
45
|
+
* commit, so an unmarked untracked file there is owned by nobody AND blocks the
|
|
46
|
+
* mirror - the last thing to hide.
|
|
47
|
+
*
|
|
48
|
+
* Never throws. DI seams (#580-DI-001), synchronous like the Phase 4b sibling
|
|
49
|
+
* ./worktree-orphan-sweep.mjs: `execFileFn` (default `execFileSync`) and
|
|
50
|
+
* `statFn` (default `lstatSync` - a symlink is reported as itself, never
|
|
51
|
+
* followed out of the vault). Every git call passes an args ARRAY, no shell
|
|
52
|
+
* (#577 HARDEN-001).
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
import path from 'node:path';
|
|
56
|
+
import { execFileSync } from 'node:child_process';
|
|
57
|
+
import { closeSync, lstatSync, openSync, readSync } from 'node:fs';
|
|
58
|
+
|
|
59
|
+
import { redactHomeDir } from '../common.mjs';
|
|
60
|
+
|
|
61
|
+
/** Upper bound on the returned `candidates` list; `total` still counts all. */
|
|
62
|
+
export const MAX_CANDIDATES = 200;
|
|
63
|
+
|
|
64
|
+
/** Bytes read per file for the generator check - same window as auto-commit.mjs. */
|
|
65
|
+
const HEAD_BYTES = 4096;
|
|
66
|
+
|
|
67
|
+
/** A frontmatter line naming one of this plugin's generators. */
|
|
68
|
+
const GENERATOR_LINE_RE = /^_generator:[ \t]*session-orchestrator-/m;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Env vars that retarget git away from `-C <dir>`. An inherited `GIT_DIR` beats
|
|
72
|
+
* `-C` (incident 2026-08-19), which here would list ANOTHER repo's untracked
|
|
73
|
+
* files as vault candidates. Stripped for the one git call below.
|
|
74
|
+
*/
|
|
75
|
+
const REPO_REDIRECT_ENV = ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_COMMON_DIR'];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Ceiling for git's NUL-separated path list: 16 MiB is ~150k paths at ~100
|
|
79
|
+
* bytes. Past it, execFileSync throws ENOBUFS and the result is `unavailable`
|
|
80
|
+
* (could not measure), never a silently cut list. Revisit if a vault ever
|
|
81
|
+
* reports that reason.
|
|
82
|
+
*/
|
|
83
|
+
const LS_FILES_MAX_BUFFER = 16 * 1024 * 1024;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A vault on a hung network filesystem must not stall /close: past this the
|
|
87
|
+
* call is killed and the result is `unavailable` (git-failed). 20k untracked
|
|
88
|
+
* files took 0.46 s (2026-10-11), so 20 s is a hang, not a slow vault.
|
|
89
|
+
*/
|
|
90
|
+
const LS_FILES_TIMEOUT_MS = 20_000;
|
|
91
|
+
|
|
92
|
+
/** @param {string} absPath @returns {boolean} true when the head names one of our generators */
|
|
93
|
+
function isGeneratorOwned(absPath) {
|
|
94
|
+
let fd;
|
|
95
|
+
try {
|
|
96
|
+
fd = openSync(absPath, 'r');
|
|
97
|
+
const buf = Buffer.alloc(HEAD_BYTES);
|
|
98
|
+
const n = readSync(fd, buf, 0, HEAD_BYTES, 0);
|
|
99
|
+
return GENERATOR_LINE_RE.test(buf.subarray(0, n).toString('utf8'));
|
|
100
|
+
} catch {
|
|
101
|
+
// Unreadable -> not provably someone else's. This module only REPORTS, so
|
|
102
|
+
// the safe direction is to show the file, not to hide it.
|
|
103
|
+
return false;
|
|
104
|
+
} finally {
|
|
105
|
+
if (fd !== undefined) {
|
|
106
|
+
try {
|
|
107
|
+
closeSync(fd);
|
|
108
|
+
} catch {
|
|
109
|
+
/* already closed */
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {string} reason
|
|
117
|
+
* @param {string} [detail]
|
|
118
|
+
* @returns {{status: 'unavailable', reason: string, detail?: string, candidates: []}}
|
|
119
|
+
*/
|
|
120
|
+
function unavailable(reason, detail) {
|
|
121
|
+
return detail ? { status: 'unavailable', reason, detail, candidates: [] } : { status: 'unavailable', reason, candidates: [] };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Find untracked vault files modified at or after `sinceMs`.
|
|
126
|
+
*
|
|
127
|
+
* @param {object} [opts]
|
|
128
|
+
* @param {string} opts.vaultDir - Vault working-tree root (already tilde-expanded).
|
|
129
|
+
* @param {number} opts.sinceMs - Session start, epoch ms. Files with an older
|
|
130
|
+
* mtime are not candidates.
|
|
131
|
+
* @param {Function} [opts.execFileFn] - execFileSync seam: (file, args, options) => string.
|
|
132
|
+
* @param {Function} [opts.statFn] - lstatSync seam: (absPath) => { mtimeMs, isFile() }.
|
|
133
|
+
* @returns {{status: 'ok', candidates: Array<{path: string, mtimeMs: number}>,
|
|
134
|
+
* total: number, truncated: boolean, untracked: number}
|
|
135
|
+
* | {status: 'unavailable', reason: string, detail?: string, candidates: []}}
|
|
136
|
+
* `path` is vault-relative as git reports it. `total` counts every candidate,
|
|
137
|
+
* `candidates` holds at most MAX_CANDIDATES of them (sorted by path),
|
|
138
|
+
* `untracked` is everything git listed before the mtime and generator filters.
|
|
139
|
+
*/
|
|
140
|
+
export function findVaultOrphans({ vaultDir, sinceMs, execFileFn, statFn } = {}) {
|
|
141
|
+
try {
|
|
142
|
+
if (typeof vaultDir !== 'string' || vaultDir.trim() === '') return unavailable('no-vault-dir');
|
|
143
|
+
if (typeof sinceMs !== 'number' || !Number.isFinite(sinceMs)) return unavailable('invalid-since');
|
|
144
|
+
|
|
145
|
+
const exec = typeof execFileFn === 'function' ? execFileFn : execFileSync;
|
|
146
|
+
const stat = typeof statFn === 'function' ? statFn : lstatSync;
|
|
147
|
+
const root = path.resolve(vaultDir);
|
|
148
|
+
|
|
149
|
+
const env = { ...process.env };
|
|
150
|
+
for (const key of REPO_REDIRECT_ENV) delete env[key];
|
|
151
|
+
|
|
152
|
+
// -- THE ONLY GIT CALL - read-only --------------------------------------
|
|
153
|
+
let out;
|
|
154
|
+
try {
|
|
155
|
+
out = exec('git', ['-C', root, 'ls-files', '--others', '--exclude-standard', '-z'], {
|
|
156
|
+
encoding: 'utf8',
|
|
157
|
+
env,
|
|
158
|
+
maxBuffer: LS_FILES_MAX_BUFFER,
|
|
159
|
+
timeout: LS_FILES_TIMEOUT_MS,
|
|
160
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
161
|
+
});
|
|
162
|
+
} catch (err) {
|
|
163
|
+
const stderr = typeof err?.stderr === 'string' ? err.stderr : String(err?.stderr ?? '');
|
|
164
|
+
const reason = /not a git repository/i.test(stderr) ? 'not-a-git-repo' : 'git-failed';
|
|
165
|
+
const firstLine = redactHomeDir(stderr.split('\n').find((l) => l.trim() !== '') ?? err?.code ?? '');
|
|
166
|
+
return unavailable(reason, typeof firstLine === 'string' ? firstLine.trim().slice(0, 200) : undefined);
|
|
167
|
+
}
|
|
168
|
+
if (typeof out !== 'string') return unavailable('git-failed', 'non-string git output');
|
|
169
|
+
|
|
170
|
+
const untrackedPaths = out.split('\0').filter((p) => p !== '');
|
|
171
|
+
|
|
172
|
+
// Cost is linear in the untracked count: one lstat per path, plus one 4 KB
|
|
173
|
+
// read per path that passes the mtime filter (cheapest filter first). Fine
|
|
174
|
+
// for the measured 268; revisit if a close reports more than ~10k untracked
|
|
175
|
+
// files, where the per-file read would start to show in /close time.
|
|
176
|
+
const all = [];
|
|
177
|
+
for (const rel of untrackedPaths) {
|
|
178
|
+
const abs = path.join(root, rel);
|
|
179
|
+
let st;
|
|
180
|
+
try {
|
|
181
|
+
st = stat(abs);
|
|
182
|
+
} catch {
|
|
183
|
+
// Vanished between listing and stat (a concurrent writer moved it):
|
|
184
|
+
// nothing left to name.
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const mtimeMs = Number(st?.mtimeMs);
|
|
188
|
+
if (!Number.isFinite(mtimeMs) || mtimeMs < sinceMs) continue;
|
|
189
|
+
if (typeof st.isFile === 'function' && st.isFile() && isGeneratorOwned(abs)) continue;
|
|
190
|
+
all.push({ path: rel, mtimeMs });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
all.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
194
|
+
return {
|
|
195
|
+
status: 'ok',
|
|
196
|
+
candidates: all.slice(0, MAX_CANDIDATES),
|
|
197
|
+
total: all.length,
|
|
198
|
+
truncated: all.length > MAX_CANDIDATES,
|
|
199
|
+
untracked: untrackedPaths.length,
|
|
200
|
+
};
|
|
201
|
+
} catch (err) {
|
|
202
|
+
// Defensive catch-all - this probe must never throw into /close.
|
|
203
|
+
return unavailable('internal-error', String(err?.message ?? err).slice(0, 200));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -43,6 +43,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
43
43
|
import path from 'node:path';
|
|
44
44
|
|
|
45
45
|
import { eventsFilePath } from './events.mjs';
|
|
46
|
+
import { resolveEventsLedgerRoot } from './platform.mjs';
|
|
46
47
|
import { readCanonicalSessions } from './sessions-canonical.mjs';
|
|
47
48
|
import { readTailWindow } from './tail-window.mjs';
|
|
48
49
|
import { withStateMdLock } from './session-lock.mjs';
|
|
@@ -144,7 +145,11 @@ function isValidMode(mode) {
|
|
|
144
145
|
* only across DIFFERENT ids — never the same id twice, see above).
|
|
145
146
|
*/
|
|
146
147
|
async function readSessionIdsFromHistory(repoRoot) {
|
|
147
|
-
|
|
148
|
+
// #1592: the repo's ONE sessions ledger (main checkout of a linked worktree,
|
|
149
|
+
// where every close writes since #1584). Aggregation is a monotone max, so the
|
|
150
|
+
// wider ledger can only raise n — the events source does not always cover a
|
|
151
|
+
// closed session (rotated ledger, no lock event), the worktree read was empty.
|
|
152
|
+
const filePath = path.join(resolveEventsLedgerRoot(repoRoot), '.orchestrator', 'metrics', 'sessions.jsonl');
|
|
148
153
|
return readCanonicalSessions({ filePath }).map((rec) => rec.session_id);
|
|
149
154
|
}
|
|
150
155
|
|
|
@@ -266,6 +271,9 @@ async function readSessionIdsFromEvents(repoRoot) {
|
|
|
266
271
|
* @returns {Promise<string|null>} The session_id string, or null when absent.
|
|
267
272
|
*/
|
|
268
273
|
async function readSessionIdFromStateMd(repoRoot) {
|
|
274
|
+
// #1636 decision: deliberately the READ resolver, foreign STATE.md included —
|
|
275
|
+
// this source only collects ids already TAKEN, so a foreign one can only raise
|
|
276
|
+
// n (max over a union) and ignoring it could re-mint a peer's id.
|
|
269
277
|
const filePath = resolveStateMdPath(repoRoot);
|
|
270
278
|
let raw;
|
|
271
279
|
try {
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import { readFileSync } from 'node:fs';
|
|
30
|
+
import { execFileSync } from 'node:child_process';
|
|
31
|
+
import { homedir } from 'node:os';
|
|
30
32
|
import path from 'node:path';
|
|
31
33
|
import { isLockShape } from '../session-lock-shape.mjs';
|
|
32
34
|
|
|
@@ -228,6 +230,67 @@ export function readProcessLocalSessionIds({ env = process.env, hookInput = null
|
|
|
228
230
|
return ids;
|
|
229
231
|
}
|
|
230
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Read only process parent/name metadata, never command arguments or env.
|
|
235
|
+
* @param {number} pid
|
|
236
|
+
* @returns {{ ppid: number, comm: string } | null}
|
|
237
|
+
*/
|
|
238
|
+
function readProcessParent(pid) {
|
|
239
|
+
const output = execFileSync('ps', ['-p', String(pid), '-o', 'ppid=,comm='], {
|
|
240
|
+
encoding: 'utf8', timeout: 1000, maxBuffer: 2048,
|
|
241
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
242
|
+
});
|
|
243
|
+
const match = /^\s*(\d+)\s+(\S+)\s*$/.exec(output);
|
|
244
|
+
return match ? { ppid: Number(match[1]), comm: match[2] } : null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Prove an acknowledgement's own invocation using the nearest Claude process.
|
|
249
|
+
* Unlike the general identity union, inherited env and shared repo files are
|
|
250
|
+
* never proof here. Stop at the nearest Claude even when its session is unreadable.
|
|
251
|
+
* Missing, contradictory or unmeasurable evidence returns null; never throws.
|
|
252
|
+
* Reader injection permits deterministic fixtures when native ps is unavailable.
|
|
253
|
+
*
|
|
254
|
+
* @param {{ env?: object, pid?: number, homeDir?: string,
|
|
255
|
+
* readProcess?: (pid: number) => { ppid: number, comm: string } | null,
|
|
256
|
+
* readSession?: (pid: number) => unknown }} [opts]
|
|
257
|
+
* @returns {string|null}
|
|
258
|
+
*/
|
|
259
|
+
export function readInvocationSessionId({
|
|
260
|
+
env = process.env, pid = process.pid, homeDir = homedir(),
|
|
261
|
+
readProcess = readProcessParent,
|
|
262
|
+
readSession = (ancestorPid) => JSON.parse(readFileSync(
|
|
263
|
+
path.join(homeDir, '.claude', 'sessions', `${ancestorPid}.json`), 'utf8',
|
|
264
|
+
)),
|
|
265
|
+
} = {}) {
|
|
266
|
+
const seen = new Set();
|
|
267
|
+
try {
|
|
268
|
+
for (let depth = 0; depth < 64; depth++) {
|
|
269
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || seen.has(pid)) return null;
|
|
270
|
+
seen.add(pid);
|
|
271
|
+
const processInfo = readProcess(pid);
|
|
272
|
+
if (!processInfo || !Number.isSafeInteger(processInfo.ppid) || processInfo.ppid < 0 ||
|
|
273
|
+
seen.has(processInfo.ppid) || typeof processInfo.comm !== 'string' || !processInfo.comm.trim()) return null;
|
|
274
|
+
// macOS reports comm as argv[0]; a Claude started by absolute path must still count.
|
|
275
|
+
if (path.basename(processInfo.comm) === 'claude') {
|
|
276
|
+
const session = readSession(pid);
|
|
277
|
+
const sessionId = typeof session?.sessionId === 'string' ? session.sessionId.trim() : '';
|
|
278
|
+
if (!sessionId) return null;
|
|
279
|
+
// Every present native ID must agree; platform selection cannot hide
|
|
280
|
+
// a contradictory inherited ID from a nested harness.
|
|
281
|
+
for (const key of ['CLAUDE_CODE_SESSION_ID', 'CODEX_THREAD_ID']) {
|
|
282
|
+
const value = env?.[key];
|
|
283
|
+
if (value !== undefined && value !== null &&
|
|
284
|
+
(typeof value !== 'string' || (value.trim() && value.trim() !== sessionId))) return null;
|
|
285
|
+
}
|
|
286
|
+
return sessionId;
|
|
287
|
+
}
|
|
288
|
+
pid = processInfo.ppid;
|
|
289
|
+
}
|
|
290
|
+
} catch { /* Missing process/session evidence must fail closed. */ }
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
231
294
|
/**
|
|
232
295
|
* Decide whether a wave-scope manifest belongs to THIS session.
|
|
233
296
|
*
|
|
@@ -3,15 +3,26 @@
|
|
|
3
3
|
* This is a text boundary, not a shell/option parser: quotes and command syntax
|
|
4
4
|
* in the context remain data. `unknown` is a ledger value, never a live mode.
|
|
5
5
|
*
|
|
6
|
+
* A bare `/session` defaults to `deep`. In a repo without any session history
|
|
7
|
+
* of its own that default is not silent: the result carries
|
|
8
|
+
* `defaultReason: 'no-session-history'`, and the caller asks for task and mode
|
|
9
|
+
* first (#1561). `hasHistory` left undefined means "not measured" and keeps
|
|
10
|
+
* the plain default.
|
|
11
|
+
*
|
|
6
12
|
* @param {string} [argumentsText] Complete text following /session.
|
|
7
|
-
* @
|
|
13
|
+
* @param {{hasHistory?: boolean}} [options]
|
|
14
|
+
* @returns {{sessionType: string, context: string, profile?: string, invalidMode?: string, defaultReason?: string}}
|
|
8
15
|
*/
|
|
9
|
-
export function resolveSessionInvocation(argumentsText = '') {
|
|
16
|
+
export function resolveSessionInvocation(argumentsText = '', { hasHistory } = {}) {
|
|
10
17
|
if (typeof argumentsText !== 'string') {
|
|
11
18
|
throw new TypeError('session invocation arguments must be a string');
|
|
12
19
|
}
|
|
13
20
|
const [_, mode, context = ''] = argumentsText.trimStart().match(/^(\S+)(?:\s+([\s\S]*))?$/u) ?? [];
|
|
14
|
-
if (mode === undefined)
|
|
21
|
+
if (mode === undefined) {
|
|
22
|
+
return hasHistory === false
|
|
23
|
+
? { sessionType: 'deep', context: '', defaultReason: 'no-session-history' }
|
|
24
|
+
: { sessionType: 'deep', context: '' };
|
|
25
|
+
}
|
|
15
26
|
if (mode === 'ultradeep') return { sessionType: 'deep', profile: 'ultradeep', context };
|
|
16
27
|
if (['housekeeping', 'feature', 'deep'].includes(mode)) {
|
|
17
28
|
return { sessionType: mode, context };
|