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
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
* a size-ceiling check and a frontmatter-round-trip-safety check (both
|
|
86
86
|
* documented at frontmatter-mutators.mjs:307-328) — a breach of either
|
|
87
87
|
* REFUSES the write (leaves STATE.md untouched) and surfaces as
|
|
88
|
-
* `{ written: false, reason: 'size-ceiling' | 'frontmatter-unsafe' }`
|
|
88
|
+
* `{ written: false, reason: 'size-ceiling' | 'frontmatter-unsafe' | 'foreign-state-md' }`
|
|
89
89
|
* instead of silently bypassing the guard the way the old private
|
|
90
90
|
* `writeStateMdAtomic()` helper did.
|
|
91
91
|
*
|
|
@@ -119,12 +119,16 @@
|
|
|
119
119
|
* No I/O at import time. Node 20+ stdlib only.
|
|
120
120
|
*/
|
|
121
121
|
|
|
122
|
-
import { readFileSync } from 'node:fs';
|
|
122
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
123
123
|
import { execFileSync } from 'node:child_process';
|
|
124
|
+
import path from 'node:path';
|
|
124
125
|
|
|
125
|
-
import { parseStateMd, serializeStateMd,
|
|
126
|
+
import { parseStateMd, serializeStateMd, writeStateMd } from './state-md.mjs';
|
|
127
|
+
import { resolveStateMdWriteTarget } from './state-md/frontmatter-mutators.mjs';
|
|
126
128
|
import { pathMatchesPattern } from './scope-gate.mjs';
|
|
127
129
|
import { resolveBaselineRange, isQueryFailure } from './vcs-repo-spec.mjs';
|
|
130
|
+
import { readProcessLocalSessionIds } from './session-identity/own-session.mjs';
|
|
131
|
+
import { readSessionWorkTrees, sessionCommitFiles } from './session-work-trees.mjs';
|
|
128
132
|
|
|
129
133
|
// ---------------------------------------------------------------------------
|
|
130
134
|
// DRIFT_EXCLUDE_PATTERNS — the SINGLE filter source for both the denominator
|
|
@@ -271,6 +275,78 @@ function outputPaths(stdout) {
|
|
|
271
275
|
// throws like any other failure and lands in the same skip.
|
|
272
276
|
const DRIFT_GIT_TIMEOUT_MS = 10_000;
|
|
273
277
|
|
|
278
|
+
/** realpath, or the resolved spelling when the path cannot be resolved. */
|
|
279
|
+
function realOrResolved(p) {
|
|
280
|
+
try { return realpathSync(p); } catch { return path.resolve(p); }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Committed files from the session's OTHER own work trees (#1659).
|
|
285
|
+
*
|
|
286
|
+
* `session-start-ref` names one sha in the coordinator's own checkout; commits
|
|
287
|
+
* the same session made in its other worktrees are recorded as
|
|
288
|
+
* `orchestrator.session.work_tree` events (hooks/pre-bash-staging-fence.mjs).
|
|
289
|
+
* This reads them for the PROCESS-LOCAL raw session id
|
|
290
|
+
* (`readProcessLocalSessionIds`, never a repo-global file) and diffs each tree
|
|
291
|
+
* inside itself (`sessionCommitFiles`) — never across trees.
|
|
292
|
+
*
|
|
293
|
+
* Excluded before diffing: the own tree (realpath-equal to `ownTop`; the
|
|
294
|
+
* STATE.md ref stays authoritative there) and trees of a DIFFERENT repository
|
|
295
|
+
* (other `--git-common-dir`; their paths live in another path space). A tree
|
|
296
|
+
* whose common dir cannot be queried is passed through, so
|
|
297
|
+
* `sessionCommitFiles` names why it is unusable (`tree-missing`, …).
|
|
298
|
+
*
|
|
299
|
+
* Never throws: any failure yields `null` (no other-tree data), so the
|
|
300
|
+
* existing own-tree measurement is never turned into a skip by this source.
|
|
301
|
+
* That includes the own-tree identity queries (`--show-toplevel`,
|
|
302
|
+
* `--path-format=absolute` needs git >= 2.31): they run HERE, only once a
|
|
303
|
+
* session id exists, never inside computeDrift()'s numerator `try`.
|
|
304
|
+
*
|
|
305
|
+
* @param {{ repoRoot: string|undefined, gitOpts: object, env: object,
|
|
306
|
+
* eventsFile?: string }} args
|
|
307
|
+
* @returns {{ files: string[], counted: number, skipped: number, otherRepo: number,
|
|
308
|
+
* malformed: number, invalid: number } | null}
|
|
309
|
+
* `null` when no other own tree is recorded. `malformed` / `invalid` carry
|
|
310
|
+
* the reader's unreadable lines and own events without a usable tree/ref.
|
|
311
|
+
*/
|
|
312
|
+
function otherWorkTreeFiles({ repoRoot, gitOpts, env, eventsFile }) {
|
|
313
|
+
try {
|
|
314
|
+
const [sessionId] = readProcessLocalSessionIds({ env });
|
|
315
|
+
if (!sessionId) return null;
|
|
316
|
+
const read = readSessionWorkTrees({ sessionId, repoRoot, eventsFile });
|
|
317
|
+
if (read.trees.length === 0) return null;
|
|
318
|
+
const ownTop = realOrResolved(execFileSync('git', ['rev-parse', '--show-toplevel'], gitOpts).trim());
|
|
319
|
+
const ownCommonDir = realOrResolved(
|
|
320
|
+
execFileSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], gitOpts).trim(),
|
|
321
|
+
);
|
|
322
|
+
const others = read.trees.filter((t) => realOrResolved(t.tree) !== ownTop);
|
|
323
|
+
if (others.length === 0) return null;
|
|
324
|
+
const sameRepo = [];
|
|
325
|
+
let otherRepo = 0;
|
|
326
|
+
for (const t of others) {
|
|
327
|
+
let common = null;
|
|
328
|
+
try {
|
|
329
|
+
common = execFileSync('git', ['-C', t.tree, 'rev-parse', '--path-format=absolute', '--git-common-dir'], {
|
|
330
|
+
encoding: 'utf8', timeout: DRIFT_GIT_TIMEOUT_MS, stdio: ['ignore', 'pipe', 'ignore'],
|
|
331
|
+
}).trim();
|
|
332
|
+
} catch { /* unusable tree: sessionCommitFiles reports the reason */ }
|
|
333
|
+
if (common !== null && realOrResolved(common) !== ownCommonDir) { otherRepo += 1; continue; }
|
|
334
|
+
sameRepo.push(t);
|
|
335
|
+
}
|
|
336
|
+
const res = sessionCommitFiles({ trees: sameRepo });
|
|
337
|
+
return {
|
|
338
|
+
files: res.union,
|
|
339
|
+
counted: res.perTree.length,
|
|
340
|
+
skipped: res.skipped.length,
|
|
341
|
+
otherRepo,
|
|
342
|
+
malformed: read.malformed,
|
|
343
|
+
invalid: read.invalid,
|
|
344
|
+
};
|
|
345
|
+
} catch {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
274
350
|
/**
|
|
275
351
|
* Internal: read + parse STATE.md's frontmatter exactly once. Shared by
|
|
276
352
|
* `readBaseline()` and `computeDrift()` so a single `computeDrift()` call
|
|
@@ -280,7 +356,12 @@ const DRIFT_GIT_TIMEOUT_MS = 10_000;
|
|
|
280
356
|
* @returns {{ ok: true, fm: object } | { ok: false, reason: 'no-state-md'|'unreadable-state-md' }}
|
|
281
357
|
*/
|
|
282
358
|
function readFrontmatterOrReason(repoRoot) {
|
|
283
|
-
|
|
359
|
+
// #1636: the write side's ownership rule — another platform's STATE.md not
|
|
360
|
+
// proven to be this process's would lend its baseline and session-start-ref
|
|
361
|
+
// to THIS session's drift tripwire. It counts as absent (`no-state-md`,
|
|
362
|
+
// silent like every skip here; writeBaseline() already refuses it).
|
|
363
|
+
const { path: statePath, rejected } = resolveStateMdWriteTarget(repoRoot);
|
|
364
|
+
if (rejected) return { ok: false, reason: 'no-state-md' };
|
|
284
365
|
let raw;
|
|
285
366
|
try {
|
|
286
367
|
raw = readFileSync(statePath, 'utf8');
|
|
@@ -418,7 +499,7 @@ export function readBaseline(repoRoot) {
|
|
|
418
499
|
* `filterExcluded()` pass at all). Anything that is not an array is
|
|
419
500
|
* rejected UP FRONT — before STATE.md is read or the lock is taken —
|
|
420
501
|
* as `{ written: false, reason: 'invalid-planned-files' }`.
|
|
421
|
-
* @returns {Promise<{ written: boolean, reason?: 'invalid-planned-files'|'already-frozen'|'no-state-md'|'unreadable-state-md'|'lock-timeout'|'lock-fs-error'|'unexpected-error'|'size-ceiling'|'frontmatter-unsafe' }>}
|
|
502
|
+
* @returns {Promise<{ written: boolean, reason?: 'invalid-planned-files'|'already-frozen'|'no-state-md'|'unreadable-state-md'|'lock-timeout'|'lock-fs-error'|'unexpected-error'|'size-ceiling'|'frontmatter-unsafe'|'foreign-state-md' }>}
|
|
422
503
|
*/
|
|
423
504
|
export async function writeBaseline({ repoRoot, intent, ownerBoundary, plannedFiles } = {}) {
|
|
424
505
|
if (!Array.isArray(plannedFiles)) {
|
|
@@ -488,7 +569,8 @@ export async function writeBaseline({ repoRoot, intent, ownerBoundary, plannedFi
|
|
|
488
569
|
// frontmatter-unsafe breach) — surface that reason rather than falling
|
|
489
570
|
// back to the transformer's closure-captured reason, which was never
|
|
490
571
|
// set on this path.
|
|
491
|
-
|
|
572
|
+
// `foreign-state-md` (#1640): the target belongs to another session.
|
|
573
|
+
if (result.reason === 'size-ceiling' || result.reason === 'frontmatter-unsafe' || result.reason === 'foreign-state-md') {
|
|
492
574
|
return { written: false, reason: result.reason };
|
|
493
575
|
}
|
|
494
576
|
return { written: false, reason: skipReason ?? 'unreadable-state-md' };
|
|
@@ -600,14 +682,29 @@ export async function writeBaseline({ repoRoot, intent, ownerBoundary, plannedFi
|
|
|
600
682
|
* @param {string|undefined} args.repoRoot
|
|
601
683
|
* @param {number} [args.threshold] — breach threshold, `>=` counts as
|
|
602
684
|
* breached (default `2.0`).
|
|
685
|
+
* @param {object} [args.env=process.env] — source of the PROCESS-LOCAL raw
|
|
686
|
+
* session id whose other work trees are added (#1659); injectable for tests.
|
|
687
|
+
* @param {string} [args.eventsFile] — explicit events ledger for the
|
|
688
|
+
* work-tree records (default: the repoRoot's ledger).
|
|
603
689
|
* @returns {{ ok: true, skipped: true, reason: 'no-state-md'|'unreadable-state-md'|'no-baseline'|'stale-baseline'|'no-baseline-ref'|'unresolvable-ref' }
|
|
604
690
|
* | { ok: true, skipped: false, filesRatio: number, plannedFiles: number,
|
|
605
|
-
* actualFiles: number, breached: boolean, threshold: number, refUsed: string
|
|
691
|
+
* actualFiles: number, breached: boolean, threshold: number, refUsed: string,
|
|
692
|
+
* otherWorkTrees?: { counted: number, skipped: number, otherRepo: number,
|
|
693
|
+
* malformed: number, invalid: number } }}
|
|
606
694
|
* Additive since #1039: `no-baseline-ref` is a NEW member of the skip-reason
|
|
607
695
|
* union. No existing member was renamed or removed, and the measured-result
|
|
608
696
|
* shape (all eight keys) is byte-identical to the pre-#1039 contract.
|
|
697
|
+
* Additive since #1659: the committed source also unions the files the
|
|
698
|
+
* session (process-local raw id) committed in its OTHER own work trees of
|
|
699
|
+
* the same repository, each diffed inside itself against its own recorded
|
|
700
|
+
* start ref (`orchestrator.session.work_tree`). `otherWorkTrees` appears
|
|
701
|
+
* only when such a tree is recorded: `counted` trees measured, `skipped`
|
|
702
|
+
* trees unusable (gone, ref unresolvable — they never fail the drift),
|
|
703
|
+
* `otherRepo` trees of a different repository (not comparable, ignored),
|
|
704
|
+
* `malformed` unreadable ledger lines and `invalid` own events without a
|
|
705
|
+
* usable tree/start_ref (a partial read, never a clean verdict).
|
|
609
706
|
*/
|
|
610
|
-
export function computeDrift({ repoRoot, threshold = 2.0 } = {}) {
|
|
707
|
+
export function computeDrift({ repoRoot, threshold = 2.0, env = process.env, eventsFile } = {}) {
|
|
611
708
|
const parsedFm = readFrontmatterOrReason(repoRoot);
|
|
612
709
|
if (!parsedFm.ok) {
|
|
613
710
|
return { ok: true, skipped: true, reason: parsedFm.reason };
|
|
@@ -683,6 +780,11 @@ export function computeDrift({ repoRoot, threshold = 2.0 } = {}) {
|
|
|
683
780
|
return { ok: true, skipped: true, reason: 'unresolvable-ref' };
|
|
684
781
|
}
|
|
685
782
|
|
|
783
|
+
// #1659: add what the session committed in its OTHER own work trees. The
|
|
784
|
+
// own tree is excluded inside (its STATE.md ref above stays authoritative).
|
|
785
|
+
const otherTrees = otherWorkTreeFiles({ repoRoot, gitOpts, env, eventsFile });
|
|
786
|
+
if (otherTrees !== null) for (const file of otherTrees.files) changedFiles.add(file);
|
|
787
|
+
|
|
686
788
|
// Same `filterExcluded()` primitive `writeBaseline()`'s denominator
|
|
687
789
|
// (`countPlannedFiles()`) calls (#894 review finding F1), so the pattern
|
|
688
790
|
// filter on both sides of the ratio is one function. The untracked
|
|
@@ -710,5 +812,18 @@ export function computeDrift({ repoRoot, threshold = 2.0 } = {}) {
|
|
|
710
812
|
breached,
|
|
711
813
|
threshold,
|
|
712
814
|
refUsed,
|
|
815
|
+
// Additive (#1659), present ONLY when another own work tree is recorded,
|
|
816
|
+
// so the eight-key shape above is unchanged for single-tree sessions.
|
|
817
|
+
...(otherTrees !== null
|
|
818
|
+
? {
|
|
819
|
+
otherWorkTrees: {
|
|
820
|
+
counted: otherTrees.counted,
|
|
821
|
+
skipped: otherTrees.skipped,
|
|
822
|
+
otherRepo: otherTrees.otherRepo,
|
|
823
|
+
malformed: otherTrees.malformed,
|
|
824
|
+
invalid: otherTrees.invalid,
|
|
825
|
+
},
|
|
826
|
+
}
|
|
827
|
+
: {}),
|
|
713
828
|
};
|
|
714
829
|
}
|
|
@@ -52,6 +52,7 @@ import { join, resolve } from 'node:path';
|
|
|
52
52
|
import { digestSha256Short } from './crypto-digest-utils.mjs';
|
|
53
53
|
import { isMainModule } from './is-main-module.mjs';
|
|
54
54
|
import { isCoordinatorRecordId } from './scope-gate.mjs';
|
|
55
|
+
import { resolveEventsLedgerRoot } from './platform.mjs';
|
|
55
56
|
|
|
56
57
|
/** Marker the agent must emit. Case-sensitive by design — a lowercase lookalike is not an echo. */
|
|
57
58
|
export const SCOPE_ECHO_MARKER = 'SCOPE-DIGEST:';
|
|
@@ -420,7 +421,8 @@ export function digestScopeFiles(stateDir, wave, { readDir = readdirSync, readFi
|
|
|
420
421
|
* @param {number} params.wave
|
|
421
422
|
* @param {string} params.stateDir
|
|
422
423
|
* @param {string} [params.session] semantic (or raw) session id to filter on
|
|
423
|
-
* @param {string} [params.eventsPath] defaults to
|
|
424
|
+
* @param {string} [params.eventsPath] defaults to `.orchestrator/metrics/events.jsonl` under
|
|
425
|
+
* `resolveEventsLedgerRoot(cwd)`;
|
|
424
426
|
* the CLI passes `eventsFilePath(<state-dir>/..)` (the repo's one ledger, #1514)
|
|
425
427
|
* @param {typeof readFileSync} [params.readFile]
|
|
426
428
|
* @param {typeof readdirSync} [params.readDir]
|
|
@@ -437,7 +439,8 @@ export function verifyWaveScope({
|
|
|
437
439
|
readFile = readFileSync,
|
|
438
440
|
readDir = readdirSync,
|
|
439
441
|
}) {
|
|
440
|
-
|
|
442
|
+
// #1514/#1722: the default maps a linked worktree to the main checkout's one ledger.
|
|
443
|
+
const ledgerPath = eventsPath || join(resolveEventsLedgerRoot(process.cwd()), '.orchestrator', 'metrics', 'events.jsonl');
|
|
441
444
|
let records = [];
|
|
442
445
|
let malformedLines = 0;
|
|
443
446
|
try {
|
|
@@ -704,10 +704,12 @@ export function assertTestSiblingCoverage(fileScope, allowedPaths, opts = {}) {
|
|
|
704
704
|
* module importing the barrel WOULD cycle).
|
|
705
705
|
*
|
|
706
706
|
* Two deltas the shared token stream does not carry, handled locally below:
|
|
707
|
-
* - `(`/`)`: `tokenizeCommand`
|
|
708
|
-
*
|
|
709
|
-
*
|
|
710
|
-
*
|
|
707
|
+
* - `(`/`)`: since #1667 `tokenizeCommand` already emits a subshell's parens
|
|
708
|
+
* as `;` separators and hoists `$( … )` / backtick bodies as their own
|
|
709
|
+
* commands. What still reaches this pass glued is word text the lexer keeps
|
|
710
|
+
* on purpose — a process substitution word (`>(cat)`), `f()`, `arr=(…)`,
|
|
711
|
+
* case patterns. {@link peelSubshellParens} splits those so `> >(cat)`
|
|
712
|
+
* still yields nothing (the peeled `cat` lands in command-head position).
|
|
711
713
|
* - here-doc BODY tokens arrive as ordinary quoted words. They are DATA, and a
|
|
712
714
|
* body is multi-line, so the whitespace skip rule in
|
|
713
715
|
* {@link shouldSkipWriteTarget} drops them. (The delimiter word itself now
|
|
@@ -906,13 +908,13 @@ const INPUT_REDIRECT_OP_RE = /^\d*(?:<<<|<)$/;
|
|
|
906
908
|
* Split leading `(` / trailing `)` off an UNQUOTED word token into standalone
|
|
907
909
|
* separator tokens.
|
|
908
910
|
*
|
|
909
|
-
* `tokenizeCommand`
|
|
910
|
-
*
|
|
911
|
-
*
|
|
912
|
-
*
|
|
913
|
-
*
|
|
914
|
-
* `> >(cat)` still yields nothing because the peeled `cat` lands in
|
|
915
|
-
* position rather than redirect-target position.
|
|
911
|
+
* Since #1667 `tokenizeCommand` splits a word-start subshell `(` / `)` itself,
|
|
912
|
+
* so `(echo x > y)` arrives as `;` `echo` `x` `>` `y` `;`. What still arrives
|
|
913
|
+
* glued is paren text the lexer deliberately keeps inside a word — a process
|
|
914
|
+
* substitution word (`>(cat)`), `f()`, `arr=(…)`, case patterns, and openers
|
|
915
|
+
* past its nesting cap. Peeling keeps those from reading as a literal `y)`
|
|
916
|
+
* target, and `> >(cat)` still yields nothing because the peeled `cat` lands in
|
|
917
|
+
* command-head position rather than redirect-target position.
|
|
916
918
|
*
|
|
917
919
|
* Quoted tokens are never peeled: `echo '(' x` must keep its literal paren.
|
|
918
920
|
*
|
|
@@ -2021,6 +2023,11 @@ const DENIED_ABSOLUTE_TOP_SEGMENTS = Object.freeze([
|
|
|
2021
2023
|
'Library',
|
|
2022
2024
|
'Applications',
|
|
2023
2025
|
'Windows',
|
|
2026
|
+
// Windows machine-wide program and data roots (#1612, Opus review M3): the
|
|
2027
|
+
// drive-less spelling of C:/ProgramData/** and C:/Program Files/** lands here.
|
|
2028
|
+
'ProgramData',
|
|
2029
|
+
'Program Files',
|
|
2030
|
+
'Program Files (x86)',
|
|
2024
2031
|
]);
|
|
2025
2032
|
|
|
2026
2033
|
/**
|
|
@@ -2151,7 +2158,10 @@ const CACHE_CARVEOUT_MIN_LITERAL_SEGMENTS = 4;
|
|
|
2151
2158
|
* @returns {boolean}
|
|
2152
2159
|
*/
|
|
2153
2160
|
function isSensitiveHomeSegment(segment) {
|
|
2154
|
-
|
|
2161
|
+
// AppData is Windows' counterpart of ~/Library: gh/glab tokens, browser
|
|
2162
|
+
// profiles, credential caches (#1612, Opus review M3).
|
|
2163
|
+
const folded = foldPath(segment);
|
|
2164
|
+
return segment.startsWith('.') || folded === 'library' || folded === 'appdata';
|
|
2155
2165
|
}
|
|
2156
2166
|
|
|
2157
2167
|
/**
|
|
@@ -2267,14 +2277,24 @@ function classifyHomeGrant(normalizedEntry) {
|
|
|
2267
2277
|
const cacheCarveout =
|
|
2268
2278
|
foldPath(firstBelowHome) === CACHE_CARVEOUT_SEGMENT &&
|
|
2269
2279
|
literal.length >= CACHE_CARVEOUT_MIN_LITERAL_SEGMENTS;
|
|
2270
|
-
|
|
2280
|
+
// #1612 — Windows' os.tmpdir() is ~/AppData/Local/Temp, where worktrees and
|
|
2281
|
+
// fixtures live. Same DEPTH rule as ~/.cache: the Temp root stays refused,
|
|
2282
|
+
// a named directory below it is honoured.
|
|
2283
|
+
const below = HOME_MIN_LITERAL_SEGMENTS - 1;
|
|
2284
|
+
const windowsTempCarveout =
|
|
2285
|
+
foldPath(firstBelowHome) === 'appdata' &&
|
|
2286
|
+
literal.length >= below + 4 &&
|
|
2287
|
+
foldPath(literal[below + 1]) === 'local' &&
|
|
2288
|
+
foldPath(literal[below + 2]) === 'temp';
|
|
2289
|
+
if (!cacheCarveout && !windowsTempCarveout && isSensitiveHomeSegment(firstBelowHome)) {
|
|
2271
2290
|
return {
|
|
2272
2291
|
verdict: 'error',
|
|
2273
2292
|
reason:
|
|
2274
|
-
`grants a sensitive home subdirectory ("${firstBelowHome}") — dot-directories and
|
|
2293
|
+
`grants a sensitive home subdirectory ("${firstBelowHome}") — dot-directories, Library and AppData ` +
|
|
2275
2294
|
`hold credentials, tokens, owner.yaml and the agent guards themselves; scope a project ` +
|
|
2276
|
-
`directory instead (the
|
|
2277
|
-
`${CACHE_CARVEOUT_MIN_LITERAL_SEGMENTS} literal segments — #1406
|
|
2295
|
+
`directory instead (the exceptions are ~/.cache/<project>/…, which must name at least ` +
|
|
2296
|
+
`${CACHE_CARVEOUT_MIN_LITERAL_SEGMENTS} literal segments — #1406, and a named directory ` +
|
|
2297
|
+
`below Windows' temp root ~/AppData/Local/Temp — #1612)`,
|
|
2278
2298
|
};
|
|
2279
2299
|
}
|
|
2280
2300
|
|
|
@@ -2396,6 +2416,13 @@ export function canonicalizeGrantPrefix(absPath) {
|
|
|
2396
2416
|
*/
|
|
2397
2417
|
export function gradeScopeEntry(entry, opts = {}) {
|
|
2398
2418
|
if (typeof entry !== 'string' || entry.length === 0) return null;
|
|
2419
|
+
// Windows drive-absolute grants (#1612): every check below speaks POSIX
|
|
2420
|
+
// separators and Gate 5b matches the realpath candidate in its forward-slash
|
|
2421
|
+
// form ("C:/Users/..."). Graded verbatim, `C:\a\b\**` had no "/" prefix at
|
|
2422
|
+
// all ("/" resolves to "C:/") and a clean grant was reported non-canonical,
|
|
2423
|
+
// so scripts/validate-wave-scope.mjs refused it (measured on Windows 11).
|
|
2424
|
+
const windowsDrive = /^[A-Za-z]:[\\/]/.test(entry);
|
|
2425
|
+
if (windowsDrive) entry = entry.replace(/\\/g, '/');
|
|
2399
2426
|
const bag = opts !== null && typeof opts === 'object' ? opts : {};
|
|
2400
2427
|
const resolver = typeof bag.resolve === 'function' ? bag.resolve : null;
|
|
2401
2428
|
/** @param {string} p @returns {string} */
|
|
@@ -2431,7 +2458,7 @@ export function gradeScopeEntry(entry, opts = {}) {
|
|
|
2431
2458
|
};
|
|
2432
2459
|
}
|
|
2433
2460
|
|
|
2434
|
-
if (!path.isAbsolute(entry)) return null;
|
|
2461
|
+
if (!windowsDrive && !path.isAbsolute(entry)) return null;
|
|
2435
2462
|
|
|
2436
2463
|
// `.` segments are collapsed BEFORE any classification (#1398): without it,
|
|
2437
2464
|
// `/Users/./alice/.ssh/**` has a literal prefix whose third element is
|
|
@@ -2440,8 +2467,12 @@ export function gradeScopeEntry(entry, opts = {}) {
|
|
|
2440
2467
|
// any entry containing `../`, whatever else it is.
|
|
2441
2468
|
const normalized = path.posix.normalize(entry);
|
|
2442
2469
|
const literalPrefix = literalGrantPrefix(normalized);
|
|
2470
|
+
// The system-directory and home-root rules are written for POSIX roots; a
|
|
2471
|
+
// drive-less spelling makes `C:/Users/<name>/**` as strict as `/Users/<name>/**`
|
|
2472
|
+
// (before #1612 a Windows home grant was never classified at all).
|
|
2473
|
+
const rootless = windowsDrive ? normalized.replace(/^[A-Za-z]:/, '') : normalized;
|
|
2443
2474
|
|
|
2444
|
-
const deniedLiteral = deniedRootFor(
|
|
2475
|
+
const deniedLiteral = deniedRootFor(literalGrantPrefix(rootless), (p) => p);
|
|
2445
2476
|
if (deniedLiteral !== null) {
|
|
2446
2477
|
return {
|
|
2447
2478
|
verdict: 'error',
|
|
@@ -2452,7 +2483,7 @@ export function gradeScopeEntry(entry, opts = {}) {
|
|
|
2452
2483
|
};
|
|
2453
2484
|
}
|
|
2454
2485
|
|
|
2455
|
-
const home = classifyHomeGrant(
|
|
2486
|
+
const home = classifyHomeGrant(rootless);
|
|
2456
2487
|
if (home !== null) {
|
|
2457
2488
|
return home.verdict === 'error'
|
|
2458
2489
|
? { verdict: 'error', code: 'home-grant', message: `${home.reason}: ${entry}` }
|
|
@@ -2481,7 +2512,11 @@ export function gradeScopeEntry(entry, opts = {}) {
|
|
|
2481
2512
|
};
|
|
2482
2513
|
}
|
|
2483
2514
|
|
|
2484
|
-
|
|
2515
|
+
// Windows: realpath answers in forward slashes with an upper-case drive letter.
|
|
2516
|
+
const comparable = (p) => (windowsDrive
|
|
2517
|
+
? p.replace(/\\/g, '/').replace(/^([a-z]):/, (_, d) => `${d.toUpperCase()}:`)
|
|
2518
|
+
: p);
|
|
2519
|
+
if (comparable(canonicalPrefix) !== comparable(literalPrefix)) {
|
|
2485
2520
|
return {
|
|
2486
2521
|
verdict: 'error',
|
|
2487
2522
|
code: 'non-canonical',
|
|
@@ -64,6 +64,7 @@ import { serializeSessionLineChecked as defaultSerialize } from './session-schem
|
|
|
64
64
|
import { resolveStateMdPath as defaultResolveStateMdPath } from './state-md/frontmatter-mutators.mjs';
|
|
65
65
|
import { parseStateMd as defaultParseStateMd } from './state-md/yaml-parser.mjs';
|
|
66
66
|
import { canonicalizeSessions } from './sessions-canonical.mjs';
|
|
67
|
+
import { explainEventsLedgerRoot, resolveEventsLedgerRoot } from './platform.mjs';
|
|
67
68
|
// Already in the hook import set (`emitEvent` lives there): the rotation-aware
|
|
68
69
|
// reader adds no new file to the SessionStart/SessionEnd hook graph.
|
|
69
70
|
import { eventsFilePath, scanEventsBackwards } from './events.mjs'; // eventsFilePath: #1514, the repo's one ledger
|
|
@@ -137,6 +138,25 @@ export function isLoggedBackfillAction(action) {
|
|
|
137
138
|
// Helpers
|
|
138
139
|
// ---------------------------------------------------------------------------
|
|
139
140
|
|
|
141
|
+
/**
|
|
142
|
+
* A skip outcome when `repoRoot` sits in a LINKED worktree with no main
|
|
143
|
+
* checkout to map the ledger to (`git init --separate-git-dir`, a bare repo's
|
|
144
|
+
* worktree, stale worktree metadata): the resolver falls back to the worktree
|
|
145
|
+
* itself, and this writer would append into ITS `sessions.jsonl` again — the
|
|
146
|
+
* #1660 incident class. Says so on stderr in one line (#1666). `undefined`
|
|
147
|
+
* otherwise: write as before. The skip is a `skipped-*` action, so the
|
|
148
|
+
* breadcrumb log stays out of the worktree too.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} repoRoot
|
|
151
|
+
* @returns {{ action: string, reason: string } | undefined}
|
|
152
|
+
*/
|
|
153
|
+
function unresolvedLedgerSkip(repoRoot) {
|
|
154
|
+
const { resolved, reason } = explainEventsLedgerRoot(repoRoot);
|
|
155
|
+
if (resolved) return undefined;
|
|
156
|
+
process.stderr.write(`session-close-backfill: skipped, sessions ledger not written into the worktree: ${reason}\n`);
|
|
157
|
+
return { action: 'skipped-ledger-root-unresolved', reason };
|
|
158
|
+
}
|
|
159
|
+
|
|
140
160
|
/**
|
|
141
161
|
* True when `s` is an RFC 9562 UUID session id (any version 1-8, variant `10xx`).
|
|
142
162
|
*
|
|
@@ -161,6 +181,19 @@ function markerName(id) {
|
|
|
161
181
|
return `.backfilled-${cleaned}.marker`;
|
|
162
182
|
}
|
|
163
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Give a TOCTOU marker back after the ledger append failed (#1609): a marker
|
|
186
|
+
* without its record would keep the candidate from ever being backfilled.
|
|
187
|
+
* Best-effort — a failed unlink leaves the old behaviour, never throws.
|
|
188
|
+
*/
|
|
189
|
+
function releaseMarker(markerPath) {
|
|
190
|
+
try {
|
|
191
|
+
fs.unlinkSync(markerPath);
|
|
192
|
+
} catch {
|
|
193
|
+
/* best-effort */
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
164
197
|
/**
|
|
165
198
|
* Read a JSONL file into an array of parsed objects. Missing file (ENOENT) →
|
|
166
199
|
* [] silently; an unreadable one (EACCES/EISDIR/…) → [] with a stderr WARN
|
|
@@ -971,9 +1004,11 @@ export async function backfillAbandonedSession({
|
|
|
971
1004
|
if (!sessionId && !semanticSessionId) {
|
|
972
1005
|
return { action: 'skipped-no-identifier' };
|
|
973
1006
|
}
|
|
1007
|
+
const unresolved = unresolvedLedgerSkip(repoRoot);
|
|
1008
|
+
if (unresolved) return unresolved;
|
|
974
1009
|
|
|
975
1010
|
const eventsPath = eventsFilePath(repoRoot);
|
|
976
|
-
const sessionsPath = path.join(repoRoot, ...SESSIONS_REL);
|
|
1011
|
+
const sessionsPath = path.join(resolveEventsLedgerRoot(repoRoot), ...SESSIONS_REL);
|
|
977
1012
|
|
|
978
1013
|
// -- Resolve the SEMANTIC record id (sessions.jsonl key) ----------------
|
|
979
1014
|
// Two-phase so the common SessionEnd path stays cheap: when the id is
|
|
@@ -1145,8 +1180,29 @@ export async function backfillAbandonedSession({
|
|
|
1145
1180
|
};
|
|
1146
1181
|
}
|
|
1147
1182
|
|
|
1148
|
-
// -- TOCTOU marker — atomic create-or-fail keyed by the
|
|
1149
|
-
|
|
1183
|
+
// -- TOCTOU marker — atomic create-or-fail keyed by the CANDIDATE -------
|
|
1184
|
+
// #1592: the marker guards the ledger it sits beside — the MAIN
|
|
1185
|
+
// checkout's, shared by every worktree. A linked worktree has no
|
|
1186
|
+
// `.orchestrator/metrics/` since #1584, hence the mkdir.
|
|
1187
|
+
// #1609: keyed by the raw UUID when known, never by `recordId`: which id a
|
|
1188
|
+
// run records under (semantic, or the UUID when the semantic key is
|
|
1189
|
+
// occupied) depends on the ledger it read, so two racing runs of one
|
|
1190
|
+
// candidate claimed different markers and both appended. A legacy marker
|
|
1191
|
+
// (keyed by the record id) still counts as a lost claim, but only without a
|
|
1192
|
+
// key switch: once the semantic key is occupied and we record under our own
|
|
1193
|
+
// UUID, the semantic marker belongs to the session occupying that key.
|
|
1194
|
+
const markerDir = path.dirname(sessionsPath);
|
|
1195
|
+
const markerKey = isUuid(sessionId) ? sessionId : semanticRecordId;
|
|
1196
|
+
const markerPath = path.join(markerDir, markerName(markerKey));
|
|
1197
|
+
try {
|
|
1198
|
+
fs.mkdirSync(markerDir, { recursive: true });
|
|
1199
|
+
} catch {
|
|
1200
|
+
/* the openSync below reports the real failure */
|
|
1201
|
+
}
|
|
1202
|
+
if (recordId === semanticRecordId && recordId !== markerKey
|
|
1203
|
+
&& fs.existsSync(path.join(markerDir, markerName(recordId)))) {
|
|
1204
|
+
return { action: 'skipped-marker-exists', sessionId: recordId };
|
|
1205
|
+
}
|
|
1150
1206
|
try {
|
|
1151
1207
|
const fd = openSync(markerPath, 'wx');
|
|
1152
1208
|
closeSync(fd);
|
|
@@ -1162,6 +1218,7 @@ export async function backfillAbandonedSession({
|
|
|
1162
1218
|
try {
|
|
1163
1219
|
await appendJsonl(sessionsPath, validated);
|
|
1164
1220
|
} catch (err) {
|
|
1221
|
+
releaseMarker(markerPath);
|
|
1165
1222
|
return { action: 'error', error: `append: ${err?.message ?? String(err)}`, sessionId: recordId };
|
|
1166
1223
|
}
|
|
1167
1224
|
return {
|
|
@@ -1185,7 +1242,8 @@ export async function backfillAbandonedSession({
|
|
|
1185
1242
|
}
|
|
1186
1243
|
if (typeof repoRoot !== 'string' || repoRoot.length === 0) return;
|
|
1187
1244
|
if (!isLoggedBackfillAction(res.action)) return;
|
|
1188
|
-
|
|
1245
|
+
// #1592: one breadcrumb log beside the one ledger, not one per worktree.
|
|
1246
|
+
const logPath = path.join(resolveEventsLedgerRoot(repoRoot), ...BACKFILL_LOG_REL);
|
|
1189
1247
|
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
1190
1248
|
fs.appendFileSync(
|
|
1191
1249
|
logPath,
|
|
@@ -1298,6 +1356,8 @@ export async function backfillCompletedFromStateMd({
|
|
|
1298
1356
|
if (typeof repoRoot !== 'string' || repoRoot.length === 0) {
|
|
1299
1357
|
return { action: 'error', error: 'repoRoot must be a non-empty string' };
|
|
1300
1358
|
}
|
|
1359
|
+
const unresolved = unresolvedLedgerSkip(repoRoot);
|
|
1360
|
+
if (unresolved) return unresolved;
|
|
1301
1361
|
|
|
1302
1362
|
// -- Read + parse STATE.md ------------------------------------------------
|
|
1303
1363
|
const stateMdPath = resolveStateMdPath(repoRoot);
|
|
@@ -1334,7 +1394,7 @@ export async function backfillCompletedFromStateMd({
|
|
|
1334
1394
|
// record (carrying `supersedes: <stub id>`) instead of skipping. An
|
|
1335
1395
|
// authoritative record already on file still short-circuits exactly as
|
|
1336
1396
|
// before.
|
|
1337
|
-
const sessionsPath = path.join(repoRoot, ...SESSIONS_REL);
|
|
1397
|
+
const sessionsPath = path.join(resolveEventsLedgerRoot(repoRoot), ...SESSIONS_REL);
|
|
1338
1398
|
const existing = classifyExisting(readFileSync, sessionsPath, {
|
|
1339
1399
|
recordId,
|
|
1340
1400
|
sessionId: nativeId,
|
|
@@ -1407,7 +1467,13 @@ export async function backfillCompletedFromStateMd({
|
|
|
1407
1467
|
|
|
1408
1468
|
// -- TOCTOU marker — atomic create-or-fail, own namespace so a concurrent
|
|
1409
1469
|
// abandoned-path claim for the same id can never collide with this one. --
|
|
1410
|
-
|
|
1470
|
+
// #1592: beside the shared ledger it guards (see backfillAbandonedSession).
|
|
1471
|
+
const markerPath = path.join(path.dirname(sessionsPath), markerName(`completed-${recordId}`));
|
|
1472
|
+
try {
|
|
1473
|
+
fs.mkdirSync(path.dirname(markerPath), { recursive: true });
|
|
1474
|
+
} catch {
|
|
1475
|
+
/* the openSync below reports the real failure */
|
|
1476
|
+
}
|
|
1411
1477
|
try {
|
|
1412
1478
|
const fd = openSync(markerPath, 'wx');
|
|
1413
1479
|
closeSync(fd);
|
|
@@ -1422,6 +1488,7 @@ export async function backfillCompletedFromStateMd({
|
|
|
1422
1488
|
try {
|
|
1423
1489
|
await appendJsonl(sessionsPath, validated);
|
|
1424
1490
|
} catch (err) {
|
|
1491
|
+
releaseMarker(markerPath);
|
|
1425
1492
|
return { action: 'error', error: `append: ${err?.message ?? String(err)}`, sessionId: recordId };
|
|
1426
1493
|
}
|
|
1427
1494
|
return {
|
|
@@ -1449,7 +1516,8 @@ export async function backfillCompletedFromStateMd({
|
|
|
1449
1516
|
}
|
|
1450
1517
|
if (typeof repoRoot !== 'string' || repoRoot.length === 0) return;
|
|
1451
1518
|
if (!isLoggedBackfillAction(res.action)) return;
|
|
1452
|
-
|
|
1519
|
+
// #1592: one breadcrumb log beside the one ledger, not one per worktree.
|
|
1520
|
+
const logPath = path.join(resolveEventsLedgerRoot(repoRoot), ...BACKFILL_LOG_REL);
|
|
1453
1521
|
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
1454
1522
|
fs.appendFileSync(
|
|
1455
1523
|
logPath,
|