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,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idle Reset for STATE.md (issue #1560) — the code form of session-start
|
|
3
|
+
* Phase 1.5 § "Idle Reset" (skills/session-start/references/phase-1-5-session-continuity.md).
|
|
4
|
+
*
|
|
5
|
+
* Until #1560 the reset existed only as prose and every coordinator rewrote
|
|
6
|
+
* STATE.md by hand. Its Rule 6 deleted the five recommendation keys from the
|
|
7
|
+
* frontmatter, which is exactly what Phase 7.5's `buildLiveSignals()` reads —
|
|
8
|
+
* so after every clean `/close` the Mode-Selector saw no recommendation and
|
|
9
|
+
* fell back to `feature` at confidence 0. The keys now STAY in the frontmatter:
|
|
10
|
+
* they are the session-to-session handoff, and session-end Phase 3.7a (their
|
|
11
|
+
* sole writer) overwrites them at the next close.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately NOT re-exported from `scripts/lib/state-md.mjs`: that barrel is
|
|
14
|
+
* part of the hook import closure (`hooks/_lib/hook-import-set.json`), and no
|
|
15
|
+
* hook needs the reset. The CLI `scripts/state-md-idle-reset.mjs` imports this
|
|
16
|
+
* module directly.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { parseStateMd, serializeStateMd } from './yaml-parser.mjs';
|
|
20
|
+
import { existsSync } from 'node:fs';
|
|
21
|
+
import { resolveStateMdWriteTarget, updateFrontmatterFields, warnForeignStateMd, writeStateMd } from './frontmatter-mutators.mjs';
|
|
22
|
+
import { parseRecommendations } from './recommendations.mjs';
|
|
23
|
+
|
|
24
|
+
/** Rule 7: the drift-baseline keys that must not survive into session N+1. */
|
|
25
|
+
const SCOPE_BASELINE_KEYS = [
|
|
26
|
+
'scope-baseline-intent',
|
|
27
|
+
'scope-baseline-owner-boundary',
|
|
28
|
+
'scope-baseline-planned-files',
|
|
29
|
+
'scope-baseline-session',
|
|
30
|
+
'scope-baseline-frozen-at',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/** A level-2 heading (`## X`), never a deeper one (`### X`). */
|
|
34
|
+
const H2_RE = /^##(?!#)\s/;
|
|
35
|
+
|
|
36
|
+
/** Rule 6: heading of the human-readable recommendations copy. */
|
|
37
|
+
const RECOMMENDATIONS_HEADING = '### Recommendations (archived from v1.1 frontmatter)';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Splits a STATE.md body into the text before the first level-2 heading and
|
|
41
|
+
* one record per level-2 section. Every record keeps its raw lines (heading
|
|
42
|
+
* included, up to the next level-2 heading), so re-joining an untouched
|
|
43
|
+
* section is byte-identical.
|
|
44
|
+
*
|
|
45
|
+
* KNOWN CEILING: a `## ` line inside a fenced code block is read as a heading,
|
|
46
|
+
* the same limitation every helper in body-sections.mjs shares. No STATE.md
|
|
47
|
+
* section observed so far carries fenced code.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} body
|
|
50
|
+
* @returns {{preamble: string[], sections: Array<{heading: string, lines: string[]}>}}
|
|
51
|
+
*/
|
|
52
|
+
function splitSections(body) {
|
|
53
|
+
const preamble = [];
|
|
54
|
+
const sections = [];
|
|
55
|
+
for (const line of body.split('\n')) {
|
|
56
|
+
if (H2_RE.test(line)) sections.push({ heading: line, lines: [line] });
|
|
57
|
+
else if (sections.length === 0) preamble.push(line);
|
|
58
|
+
else sections[sections.length - 1].lines.push(line);
|
|
59
|
+
}
|
|
60
|
+
return { preamble, sections };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** @param {string} heading @param {string} title */
|
|
64
|
+
function isSection(heading, title) {
|
|
65
|
+
// The title must END there (or open a parenthesis): `## Wave History Archive`
|
|
66
|
+
// is a different section.
|
|
67
|
+
return new RegExp(`^##\\s+${title}(?:\\s*\\(|\\s*$)`).test(heading);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Drops leading and trailing blank lines. */
|
|
71
|
+
function trimBlankLines(lines) {
|
|
72
|
+
let start = 0;
|
|
73
|
+
let end = lines.length;
|
|
74
|
+
while (start < end && lines[start].trim() === '') start++;
|
|
75
|
+
while (end > start && lines[end - 1].trim() === '') end--;
|
|
76
|
+
return lines.slice(start, end);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @param {number} x a ratio in 0..1 */
|
|
80
|
+
function pct(x) {
|
|
81
|
+
return `${Math.round(x * 100)}%`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Rule 6: the human-readable copy of the recommendation fields. Null fields
|
|
86
|
+
* are omitted; `null` overall when there is nothing to show.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} frontmatter
|
|
89
|
+
* @returns {string|null}
|
|
90
|
+
*/
|
|
91
|
+
function renderRecommendationsBlock(frontmatter) {
|
|
92
|
+
const rec = parseRecommendations(frontmatter);
|
|
93
|
+
if (rec === null) return null;
|
|
94
|
+
const bullets = [];
|
|
95
|
+
if (rec.mode !== null) bullets.push(`- **Recommended mode:** ${rec.mode}`);
|
|
96
|
+
if (rec.rationale !== null) bullets.push(`- **Rationale:** ${rec.rationale}`);
|
|
97
|
+
if (rec.completionRate !== null) bullets.push(`- **Completion rate:** ${pct(rec.completionRate)}`);
|
|
98
|
+
if (rec.carryoverRatio !== null) bullets.push(`- **Carryover ratio:** ${pct(rec.carryoverRatio)}`);
|
|
99
|
+
if (rec.priorities !== null) {
|
|
100
|
+
const list = rec.priorities.length > 0 ? rec.priorities.map((id) => `#${id}`).join(', ') : 'none';
|
|
101
|
+
bullets.push(`- **Top priorities:** ${list}`);
|
|
102
|
+
}
|
|
103
|
+
if (bullets.length === 0) return null;
|
|
104
|
+
return [RECOMMENDATIONS_HEADING, ...bullets].join('\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Removes one `### <heading>` subsection (up to the next `### ` or `## ` line).
|
|
109
|
+
*
|
|
110
|
+
* @param {string[]} lines
|
|
111
|
+
* @param {string} heading the full `### …` line
|
|
112
|
+
* @returns {string[]}
|
|
113
|
+
*/
|
|
114
|
+
function dropSubsection(lines, heading) {
|
|
115
|
+
const out = [];
|
|
116
|
+
let skipping = false;
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
if (line.trimEnd() === heading) skipping = true;
|
|
119
|
+
else if (skipping && /^#{2,3}\s/.test(line)) skipping = false;
|
|
120
|
+
if (!skipping) out.push(line);
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resets a `status: completed` STATE.md to idle (Phase 1.5 § Idle Reset,
|
|
127
|
+
* Rules 1-7). Pure: string in, result out, no file I/O.
|
|
128
|
+
*
|
|
129
|
+
* 1. `status: idle`. 2. `current-wave: 0`. 3. Every `## Wave History` body moves
|
|
130
|
+
* into a new `## Previous Session` section (appended at the end of the body,
|
|
131
|
+
* replacing any earlier one) under `### Wave History (<session>)`; the original
|
|
132
|
+
* section is removed. 4. `## Deviations` keeps its heading with an empty body;
|
|
133
|
+
* `## What Not To Retry` and `## Open Questions` stay byte-for-byte. 5. Other
|
|
134
|
+
* frontmatter keys are untouched. 6. The five recommendation keys STAY in the
|
|
135
|
+
* frontmatter (Phase 7.5 reads them); a readable copy heads `## Previous Session`.
|
|
136
|
+
* 7. The five `scope-baseline-*` keys are deleted, no other key.
|
|
137
|
+
*
|
|
138
|
+
* When neither a Wave History section nor a recommendation exists, an existing
|
|
139
|
+
* `## Previous Session` is left as it is — there is nothing to replace it with.
|
|
140
|
+
* With recommendations but no Wave History (a coordinator-direct session), the
|
|
141
|
+
* earlier archive body stays below the new recommendations block.
|
|
142
|
+
*
|
|
143
|
+
* @param {string} contents STATE.md contents
|
|
144
|
+
* @returns {{changed: boolean, contents: string, reason?: 'status-not-completed'|'unparseable'}}
|
|
145
|
+
*/
|
|
146
|
+
export function resetStateMdToIdle(contents) {
|
|
147
|
+
const parsed = parseStateMd(contents);
|
|
148
|
+
if (parsed === null) return { changed: false, contents, reason: 'unparseable' };
|
|
149
|
+
if (parsed.frontmatter.status !== 'completed') {
|
|
150
|
+
return { changed: false, contents, reason: 'status-not-completed' };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const { preamble, sections } = splitSections(parsed.body);
|
|
154
|
+
const waveHistory = sections.filter((s) => isSection(s.heading, 'Wave History'));
|
|
155
|
+
const recBlock = renderRecommendationsBlock(parsed.frontmatter);
|
|
156
|
+
const archive = waveHistory.length > 0 || recBlock !== null;
|
|
157
|
+
// Rule 6 PREPENDS: without a new Wave History, the earlier archive stays below
|
|
158
|
+
// the fresh recommendations block (minus its own, now superseded, copy).
|
|
159
|
+
const keptArchive =
|
|
160
|
+
waveHistory.length === 0
|
|
161
|
+
? trimBlankLines(
|
|
162
|
+
dropSubsection(
|
|
163
|
+
sections.filter((s) => isSection(s.heading, 'Previous Session')).flatMap((s) => s.lines.slice(1)),
|
|
164
|
+
RECOMMENDATIONS_HEADING,
|
|
165
|
+
),
|
|
166
|
+
)
|
|
167
|
+
: [];
|
|
168
|
+
|
|
169
|
+
const lines = [...preamble];
|
|
170
|
+
for (const { heading, lines: own } of sections) {
|
|
171
|
+
if (isSection(heading, 'Wave History')) continue;
|
|
172
|
+
if (archive && isSection(heading, 'Previous Session')) continue;
|
|
173
|
+
lines.push(...(isSection(heading, 'Deviations') ? [heading, ''] : own));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let body = lines.join('\n');
|
|
177
|
+
if (archive) {
|
|
178
|
+
const blocks = [];
|
|
179
|
+
if (recBlock !== null) blocks.push(recBlock);
|
|
180
|
+
if (waveHistory.length > 0) {
|
|
181
|
+
const session = typeof parsed.frontmatter.session === 'string' ? parsed.frontmatter.session : '';
|
|
182
|
+
const heading = session ? `### Wave History (${session})` : '### Wave History';
|
|
183
|
+
const moved = trimBlankLines(waveHistory.flatMap((s) => s.lines.slice(1)));
|
|
184
|
+
blocks.push([heading, ...moved].join('\n'));
|
|
185
|
+
}
|
|
186
|
+
if (keptArchive.length > 0) blocks.push(keptArchive.join('\n'));
|
|
187
|
+
// Only ever ADD newlines before the new section — trimming would change the
|
|
188
|
+
// trailing bytes of whatever section now precedes it (possibly WNTR).
|
|
189
|
+
if (body.length > 0 && !body.endsWith('\n\n')) body += body.endsWith('\n') ? '\n' : '\n\n';
|
|
190
|
+
body += `## Previous Session\n\n${blocks.join('\n\n')}\n`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const fields = { status: 'idle', 'current-wave': 0 };
|
|
194
|
+
for (const key of SCOPE_BASELINE_KEYS) fields[key] = null;
|
|
195
|
+
const next = updateFrontmatterFields(
|
|
196
|
+
serializeStateMd({ frontmatter: parsed.frontmatter, body }),
|
|
197
|
+
fields,
|
|
198
|
+
);
|
|
199
|
+
return { changed: next !== contents, contents: next };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Applies `resetStateMdToIdle` to the repo's STATE.md under the STATE.md lock
|
|
204
|
+
* (`writeStateMd`). A missing or empty STATE.md is a no-op with reason `missing`;
|
|
205
|
+
* when the only STATE.md belongs to another platform's foreign session it is
|
|
206
|
+
* `foreign-state-md` (#1618), with one stderr WARN naming that file.
|
|
207
|
+
* A write the size-ceiling / frontmatter-safe guards refuse comes back as
|
|
208
|
+
* `changed: false` with that guard's reason.
|
|
209
|
+
*
|
|
210
|
+
* @param {string} repoRoot absolute repository root (required)
|
|
211
|
+
* @param {object} [opts] passed through to `writeStateMd`
|
|
212
|
+
* @returns {Promise<{changed: boolean, reason?: string, path: string}>}
|
|
213
|
+
* @throws {Error} when repoRoot is missing
|
|
214
|
+
*/
|
|
215
|
+
export async function resetStateMdToIdleOnDisk(repoRoot, opts = {}) {
|
|
216
|
+
if (!repoRoot) {
|
|
217
|
+
throw new Error(`resetStateMdToIdleOnDisk: repoRoot is required (got ${typeof repoRoot}).`);
|
|
218
|
+
}
|
|
219
|
+
// No STATE.md: answer before the lock, which would otherwise create
|
|
220
|
+
// <repoRoot>/.orchestrator/ in whatever directory a mistyped root names.
|
|
221
|
+
// Probe the same path writeStateMd will target (#1618), never a foreign one.
|
|
222
|
+
// The reset is the hand-over that turns the PREVIOUS session's completed
|
|
223
|
+
// record into idle, so a preferred `status: completed` file qualifies (#1640).
|
|
224
|
+
const writeOpts = { ...opts, completedHandoff: true };
|
|
225
|
+
const { path: statePath, rejected } = resolveStateMdWriteTarget(repoRoot, writeOpts);
|
|
226
|
+
if (!existsSync(statePath)) {
|
|
227
|
+
if (rejected) warnForeignStateMd('resetStateMdToIdleOnDisk', rejected, statePath);
|
|
228
|
+
return { changed: false, reason: rejected ? 'foreign-state-md' : 'missing', path: statePath };
|
|
229
|
+
}
|
|
230
|
+
let reason;
|
|
231
|
+
const result = await writeStateMd(
|
|
232
|
+
repoRoot,
|
|
233
|
+
(contents) => {
|
|
234
|
+
if (contents === '') {
|
|
235
|
+
reason = 'missing';
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
const reset = resetStateMdToIdle(contents);
|
|
239
|
+
reason = reset.reason;
|
|
240
|
+
return reset.changed ? reset.contents : null;
|
|
241
|
+
},
|
|
242
|
+
writeOpts,
|
|
243
|
+
);
|
|
244
|
+
const out = { changed: result.written, path: result.path };
|
|
245
|
+
const why = result.reason ?? reason;
|
|
246
|
+
if (why !== undefined) out.reason = why;
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { parseStateMd, serializeStateMd } from './yaml-parser.mjs';
|
|
26
|
-
import { updateFrontmatterFields, writeStateMd } from './frontmatter-mutators.mjs';
|
|
26
|
+
import { resolveStateMdWriteTarget, updateFrontmatterFields, writeStateMd } from './frontmatter-mutators.mjs';
|
|
27
27
|
|
|
28
28
|
const MISSION_STATUS_HEADING_RE = /^##\s+Mission Status\s*$/;
|
|
29
29
|
const WRITER_TIMESTAMP_SOURCE = '\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z';
|
|
@@ -677,9 +677,13 @@ const REFUSAL_REASONS = Object.freeze({
|
|
|
677
677
|
*/
|
|
678
678
|
export async function setMissionStatusOnDisk(repoRoot, taskId, status, opts = {}) {
|
|
679
679
|
let refused;
|
|
680
|
+
// A fail-closed write target (#1618) hands an empty buffer; writeStateMd
|
|
681
|
+
// already names the rejected foreign STATE.md, so do not blame its content.
|
|
682
|
+
const foreign = resolveStateMdWriteTarget(repoRoot, opts).rejected !== null;
|
|
680
683
|
const result = await writeStateMd(
|
|
681
684
|
repoRoot,
|
|
682
685
|
(contents) => {
|
|
686
|
+
if (foreign && contents === '') return contents;
|
|
683
687
|
const outcome = setMissionStatusDetailed(contents, taskId, status);
|
|
684
688
|
refused = outcome.refused;
|
|
685
689
|
if (outcome.refused) {
|
|
@@ -37,10 +37,7 @@ export function parseRecommendations(frontmatter) {
|
|
|
37
37
|
const mode = typeof frontmatter['recommended-mode'] === 'string'
|
|
38
38
|
? frontmatter['recommended-mode']
|
|
39
39
|
: null;
|
|
40
|
-
const priorities =
|
|
41
|
-
&& frontmatter['top-priorities'].every((x) => Number.isInteger(x))
|
|
42
|
-
? frontmatter['top-priorities'].slice()
|
|
43
|
-
: null;
|
|
40
|
+
const priorities = parsePriorities(frontmatter['top-priorities']);
|
|
44
41
|
const carryoverRatio = typeof frontmatter['carryover-ratio'] === 'number'
|
|
45
42
|
&& !Number.isNaN(frontmatter['carryover-ratio'])
|
|
46
43
|
? frontmatter['carryover-ratio']
|
|
@@ -55,3 +52,28 @@ export function parseRecommendations(frontmatter) {
|
|
|
55
52
|
|
|
56
53
|
return { mode, priorities, carryoverRatio, completionRate, rationale };
|
|
57
54
|
}
|
|
55
|
+
|
|
56
|
+
/** An entry that is exactly an issue reference of THIS repo: `42` or `#42`. */
|
|
57
|
+
const ISSUE_REF_RE = /^#?\d+$/;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Normalises `top-priorities` to issue numbers. Integers pass; strings that are
|
|
61
|
+
* exactly an issue ref (`"#42"`, `"42"`) become integers; every other entry is
|
|
62
|
+
* dropped. A hand-written `"#370 fix banner"` or `"other-repo#74"` is never
|
|
63
|
+
* mined for digits, because the number may belong to another repo. An array
|
|
64
|
+
* that held entries but keeps none is a type mismatch (`null`), as before.
|
|
65
|
+
*
|
|
66
|
+
* @param {unknown} raw
|
|
67
|
+
* @returns {number[]|null}
|
|
68
|
+
*/
|
|
69
|
+
function parsePriorities(raw) {
|
|
70
|
+
if (!Array.isArray(raw)) return null;
|
|
71
|
+
const ids = [];
|
|
72
|
+
for (const entry of raw) {
|
|
73
|
+
if (Number.isInteger(entry)) ids.push(entry);
|
|
74
|
+
else if (typeof entry === 'string' && ISSUE_REF_RE.test(entry.trim())) {
|
|
75
|
+
ids.push(Number.parseInt(entry.trim().replace('#', ''), 10));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return raw.length > 0 && ids.length === 0 ? null : ids;
|
|
79
|
+
}
|
|
@@ -55,6 +55,8 @@ function orderedStateMdCandidates() {
|
|
|
55
55
|
* @returns {string} Absolute path (may not exist on disk).
|
|
56
56
|
*/
|
|
57
57
|
function _resolveStateMdPath(repoRoot) {
|
|
58
|
+
// #1636 decision: no ownership filter here — this guard exists to SEE another
|
|
59
|
+
// session's STATE.md, so a foreign file is its input, not noise to drop.
|
|
58
60
|
const candidates = orderedStateMdCandidates();
|
|
59
61
|
for (const candidate of candidates) {
|
|
60
62
|
const abs = join(repoRoot, candidate);
|
package/scripts/lib/state-md.mjs
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { parseStateMd as _parseStateMd } from './state-md/yaml-parser.mjs';
|
|
20
20
|
import { updateFrontmatterFields as _updateFrontmatterFields } from './state-md/frontmatter-mutators.mjs';
|
|
21
21
|
import { readLock as _readLock } from './session-lock.mjs';
|
|
22
|
+
import { readProcessLocalSessionIds as _readProcessLocalSessionIds } from './session-identity/own-session.mjs';
|
|
22
23
|
|
|
23
24
|
export { parseStateMd, serializeStateMd } from './state-md/yaml-parser.mjs';
|
|
24
25
|
|
|
@@ -195,27 +196,49 @@ export function resolveSessionStartedAt({ repoRoot } = {}) {
|
|
|
195
196
|
/**
|
|
196
197
|
* The session ids a STATE.md writer may stamp into frontmatter: `session-id`
|
|
197
198
|
* (the RAW/native harness id) and `session` (the semantic attribution label),
|
|
198
|
-
* both read from the live lock
|
|
199
|
+
* both read from the live lock — and `session-id` only when the lock is
|
|
200
|
+
* provably THIS process's own (#1663).
|
|
199
201
|
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
202
|
+
* The lock is repo-global: a session that lost the acquire race, or a
|
|
203
|
+
* pre-#1641 lock without `claude_pid` that survives until its TTL, names a
|
|
204
|
+
* PEER. Since #1661 `/close` decides ownership by comparing STATE.md
|
|
205
|
+
* `session-id` against `readProcessLocalSessionIds()`, so stamping the peer's
|
|
206
|
+
* id would make this session's own STATE.md read `foreign` and stop its own
|
|
207
|
+
* close. Hence:
|
|
208
|
+
* - `session_id` is the lock's raw id only when it is contained in
|
|
209
|
+
* `readProcessLocalSessionIds({ env })`; otherwise `null`. A process that
|
|
210
|
+
* cannot name its own id (Cursor, Pi, Codex without a thread id) gets
|
|
211
|
+
* `null` too — unprovable is not own.
|
|
212
|
+
* - `semantic_session_id` is `null` when the lock is PROVABLY foreign (this
|
|
213
|
+
* process has an id and the lock names another). Ceiling (BV-004): when
|
|
214
|
+
* the process has no id at all, the label is still taken from the lock,
|
|
215
|
+
* as before #1663, so the #429 label join keeps working on those
|
|
216
|
+
* harnesses — it may then name a peer. Revisit when every harness
|
|
217
|
+
* exposes a native id.
|
|
218
|
+
*
|
|
219
|
+
* ABSENCE IS NEVER COERCED: a writer must OMIT a `null` key rather than write
|
|
220
|
+
* a placeholder. Neither id grants ownership of anything (see
|
|
203
221
|
* `skills/_shared/state-ownership.md`); `session-id` exists so `/close`'s #429
|
|
204
222
|
* pre-check can join STATE.md to sessions.jsonl on a native identity instead
|
|
205
223
|
* of falling through to the label + timestamp path.
|
|
206
224
|
*
|
|
207
225
|
* Never throws.
|
|
208
226
|
*
|
|
209
|
-
* @param {{ repoRoot?: string }} [opts]
|
|
227
|
+
* @param {{ repoRoot?: string, env?: object }} [opts] `env` defaults to
|
|
228
|
+
* `process.env`; injectable for tests.
|
|
210
229
|
* @returns {{ session_id: string|null, semantic_session_id: string|null }}
|
|
211
230
|
*/
|
|
212
|
-
export function resolveSessionIds({ repoRoot } = {}) {
|
|
231
|
+
export function resolveSessionIds({ repoRoot, env = process.env } = {}) {
|
|
213
232
|
const pick = (value) => (typeof value === 'string' && value.trim().length > 0 ? value.trim() : null);
|
|
214
233
|
try {
|
|
215
234
|
const lock = _readLock({ repoRoot });
|
|
235
|
+
const lockId = pick(lock?.session_id);
|
|
236
|
+
const ownIds = _readProcessLocalSessionIds({ env });
|
|
237
|
+
const own = lockId !== null && ownIds.includes(lockId);
|
|
238
|
+
const provablyForeign = lockId !== null && ownIds.length > 0 && !own;
|
|
216
239
|
return {
|
|
217
|
-
session_id:
|
|
218
|
-
semantic_session_id: pick(lock?.semantic_session_id),
|
|
240
|
+
session_id: own ? lockId : null,
|
|
241
|
+
semantic_session_id: provablyForeign ? null : pick(lock?.semantic_session_id),
|
|
219
242
|
};
|
|
220
243
|
} catch {
|
|
221
244
|
return { session_id: null, semantic_session_id: null };
|
|
@@ -42,6 +42,7 @@ import path from 'node:path';
|
|
|
42
42
|
import { normalizeSkillInvocation } from '../skill-invocations-schema.mjs';
|
|
43
43
|
import { normalizeSkillJudgment } from '../skill-judgments-schema.mjs';
|
|
44
44
|
import { isMainModule } from '../is-main-module.mjs';
|
|
45
|
+
import { resolveLedgerPath } from '../ledger-path.mjs';
|
|
45
46
|
import { userInvocableSkills } from '../user-invocable-skills.mjs';
|
|
46
47
|
|
|
47
48
|
// ---------------------------------------------------------------------------
|
|
@@ -948,15 +949,15 @@ export function runSunsetWalk(repoRoot, opts = {}) {
|
|
|
948
949
|
|
|
949
950
|
const subagentsPath =
|
|
950
951
|
opts.subagentsPath ??
|
|
951
|
-
|
|
952
|
+
resolveLedgerPath('.orchestrator/metrics/subagents.jsonl', repoRoot);
|
|
952
953
|
|
|
953
954
|
const invocationsPath =
|
|
954
955
|
opts.invocationsPath ??
|
|
955
|
-
|
|
956
|
+
resolveLedgerPath('.orchestrator/metrics/skill-invocations.jsonl', repoRoot);
|
|
956
957
|
|
|
957
958
|
const judgmentsPath =
|
|
958
959
|
opts.judgmentsPath ??
|
|
959
|
-
|
|
960
|
+
resolveLedgerPath('.orchestrator/metrics/skill-judgments.jsonl', repoRoot);
|
|
960
961
|
|
|
961
962
|
const surface = enumerateSurface(repoRoot);
|
|
962
963
|
const dispatch = readDispatchCounts(subagentsPath, { windowDays, now: nowMs });
|
|
@@ -159,11 +159,13 @@ export function readTelemetryState({ path } = {}) {
|
|
|
159
159
|
export function writeTelemetryState(record, { path } = {}) {
|
|
160
160
|
const target = path || TELEMETRY_JSON_PATH;
|
|
161
161
|
try {
|
|
162
|
-
|
|
162
|
+
// 0o700: the default target is the private config dir, which must not come
|
|
163
|
+
// out 0777 under umask 000 (#1629).
|
|
164
|
+
mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
|
|
163
165
|
} catch (err) {
|
|
164
166
|
return { ok: false, error: err?.message ?? String(err) };
|
|
165
167
|
}
|
|
166
|
-
const res = writeJsonAtomicSync(target, record);
|
|
168
|
+
const res = writeJsonAtomicSync(target, record, { mode: 0o600 }); // consent + anon_id are host-private (#1632)
|
|
167
169
|
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
168
170
|
}
|
|
169
171
|
|
|
@@ -91,7 +91,7 @@ function _serializedByteLength(entries) {
|
|
|
91
91
|
* via tmp-file + `renameSync` in the SAME directory (same-filesystem rename
|
|
92
92
|
* is atomic on POSIX — mirrors `writeJsonAtomicSync` in `scripts/lib/io.mjs`,
|
|
93
93
|
* which has no NDJSON-array variant). Creates the parent directory with
|
|
94
|
-
* `mkdirSync(dir, { recursive: true })` first.
|
|
94
|
+
* `mkdirSync(dir, { recursive: true, mode: 0o700 })` first.
|
|
95
95
|
*
|
|
96
96
|
* @param {string} filePath
|
|
97
97
|
* @param {Array<object>} entries
|
|
@@ -101,10 +101,10 @@ function _serializedByteLength(entries) {
|
|
|
101
101
|
*/
|
|
102
102
|
function _writeEntriesAtomicSync(filePath, entries) {
|
|
103
103
|
const dir = dirname(filePath);
|
|
104
|
-
mkdirSync(dir, { recursive: true });
|
|
104
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 }); // private config dir; 0777 under umask 000 otherwise (#1629)
|
|
105
105
|
const tmpSuffix = randomBytes(6).toString('hex');
|
|
106
106
|
const tmpFile = join(dir, `.tmp.${tmpSuffix}`);
|
|
107
|
-
writeFileSync(tmpFile, _toNdjson(entries), 'utf8');
|
|
107
|
+
writeFileSync(tmpFile, _toNdjson(entries), { encoding: 'utf8', mode: 0o600 }); // rename keeps this mode (#1632)
|
|
108
108
|
renameSync(tmpFile, filePath);
|
|
109
109
|
}
|
|
110
110
|
|
|
@@ -23,9 +23,10 @@
|
|
|
23
23
|
* queue (bounded, oldest-dropped) and the session closes with zero user-facing
|
|
24
24
|
* error.
|
|
25
25
|
*
|
|
26
|
-
* Node ESM.
|
|
26
|
+
* Node ESM. Networking uses the Stdlib HTTP helper.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
+
import { httpRequest } from '../http-request.mjs';
|
|
29
30
|
import path from 'node:path';
|
|
30
31
|
import os from 'node:os';
|
|
31
32
|
import fs from 'node:fs';
|
|
@@ -43,10 +44,11 @@ import { loadOwnerConfig } from '../owner-yaml.mjs';
|
|
|
43
44
|
import { readJsonlFile } from '../io.mjs';
|
|
44
45
|
import { readEventsWithRotations } from '../events.mjs';
|
|
45
46
|
import { resolveEventsLedgerRoot } from '../platform.mjs';
|
|
47
|
+
import { resolveLedgerPath } from '../ledger-path.mjs';
|
|
46
48
|
import { readCanonicalSessions } from '../sessions-canonical.mjs';
|
|
47
49
|
import { resolvePrivateConfigDir } from '../config/private-config-dir.mjs';
|
|
48
50
|
import { readSessionProfile } from '../state-md.mjs';
|
|
49
|
-
import {
|
|
51
|
+
import { resolveStateMdWriteTarget } from '../state-md/frontmatter-mutators.mjs';
|
|
50
52
|
|
|
51
53
|
// ---------------------------------------------------------------------------
|
|
52
54
|
// Constants
|
|
@@ -73,7 +75,7 @@ function resolveEndpoint(env) {
|
|
|
73
75
|
|
|
74
76
|
/**
|
|
75
77
|
* The default network sender: one POST carrying the whole batch array as the
|
|
76
|
-
* JSON body (the server accepts arrays), with
|
|
78
|
+
* JSON body (the server accepts arrays), with a wall-clock timeout. Resolves
|
|
77
79
|
* on a 2xx status, rejects on anything else (which routes the caller into the
|
|
78
80
|
* offline queue).
|
|
79
81
|
*
|
|
@@ -85,11 +87,12 @@ function resolveEndpoint(env) {
|
|
|
85
87
|
function defaultSender({ env, timeoutMs }) {
|
|
86
88
|
const endpoint = resolveEndpoint(env);
|
|
87
89
|
return async (batches) => {
|
|
88
|
-
const res = await
|
|
90
|
+
const res = await httpRequest(endpoint, {
|
|
89
91
|
method: 'POST',
|
|
90
92
|
headers: { 'Content-Type': 'application/json' },
|
|
91
93
|
body: JSON.stringify(batches),
|
|
92
|
-
|
|
94
|
+
timeoutMs,
|
|
95
|
+
maxBytes: 64 * 1024,
|
|
93
96
|
});
|
|
94
97
|
if (!res.ok) {
|
|
95
98
|
// The status travels ON the error: `flush` needs it to tell a TRANSPORT
|
|
@@ -307,7 +310,15 @@ export function readSessionProfileForMetricsDir(metricsDir) {
|
|
|
307
310
|
// metricsDir is `<repoRoot>/.orchestrator/metrics` by construction (every
|
|
308
311
|
// caller builds it that way); two levels up is the repo root.
|
|
309
312
|
const repoRoot = path.resolve(metricsDir, '..', '..');
|
|
310
|
-
|
|
313
|
+
// #1636: the write side's ownership rule — another platform's STATE.md not
|
|
314
|
+
// proven to be this process's is no profile source (ABSENT IS NOT EMPTY:
|
|
315
|
+
// null ⇒ the ping omits the field). Deliberately NO warnForeignStateMd line:
|
|
316
|
+
// this runs inside flush, which the hooks call in-process at SessionEnd or
|
|
317
|
+
// spawn detached with `stdio: 'ignore'` — the line would be hook noise or
|
|
318
|
+
// lost, and an omitted optional field needs no operator action.
|
|
319
|
+
const { path: stateMdPath, rejected } = resolveStateMdWriteTarget(repoRoot);
|
|
320
|
+
if (rejected) return null;
|
|
321
|
+
return readSessionProfile(fs.readFileSync(stateMdPath, 'utf8'));
|
|
311
322
|
} catch {
|
|
312
323
|
return null;
|
|
313
324
|
}
|
|
@@ -494,6 +505,23 @@ export function deriveSessionFromEvents(metricsDir) {
|
|
|
494
505
|
}
|
|
495
506
|
}
|
|
496
507
|
|
|
508
|
+
/**
|
|
509
|
+
* `skill-invocations.jsonl` for the metrics dir `metricsDir` (#1722): a
|
|
510
|
+
* `<root>/.orchestrator/metrics` dir resolves through {@link resolveLedgerPath}
|
|
511
|
+
* for `<root>` — the main checkout's ledger when `<root>` is a linked worktree
|
|
512
|
+
* that ignores it, else the dir's own — and any other dir is read as given.
|
|
513
|
+
*
|
|
514
|
+
* @param {string} metricsDir
|
|
515
|
+
* @returns {string}
|
|
516
|
+
*/
|
|
517
|
+
function skillInvocationsPathFor(metricsDir) {
|
|
518
|
+
const parent = path.dirname(metricsDir);
|
|
519
|
+
if (path.basename(metricsDir) === 'metrics' && path.basename(parent) === '.orchestrator') {
|
|
520
|
+
return resolveLedgerPath('.orchestrator/metrics/skill-invocations.jsonl', path.dirname(parent));
|
|
521
|
+
}
|
|
522
|
+
return path.join(metricsDir, 'skill-invocations.jsonl');
|
|
523
|
+
}
|
|
524
|
+
|
|
497
525
|
/**
|
|
498
526
|
* Build ONE whitelist-projected usage-ping record from the local JSONL streams.
|
|
499
527
|
*
|
|
@@ -537,11 +565,19 @@ export function buildBatch({
|
|
|
537
565
|
consentState,
|
|
538
566
|
} = {}) {
|
|
539
567
|
try {
|
|
568
|
+
// `dir` stays the caller's metrics dir: STATE.md and the events fallback
|
|
569
|
+
// below derive their own roots from it.
|
|
570
|
+
// ledger-root: the caller's dir as given; each ledger in it resolves below
|
|
540
571
|
const dir = metricsDir || path.join(process.cwd(), '.orchestrator', 'metrics');
|
|
541
572
|
const nowIso = now || new Date().toISOString();
|
|
542
573
|
|
|
543
|
-
|
|
544
|
-
|
|
574
|
+
// #1722: each ledger resolves on its own, an explicit metricsDir included
|
|
575
|
+
// (the SessionEnd flush hands over the LAUNCH dir's, which is a linked
|
|
576
|
+
// worktree for a session started there; deriveSessionFromEvents maps the
|
|
577
|
+
// events fallback the same way) — sessions via the one-ledger root
|
|
578
|
+
// (#1514/#1584), skill-invocations via skillInvocationsPathFor().
|
|
579
|
+
const sessions = readCanonicalSessions({ filePath: path.join(resolveEventsLedgerRoot(dir), 'sessions.jsonl') });
|
|
580
|
+
const invocations = readJsonlFile(skillInvocationsPathFor(dir), { skipInvalid: true });
|
|
545
581
|
|
|
546
582
|
const sessionRecord = mostRecentSession(sessions);
|
|
547
583
|
|
|
@@ -855,7 +891,7 @@ export function shouldDailyFlush({ statePath, queuePath, metricsDir, now = Date.
|
|
|
855
891
|
// (271 records / 292 KB in this repo, 2026-08-23) that is sub-millisecond
|
|
856
892
|
// and it only runs once the 24h gate above has already passed. Revisit
|
|
857
893
|
// with a tail-read if any repo's sessions.jsonl passes ~10 MB.
|
|
858
|
-
const dir = metricsDir || path.join(process.cwd(), '.orchestrator', 'metrics');
|
|
894
|
+
const dir = metricsDir || path.join(resolveEventsLedgerRoot(process.cwd()), '.orchestrator', 'metrics');
|
|
859
895
|
const sessions = readCanonicalSessions({ filePath: path.join(dir, 'sessions.jsonl') });
|
|
860
896
|
const last = mostRecentSession(sessions);
|
|
861
897
|
const completedMs = Date.parse(last?.completed_at);
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import path from 'node:path';
|
|
20
|
+
import { resolveLedgerPath } from '../ledger-path.mjs';
|
|
20
21
|
|
|
21
22
|
const ROLLUP_REL = '.orchestrator/metrics/test-runs.jsonl';
|
|
22
23
|
const TEST_RUNS_REL = '.orchestrator/metrics/test-runs';
|
|
@@ -62,6 +63,7 @@ export function makeRunId() {
|
|
|
62
63
|
* @throws {TypeError} on an invalid runId (see {@link assertRunId})
|
|
63
64
|
*/
|
|
64
65
|
export function runDirPath(runId) {
|
|
66
|
+
// ledger-root: per-run artifact dir of this working tree, kept local
|
|
65
67
|
return path.join(TEST_RUNS_REL, assertRunId(runId));
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -112,8 +114,14 @@ export function consoleLogPath(runId) {
|
|
|
112
114
|
|
|
113
115
|
/**
|
|
114
116
|
* Path to the shared JSONL rollup file (across all runs).
|
|
117
|
+
*
|
|
118
|
+
* Without `repoRoot` it is the repo-relative constant. With `repoRoot` it is
|
|
119
|
+
* the absolute ledger path, routed to the main checkout when `repoRoot` is a
|
|
120
|
+
* linked worktree of a repo that ignores the rollup (#1722).
|
|
121
|
+
*
|
|
122
|
+
* @param {string} [repoRoot]
|
|
115
123
|
* @returns {string}
|
|
116
124
|
*/
|
|
117
|
-
export function jsonlRollupPath() {
|
|
118
|
-
return ROLLUP_REL;
|
|
125
|
+
export function jsonlRollupPath(repoRoot) {
|
|
126
|
+
return typeof repoRoot === 'string' && repoRoot !== '' ? resolveLedgerPath(ROLLUP_REL, repoRoot) : ROLLUP_REL;
|
|
119
127
|
}
|