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
|
@@ -261,8 +261,9 @@ export function reconcileIssueBudget({
|
|
|
261
261
|
* Emit the reconciliation record to the repo's event ledger.
|
|
262
262
|
*
|
|
263
263
|
* Follows `_emitEvaluated` in `scripts/lib/express-path.mjs`: `events.mjs` is
|
|
264
|
-
* imported LAZILY (
|
|
265
|
-
*
|
|
264
|
+
* imported LAZILY (it is not needed by the verdict path; `platform.mjs` is
|
|
265
|
+
* already reached statically through `issue-budget.mjs`, which maps the
|
|
266
|
+
* counter-file root through it since #1729), `repoRoot` is
|
|
266
267
|
* passed EXPLICITLY and a missing one SKIPS the emit rather than falling
|
|
267
268
|
* through to the ambient `SO_PROJECT_DIR` — writing a synthetic record into the
|
|
268
269
|
* operator's real fleet ledger is unrecoverable, a skipped record is not — and
|
|
@@ -33,10 +33,11 @@ import { homedir } from 'node:os';
|
|
|
33
33
|
import path from 'node:path';
|
|
34
34
|
|
|
35
35
|
import { writeJsonAtomicSync } from './io.mjs';
|
|
36
|
+
import { resolveEventsLedgerRoot } from './platform.mjs';
|
|
36
37
|
import { resolveInstructionFile } from './common.mjs';
|
|
37
38
|
import { _parseIssueBudget } from './config/issue-budget.mjs';
|
|
38
39
|
import { parseStateMd } from './state-md/yaml-parser.mjs';
|
|
39
|
-
import {
|
|
40
|
+
import { resolveStateMdWriteTarget } from './state-md/frontmatter-mutators.mjs';
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
43
|
* Legacy single-slot counter file, relative to the repo root.
|
|
@@ -192,8 +193,11 @@ export function resolveMaxPerSession(cfg, sessionType) {
|
|
|
192
193
|
*/
|
|
193
194
|
export function readSessionTypeFromStateMd(repoRoot) {
|
|
194
195
|
try {
|
|
195
|
-
|
|
196
|
-
|
|
196
|
+
// #1636: the write side's ownership rule — another platform's STATE.md not
|
|
197
|
+
// proven to be this process's must not set THIS session's budget; it counts
|
|
198
|
+
// as absent (default budget). No warn line: this runs on every Bash call.
|
|
199
|
+
const { path: filePath, rejected } = resolveStateMdWriteTarget(repoRoot);
|
|
200
|
+
if (rejected || !existsSync(filePath)) return null;
|
|
197
201
|
const parsed = parseStateMd(readFileSync(filePath, 'utf8'));
|
|
198
202
|
const value = parsed?.frontmatter?.['session-type'];
|
|
199
203
|
return typeof value === 'string' && value.trim() !== '' ? value.trim() : null;
|
|
@@ -251,7 +255,27 @@ export function loadIssueBudgetConfig(repoRoot) {
|
|
|
251
255
|
* @returns {string}
|
|
252
256
|
*/
|
|
253
257
|
export function budgetStatePath(repoRoot, sessionId = null) {
|
|
254
|
-
return path.join(repoRoot, budgetStateRel(sessionId));
|
|
258
|
+
return path.join(budgetStateRoot(repoRoot), budgetStateRel(sessionId));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Root directory the runtime counter files live under (#1729). The PreToolUse
|
|
263
|
+
* hook charges at `CLAUDE_PROJECT_DIR` (usually the MAIN checkout) while
|
|
264
|
+
* session-end reconciles and reaps with the linked-worktree root; joining the
|
|
265
|
+
* raw root made the two sides open different files and report a false
|
|
266
|
+
* `no-ledger`. Mapping a linked worktree to its main checkout (the same
|
|
267
|
+
* resolver the event ledger uses) makes every reader and writer agree. Any
|
|
268
|
+
* other root (plain checkout, non-git dir, submodule) is returned resolved but
|
|
269
|
+
* otherwise unchanged. A non-string or blank root is passed through untouched
|
|
270
|
+
* so `path.join` keeps its previous behaviour instead of falling back to the
|
|
271
|
+
* ambient project dir.
|
|
272
|
+
*
|
|
273
|
+
* @param {string} repoRoot
|
|
274
|
+
* @returns {string}
|
|
275
|
+
*/
|
|
276
|
+
function budgetStateRoot(repoRoot) {
|
|
277
|
+
if (typeof repoRoot !== 'string' || repoRoot.trim() === '') return repoRoot;
|
|
278
|
+
return resolveEventsLedgerRoot(repoRoot);
|
|
255
279
|
}
|
|
256
280
|
|
|
257
281
|
/**
|
|
@@ -360,7 +384,9 @@ export function readBudgetState(repoRoot, sessionId) {
|
|
|
360
384
|
// session has a file of its own.
|
|
361
385
|
if (existsSync(ownFile)) return fresh;
|
|
362
386
|
|
|
363
|
-
return
|
|
387
|
+
return (
|
|
388
|
+
_readStateFile(path.join(budgetStateRoot(repoRoot), BUDGET_STATE_REL), accountingSessionId) ?? fresh
|
|
389
|
+
);
|
|
364
390
|
}
|
|
365
391
|
|
|
366
392
|
/**
|
|
@@ -882,11 +908,14 @@ export function reapStaleBudgetFiles({
|
|
|
882
908
|
const removed = [];
|
|
883
909
|
const kept = [];
|
|
884
910
|
try {
|
|
885
|
-
|
|
911
|
+
// Directory and own file from ONE mapped root (#1729): mapping only one of
|
|
912
|
+
// them would make the `file === ownFile` protection below never match.
|
|
913
|
+
const stateRoot = budgetStateRoot(repoRoot);
|
|
914
|
+
const dir = path.join(stateRoot, BUDGET_STATE_DIR_REL);
|
|
886
915
|
const cutoff = now - maxAgeDays * 24 * 60 * 60 * 1000;
|
|
887
916
|
const ownFile =
|
|
888
917
|
typeof sessionId === 'string' && sessionId.length > 0
|
|
889
|
-
?
|
|
918
|
+
? path.join(stateRoot, budgetStateRel(sessionId))
|
|
890
919
|
: null;
|
|
891
920
|
|
|
892
921
|
let names;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explicit, private-audience candidate artifacts are advisory lookup hints.
|
|
3
|
+
* Only current local JSONL records enter the selector; artifact paths, scores
|
|
4
|
+
* and text never become records, executable commands or additional authority.
|
|
5
|
+
*/
|
|
6
|
+
import { constants } from 'node:fs';
|
|
7
|
+
import { open } from 'node:fs/promises';
|
|
8
|
+
import { TextDecoder } from 'node:util';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
|
|
11
|
+
import { parseLearningsText } from './io.mjs';
|
|
12
|
+
import { digestMultiBufferSha256 } from '../crypto-digest-utils.mjs';
|
|
13
|
+
|
|
14
|
+
// At most 40 hints in a small local artifact. Revisit the byte ceiling only if
|
|
15
|
+
// a valid 40-candidate producer payload demonstrably exceeds it.
|
|
16
|
+
export const EVIDENCE_ARTIFACT_MAX_BYTES = 128 * 1024;
|
|
17
|
+
|
|
18
|
+
const hash = z.string().regex(/^sha256:[0-9a-f]{64}$/);
|
|
19
|
+
const repo = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/);
|
|
20
|
+
const evidenceType = z.enum(['learning', 'skill', 'adr']);
|
|
21
|
+
const retrievalMode = z.enum(['bm25', 'eg2', 'rrf']);
|
|
22
|
+
const unique = (values) => new Set(values).size === values.length;
|
|
23
|
+
const candidate = z.object({
|
|
24
|
+
evidence_type: evidenceType,
|
|
25
|
+
source_repo: repo,
|
|
26
|
+
source_record_id: z.string().min(1).max(200).optional(),
|
|
27
|
+
source_generation: hash.optional(),
|
|
28
|
+
source_path: z.string().min(1).regex(/^(?!\/)(?![A-Za-z]:)(?!.*(?:^|\/)\.\.(?:\/|$))(?!.*\\).+$/),
|
|
29
|
+
source_sha256: hash,
|
|
30
|
+
source_revision: z.string().min(1).max(64),
|
|
31
|
+
line_start: z.number().int().min(1),
|
|
32
|
+
line_end: z.number().int().min(1),
|
|
33
|
+
index_generation: hash,
|
|
34
|
+
modelspace: hash,
|
|
35
|
+
rank: z.number().int().min(1).max(40),
|
|
36
|
+
}).strict().refine((value) => value.evidence_type === 'learning'
|
|
37
|
+
? value.source_record_id !== undefined && value.source_generation !== undefined
|
|
38
|
+
: value.source_record_id === undefined && value.source_generation === undefined);
|
|
39
|
+
|
|
40
|
+
/** Local boundary for urn:local-evidence:candidate-artifact:1. */
|
|
41
|
+
export const evidenceCandidateSchema = z.object({
|
|
42
|
+
schema_version: z.literal(1),
|
|
43
|
+
status: z.enum(['ok', 'empty', 'disabled', 'stale', 'unavailable', 'unreadable', 'corrupt', 'modelspace_mismatch', 'error']),
|
|
44
|
+
query: z.object({
|
|
45
|
+
text: z.string().min(1).max(4000),
|
|
46
|
+
source_repos: z.array(repo).min(1).refine(unique),
|
|
47
|
+
audience: z.literal('private'),
|
|
48
|
+
evidence_types: z.array(evidenceType).min(1).refine(unique),
|
|
49
|
+
}).strict(),
|
|
50
|
+
freshness: z.object({
|
|
51
|
+
mode: z.literal('live_sources'),
|
|
52
|
+
checked_at: z.string().datetime({ offset: true }),
|
|
53
|
+
}).strict(),
|
|
54
|
+
generation: hash.nullable(),
|
|
55
|
+
source_generations: z.record(repo, hash),
|
|
56
|
+
retrieval: z.object({
|
|
57
|
+
requested: retrievalMode,
|
|
58
|
+
effective: retrievalMode,
|
|
59
|
+
fallback_reason: z.enum(['semantic_unconfigured', 'semantic_unavailable', 'semantic_failed', 'modelspace_mismatch', 'index_lexical_only']).optional(),
|
|
60
|
+
}).strict(),
|
|
61
|
+
candidates: z.array(candidate).max(40),
|
|
62
|
+
}).strict().refine((value) => value.status === 'ok' || value.candidates.length === 0);
|
|
63
|
+
|
|
64
|
+
/** Bounded regular-file read; a supplied FIFO must not hang a dispatch. */
|
|
65
|
+
async function readArtifact(filePath) {
|
|
66
|
+
const handle = await open(filePath, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
67
|
+
try {
|
|
68
|
+
const info = await handle.stat();
|
|
69
|
+
if (!info.isFile() || info.size > EVIDENCE_ARTIFACT_MAX_BYTES) throw new Error('invalid-artifact-file');
|
|
70
|
+
const buffer = Buffer.alloc(EVIDENCE_ARTIFACT_MAX_BYTES + 1);
|
|
71
|
+
let size = 0;
|
|
72
|
+
while (size < buffer.length) {
|
|
73
|
+
const { bytesRead } = await handle.read(buffer, size, buffer.length - size, size);
|
|
74
|
+
if (bytesRead === 0) break;
|
|
75
|
+
size += bytesRead;
|
|
76
|
+
}
|
|
77
|
+
if (size > EVIDENCE_ARTIFACT_MAX_BYTES) throw new Error('artifact-too-large');
|
|
78
|
+
return JSON.parse(buffer.subarray(0, size).toString('utf8'));
|
|
79
|
+
} finally {
|
|
80
|
+
await handle.close();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolve hints against the explicitly supplied current repo's canonical store.
|
|
86
|
+
* Missing/stale/corrupt inputs return no additions, preserving the baseline.
|
|
87
|
+
* Diagnostics contain only statuses/counts, never artifact paths or query text.
|
|
88
|
+
*/
|
|
89
|
+
export async function readEvidenceCandidates({ artifactPath, sourceRepo, query, sourceBytes }) {
|
|
90
|
+
const result = (status, reason, entries = [], dropped = 0, retrieval) => ({
|
|
91
|
+
entries,
|
|
92
|
+
diagnostics: { status, ...(reason ? { reason } : {}), accepted: entries.length, dropped,
|
|
93
|
+
...(retrieval ? { retrieval } : {}) },
|
|
94
|
+
});
|
|
95
|
+
if (!repo.safeParse(sourceRepo).success || typeof query !== 'string' || !query.trim() ||
|
|
96
|
+
query.length > 4000 || typeof artifactPath !== 'string' || !artifactPath) {
|
|
97
|
+
return result('corrupt', 'invalid_request');
|
|
98
|
+
}
|
|
99
|
+
let artifact;
|
|
100
|
+
try {
|
|
101
|
+
const parsed = evidenceCandidateSchema.safeParse(await readArtifact(artifactPath));
|
|
102
|
+
if (!parsed.success) return result('corrupt', 'invalid_artifact');
|
|
103
|
+
artifact = parsed.data;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error instanceof SyntaxError) return result('corrupt', 'invalid_artifact');
|
|
106
|
+
return result(error?.code === 'ENOENT' ? 'unavailable' : 'unreadable', 'artifact_read_failed');
|
|
107
|
+
}
|
|
108
|
+
if (artifact.query.text !== query || artifact.query.source_repos.length !== 1 ||
|
|
109
|
+
artifact.query.source_repos[0] !== sourceRepo) return result('corrupt', 'request_mismatch');
|
|
110
|
+
if (artifact.status !== 'ok') return result(artifact.status, 'provider_status', [], 0, artifact.retrieval);
|
|
111
|
+
if (artifact.generation === null) return result('corrupt', 'missing_generation');
|
|
112
|
+
|
|
113
|
+
// One source read supplies BOTH the baseline pool and these original records.
|
|
114
|
+
// A missing store is distinct from an existing, valid empty JSONL snapshot.
|
|
115
|
+
if (!Buffer.isBuffer(sourceBytes)) return result('unavailable', 'source_read_failed');
|
|
116
|
+
let sourceText;
|
|
117
|
+
try {
|
|
118
|
+
// ignoreBOM retains the BOM in the decoded text; the canonical parser
|
|
119
|
+
// removes it only from the first record, never from the generation hash.
|
|
120
|
+
sourceText = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(sourceBytes);
|
|
121
|
+
} catch {
|
|
122
|
+
return result('corrupt', 'source_utf8_invalid');
|
|
123
|
+
}
|
|
124
|
+
const snapshot = {
|
|
125
|
+
...parseLearningsText(sourceText),
|
|
126
|
+
generation: `sha256:${digestMultiBufferSha256([sourceBytes])}`,
|
|
127
|
+
};
|
|
128
|
+
if (artifact.source_generations[sourceRepo] !== snapshot.generation) {
|
|
129
|
+
return result('stale', 'source_generation_mismatch');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const byId = new Map();
|
|
133
|
+
for (const entry of snapshot.entries) {
|
|
134
|
+
if (typeof entry.id !== 'string') continue;
|
|
135
|
+
// An ambiguous duplicate ID cannot identify one original record.
|
|
136
|
+
byId.set(entry.id, byId.has(entry.id) ? null : entry);
|
|
137
|
+
}
|
|
138
|
+
const entries = [];
|
|
139
|
+
const seen = new Set();
|
|
140
|
+
let dropped = 0;
|
|
141
|
+
for (const hint of artifact.candidates) {
|
|
142
|
+
const entry = byId.get(hint.source_record_id);
|
|
143
|
+
if (hint.evidence_type !== 'learning' || !artifact.query.evidence_types.includes('learning') ||
|
|
144
|
+
hint.source_repo !== sourceRepo || hint.source_generation !== snapshot.generation ||
|
|
145
|
+
hint.source_sha256 !== snapshot.generation || hint.index_generation !== artifact.generation ||
|
|
146
|
+
hint.line_end < hint.line_start || !entry || seen.has(hint.source_record_id)) {
|
|
147
|
+
dropped++;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
seen.add(hint.source_record_id);
|
|
151
|
+
entries.push(entry);
|
|
152
|
+
}
|
|
153
|
+
return result('ok', undefined, entries, dropped, artifact.retrieval);
|
|
154
|
+
}
|
|
@@ -70,8 +70,15 @@
|
|
|
70
70
|
import { constants as fsConstants, existsSync } from 'node:fs';
|
|
71
71
|
import { mkdir, appendFile, copyFile } from 'node:fs/promises';
|
|
72
72
|
import path from 'node:path';
|
|
73
|
-
import {
|
|
73
|
+
import {
|
|
74
|
+
appendInboxReceipts,
|
|
75
|
+
readLearnings,
|
|
76
|
+
readLearningsSnapshot,
|
|
77
|
+
rewriteLearnings,
|
|
78
|
+
withLearningsLock,
|
|
79
|
+
} from './io.mjs';
|
|
74
80
|
import { redactHomeDir } from '../common.mjs';
|
|
81
|
+
import { assertStoreRoleUsable, isCanonicalStore, resolveStoreRole } from './store-role.mjs';
|
|
75
82
|
|
|
76
83
|
/**
|
|
77
84
|
* Thrown by {@link pruneLearnings} when `expectedGeneration` no longer matches
|
|
@@ -216,6 +223,26 @@ function reconcileKey(entry) {
|
|
|
216
223
|
*/
|
|
217
224
|
export const ARCHIVE_REASONS = Object.freeze(['expired', 'pruned', 'superseded', 'merged']);
|
|
218
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Value of the additive `skipped` result field when an APPLY ran on a host
|
|
228
|
+
* whose learnings-store role is `inbox` (#1516).
|
|
229
|
+
*/
|
|
230
|
+
export const SKIPPED_STORE_ROLE_INBOX = 'store-role-inbox';
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* True when an apply against `filePath` must not archive or rewrite (#1516):
|
|
234
|
+
* the canonical store on an `inbox`-role host. There `rewriteLearnings()`
|
|
235
|
+
* never touches the live store, so an archive append would record learnings
|
|
236
|
+
* that are still live. Dry runs are unaffected — they write nothing and keep
|
|
237
|
+
* reporting what a writer host would do.
|
|
238
|
+
*
|
|
239
|
+
* @param {unknown} filePath
|
|
240
|
+
* @returns {boolean}
|
|
241
|
+
*/
|
|
242
|
+
function isInboxRoleStore(filePath) {
|
|
243
|
+
return typeof filePath === 'string' && isCanonicalStore(filePath) && resolveStoreRole().role === 'inbox';
|
|
244
|
+
}
|
|
245
|
+
|
|
219
246
|
/** Reason -> the tombstone field that reason MUST carry. */
|
|
220
247
|
const TOMBSTONE_FIELD = Object.freeze({
|
|
221
248
|
superseded: '_superseded_by',
|
|
@@ -360,7 +387,7 @@ function resolveNowMs(now) {
|
|
|
360
387
|
* @param {number} opts.nowMs
|
|
361
388
|
* @param {boolean} opts.dryRun
|
|
362
389
|
* @returns {Promise<{kept: number, archived: number, byReason: Record<string, number>,
|
|
363
|
-
* malformedField: {malformed?: number}}>}
|
|
390
|
+
* malformedField: {malformed?: number}, skipped?: string}>}
|
|
364
391
|
*/
|
|
365
392
|
async function archiveThenRewrite({
|
|
366
393
|
filePath,
|
|
@@ -372,6 +399,21 @@ async function archiveThenRewrite({
|
|
|
372
399
|
nowMs,
|
|
373
400
|
dryRun,
|
|
374
401
|
}) {
|
|
402
|
+
if (!dryRun && isInboxRoleStore(filePath)) {
|
|
403
|
+
// Inbox role (#1516): no archive line — the live store keeps every record,
|
|
404
|
+
// so archiving one would duplicate a live learning. The rewrite still runs:
|
|
405
|
+
// on this role it only appends the batch's NEW ids to the inbox (e.g. the
|
|
406
|
+
// session's fresh learnings from apply-session-learnings) and drops every
|
|
407
|
+
// update, removal and consolidation.
|
|
408
|
+
await rewriteLearnings(filePath, keep, { dryRun: false, malformedLines: malformed });
|
|
409
|
+
return {
|
|
410
|
+
kept: keep.length,
|
|
411
|
+
archived: 0,
|
|
412
|
+
byReason: {},
|
|
413
|
+
malformedField: malformedField(filePath, malformed, malformedLineNumbers),
|
|
414
|
+
skipped: SKIPPED_STORE_ROLE_INBOX,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
375
417
|
const byReason = countByReason(archiveBatch);
|
|
376
418
|
const result = {
|
|
377
419
|
kept: keep.length,
|
|
@@ -402,6 +444,18 @@ async function archiveThenRewrite({
|
|
|
402
444
|
.join('\n') + '\n';
|
|
403
445
|
await mkdir(path.dirname(archivePath), { recursive: true });
|
|
404
446
|
await appendFile(archivePath, body, 'utf8');
|
|
447
|
+
// Inbox receipts (#1516) for archived ids that came from the inbox: the
|
|
448
|
+
// archive is not synced to the inbox host, so without a receipt the record
|
|
449
|
+
// turns visible there again. Written AFTER the archive append (a crash
|
|
450
|
+
// cannot leave a receipt for a record that is neither live nor archived)
|
|
451
|
+
// and BEFORE the rewrite. A crash between the archive append and this
|
|
452
|
+
// receipt still leaves an archived inbox record without one; the next
|
|
453
|
+
// writer rewriteLearnings() heals it (io.mjs `unreceiptedArchivedIds`).
|
|
454
|
+
// Both callers hold the store lock here, as appendInboxReceipts requires.
|
|
455
|
+
await appendInboxReceipts(
|
|
456
|
+
filePath,
|
|
457
|
+
archiveBatch.map(({ entry }) => entry?.id)
|
|
458
|
+
);
|
|
405
459
|
}
|
|
406
460
|
|
|
407
461
|
// Re-validates (cheap, idempotent) and snapshots a `.bak-<ISO>` backup of
|
|
@@ -431,16 +485,24 @@ async function archiveThenRewrite({
|
|
|
431
485
|
* malformed?: number}>} `malformed` is present only when the store held
|
|
432
486
|
* unparseable lines: their count. They are kept verbatim at the end of the
|
|
433
487
|
* rewritten store, never archived, and not counted in `scanned`/`kept`.
|
|
434
|
-
* Never throws on a missing store — returns the zeroed shape instead.
|
|
488
|
+
* Never throws on a missing store — returns the zeroed shape instead. An
|
|
489
|
+
* apply on an `inbox`-role host (#1516) reads and writes nothing and returns
|
|
490
|
+
* the zeroed shape plus `skipped: 'store-role-inbox'`. Both
|
|
435
491
|
* `filePath` and `archivePath` are required; a missing/invalid `filePath`
|
|
436
492
|
* throws a plain `Error` (programmer error, not a runtime data condition).
|
|
493
|
+
* @throws {LearningsStoreRoleError} an apply on the canonical store while the
|
|
494
|
+
* host's store role is invalid (#1516) — nothing archived or rewritten
|
|
437
495
|
*/
|
|
438
496
|
export async function sweepExpiredLearnings(opts = {}) {
|
|
439
497
|
const { filePath, dryRun = true } = opts;
|
|
498
|
+
// Fail-closed (#1516): an apply under an invalid store role refuses before
|
|
499
|
+
// the archive append — archived-but-live records would follow otherwise.
|
|
500
|
+
if (!dryRun && typeof filePath === 'string') assertStoreRoleUsable(filePath);
|
|
440
501
|
// The read → rewrite runs under the store lock (#1447 point 8). A run that
|
|
441
502
|
// cannot write takes no lock: a dry run, an absent store (zeroed shape, see
|
|
442
503
|
// below), or an unusable `filePath` (the body owns that error).
|
|
443
|
-
|
|
504
|
+
// An inbox-role apply (#1516) writes nothing either — no lock.
|
|
505
|
+
if (dryRun || typeof filePath !== 'string' || !existsSync(filePath) || isInboxRoleStore(filePath)) {
|
|
444
506
|
return sweepExpiredLearningsUnlocked(opts);
|
|
445
507
|
}
|
|
446
508
|
return withLearningsLock(filePath, () => sweepExpiredLearningsUnlocked(opts));
|
|
@@ -464,6 +526,13 @@ async function sweepExpiredLearningsUnlocked({
|
|
|
464
526
|
throw new Error('sweepExpiredLearnings: archivePath is required');
|
|
465
527
|
}
|
|
466
528
|
|
|
529
|
+
if (!dryRun && isInboxRoleStore(filePath)) {
|
|
530
|
+
// Inbox role (#1516): an expiry sweep is a pure removal, which this host
|
|
531
|
+
// never performs on the live store — the writer host sweeps. Zeroed counts
|
|
532
|
+
// so no caller reads "archived N"; `skipped` names why.
|
|
533
|
+
return { scanned: 0, kept: 0, archived: 0, dryRun, archivePath, skipped: SKIPPED_STORE_ROLE_INBOX };
|
|
534
|
+
}
|
|
535
|
+
|
|
467
536
|
if (!existsSync(filePath)) {
|
|
468
537
|
return { scanned: 0, kept: 0, archived: 0, dryRun, archivePath };
|
|
469
538
|
}
|
|
@@ -575,11 +644,18 @@ async function sweepExpiredLearningsUnlocked({
|
|
|
575
644
|
* byReason: Record<string, number>, dryRun: boolean, archivePath: string,
|
|
576
645
|
* malformed?: number}>} `malformed` as in {@link sweepExpiredLearnings}: the
|
|
577
646
|
* store's unparseable lines, kept verbatim — no caller generation can carry
|
|
578
|
-
* them, so they are never read as caller drops.
|
|
647
|
+
* them, so they are never read as caller drops. On an `inbox`-role host
|
|
648
|
+
* (#1516) an apply archives nothing (`archived: 0`, `byReason: {}`) and adds
|
|
649
|
+
* `skipped: 'store-role-inbox'`; its rewrite only appends the next
|
|
650
|
+
* generation's new ids to `learnings-inbox.jsonl`.
|
|
579
651
|
* @throws {StoreGenerationMismatchError} when `expectedGeneration` is stale
|
|
652
|
+
* @throws {LearningsStoreRoleError} an apply on the canonical store while the
|
|
653
|
+
* host's store role is invalid (#1516) — nothing archived or rewritten
|
|
580
654
|
*/
|
|
581
655
|
export async function pruneLearnings(opts = {}) {
|
|
582
656
|
const { filePath, dryRun = true } = opts;
|
|
657
|
+
// Fail-closed (#1516): refuse before the archive append, see sweepExpiredLearnings.
|
|
658
|
+
if (!dryRun && typeof filePath === 'string') assertStoreRoleUsable(filePath);
|
|
583
659
|
// The read → archive → rewrite runs under the store lock (#1447 point 8): an
|
|
584
660
|
// append landing after the read below would otherwise vanish in the rename
|
|
585
661
|
// with no archive line. A dry run writes nothing and takes no lock; an
|
|
@@ -722,7 +798,7 @@ async function pruneLearningsUnlocked({
|
|
|
722
798
|
dropped += 1;
|
|
723
799
|
}
|
|
724
800
|
|
|
725
|
-
const { kept, archived, byReason, malformedField: malformedCount } = await archiveThenRewrite({
|
|
801
|
+
const { kept, archived, byReason, malformedField: malformedCount, skipped } = await archiveThenRewrite({
|
|
726
802
|
filePath,
|
|
727
803
|
archivePath,
|
|
728
804
|
keep,
|
|
@@ -741,6 +817,7 @@ async function pruneLearningsUnlocked({
|
|
|
741
817
|
dryRun,
|
|
742
818
|
archivePath,
|
|
743
819
|
...malformedCount,
|
|
820
|
+
...(skipped ? { skipped } : {}),
|
|
744
821
|
};
|
|
745
822
|
}
|
|
746
823
|
|
|
@@ -842,8 +919,12 @@ function dropSnapshotPath(filePath, nowMs) {
|
|
|
842
919
|
* remainingMalformed: number, recordBearing: number, dryRun: boolean, snapshot: string|null}>}
|
|
843
920
|
* `generation` is the store's as read; `chars` the line's full length, `preview` its first
|
|
844
921
|
* {@link DROP_PREVIEW_CHARS} characters with the home dir as `~`; `recordBearing` counts the
|
|
845
|
-
* dropped lines that held a readable record
|
|
922
|
+
* dropped lines that held a readable record. On an `inbox`-role host (#1516)
|
|
923
|
+
* an apply writes nothing: it returns the dry-run result (`dryRun: true`,
|
|
924
|
+
* `snapshot: null`) plus `skipped: 'store-role-inbox'`
|
|
846
925
|
* @throws {StoreGenerationMismatchError} the store is not at `expectedGeneration` — nothing written
|
|
926
|
+
* @throws {LearningsStoreRoleError} an apply on the canonical store while the
|
|
927
|
+
* host's store role is invalid (#1516) — nothing written
|
|
847
928
|
* @throws {MalformedLineRefusedError} a line is not unparseable, or (apply without
|
|
848
929
|
* `acceptEmbedded`) still holds a readable record — nothing written
|
|
849
930
|
*/
|
|
@@ -864,6 +945,17 @@ export async function dropMalformedLines({
|
|
|
864
945
|
if (!dryRun && (typeof expectedGeneration !== 'string' || expectedGeneration.length === 0)) {
|
|
865
946
|
throw new Error('dropMalformedLines: expectedGeneration is required to apply');
|
|
866
947
|
}
|
|
948
|
+
// Fail-closed (#1516): refuse before the snapshot and the rewrite.
|
|
949
|
+
if (!dryRun) assertStoreRoleUsable(filePath);
|
|
950
|
+
if (!dryRun && isInboxRoleStore(filePath)) {
|
|
951
|
+
// Inbox role (#1516): the live store is never rewritten on this host, so
|
|
952
|
+
// no snapshot and no rewrite. The checks still run (dry), so a stale
|
|
953
|
+
// generation or a wrong line still throws; the result says nothing moved.
|
|
954
|
+
const probe = await dropMalformedLinesUnlocked({
|
|
955
|
+
filePath, lines, expectedGeneration, acceptEmbedded, dryRun: true, now,
|
|
956
|
+
});
|
|
957
|
+
return { ...probe, skipped: SKIPPED_STORE_ROLE_INBOX };
|
|
958
|
+
}
|
|
867
959
|
const run = () => dropMalformedLinesUnlocked({ filePath, lines, expectedGeneration, acceptEmbedded, dryRun, now });
|
|
868
960
|
// An absent store has no line to drop — the body refuses without a lock.
|
|
869
961
|
return dryRun || !existsSync(filePath) ? run() : withLearningsLock(filePath, run);
|