session-orchestrator 5.12.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 +2 -2
- 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 +162 -4
- package/README.md +126 -127
- 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 +1 -1
- 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 +90 -17
- 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 +49 -15
- package/docs/session-config-template.md +30 -22
- 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 +139 -49
- 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 -2
- 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 +39 -27
- 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/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 +2 -2
- 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 +4 -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-id.mjs +9 -1
- package/scripts/lib/session-identity/own-session.mjs +63 -0
- package/scripts/lib/session-lock.mjs +114 -0
- package/scripts/lib/session-record-repair.mjs +245 -35
- package/scripts/lib/session-registry.mjs +72 -9
- 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 +49 -5
- package/scripts/lib/so-hud/runtime.mjs +15 -4
- 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 +14 -5
- package/scripts/lib/state-md/mission-status.mjs +5 -1
- 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 +19 -7
- package/scripts/lib/vault-mirror/process.mjs +56 -27
- package/scripts/lib/vault-mirror/prune.mjs +5 -2
- package/scripts/lib/vault-mirror/render-learnings.mjs +3 -3
- 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 +79 -0
- package/scripts/lib/vault-staleness-banner.mjs +4 -7
- package/scripts/lib/vault-status/board-writer.mjs +2 -1
- 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 +10 -17
- 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 +29 -6
- package/scripts/state-md-idle-reset.mjs +2 -1
- 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/skills/_shared/fleet-protocol.md +142 -1
- 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 +3 -2
- package/skills/architecture/references/domain-model.md +1 -1
- 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 +94 -11
- 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-documentation-updates.md +14 -12
- package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
- package/skills/session-end/references/session-summary-template.md +4 -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 +9 -3
- package/skills/session-start/phase-4-5-resource-health.md +11 -5
- 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 +2 -2
- 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/assets/og-card.png +0 -0
- package/assets/og-card.svg +0 -47
- package/assets/wave-lifecycle.svg +0 -98
|
@@ -40,7 +40,10 @@
|
|
|
40
40
|
* Slice: `.slice(0, n)` after sort.
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
|
+
import { existsSync } from 'node:fs';
|
|
43
44
|
import { readFile } from 'node:fs/promises';
|
|
45
|
+
import { readLearnings } from './io.mjs';
|
|
46
|
+
import { inboxPathFor, isCanonicalStore } from './store-role.mjs';
|
|
44
47
|
|
|
45
48
|
const MS_PER_DAY = 86_400_000;
|
|
46
49
|
|
|
@@ -147,6 +150,49 @@ export function effectiveScore(entry, nowMs, decay) {
|
|
|
147
150
|
* then sliced to `n`. Returns [] on missing/unreadable file or all-empty input.
|
|
148
151
|
*/
|
|
149
152
|
export async function surfaceTopN(filePath, n = 5, opts = {}) {
|
|
153
|
+
// Preserve the pre-read clock used by the existing file reader.
|
|
154
|
+
const now = opts.now instanceof Date ? opts.now.getTime()
|
|
155
|
+
: typeof opts.now === 'number' ? opts.now : Date.now();
|
|
156
|
+
let raw;
|
|
157
|
+
try {
|
|
158
|
+
raw = await readFile(filePath, 'utf8');
|
|
159
|
+
} catch {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
return surfaceLearningsText(await withVisibleInboxText(filePath, raw), n, { ...opts, now });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* `raw` (the live store's text) plus the visible inbox records of the
|
|
167
|
+
* canonical store, one JSON line each (#1516) — the same visibility rule
|
|
168
|
+
* `readLearnings()` applies. Without an inbox file beside a canonical store
|
|
169
|
+
* `raw` comes back unchanged, so every pre-#1516 output stays byte-identical.
|
|
170
|
+
*
|
|
171
|
+
* Ceiling (BV-004): the live file is read twice when an inbox exists (once
|
|
172
|
+
* raw here, once by `readLearnings`); revisit if the inbox grows past the
|
|
173
|
+
* 1 MiB trigger named in io.mjs.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} filePath — the store `raw` was read from
|
|
176
|
+
* @param {string} raw
|
|
177
|
+
* @returns {Promise<string>}
|
|
178
|
+
*/
|
|
179
|
+
export async function withVisibleInboxText(filePath, raw) {
|
|
180
|
+
if (!isCanonicalStore(filePath) || !existsSync(inboxPathFor(filePath))) return raw;
|
|
181
|
+
let read;
|
|
182
|
+
try {
|
|
183
|
+
read = await readLearnings(filePath);
|
|
184
|
+
} catch {
|
|
185
|
+
return raw; // an unreadable inbox never costs the live surface
|
|
186
|
+
}
|
|
187
|
+
const { entries, inbox } = read;
|
|
188
|
+
const visible = inbox ? entries.slice(entries.length - inbox.visible) : [];
|
|
189
|
+
if (visible.length === 0) return raw;
|
|
190
|
+
const sep = raw.length === 0 || raw.endsWith('\n') ? '' : '\n';
|
|
191
|
+
return `${raw}${sep}${visible.map((e) => JSON.stringify(e)).join('\n')}\n`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Same surface policy over one already-read JSONL snapshot; no I/O. */
|
|
195
|
+
export function surfaceLearningsText(raw, n = 5, opts = {}) {
|
|
150
196
|
const { now: nowOpt, confidenceFloor = 0.3, decay: decayOpt } = opts;
|
|
151
197
|
const nowMs =
|
|
152
198
|
nowOpt instanceof Date
|
|
@@ -163,13 +209,6 @@ export async function surfaceTopN(filePath, n = 5, opts = {}) {
|
|
|
163
209
|
floorFactor: decayOpt?.floorFactor ?? DECAY_DEFAULTS.floorFactor,
|
|
164
210
|
};
|
|
165
211
|
|
|
166
|
-
let raw;
|
|
167
|
-
try {
|
|
168
|
-
raw = await readFile(filePath, 'utf8');
|
|
169
|
-
} catch {
|
|
170
|
-
return [];
|
|
171
|
-
}
|
|
172
|
-
|
|
173
212
|
const lines = raw
|
|
174
213
|
.split('\n')
|
|
175
214
|
.map((l) => l.trim())
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a ledger under `.orchestrator/` lives when its writer or reader runs
|
|
3
|
+
* in a linked git worktree (#1722).
|
|
4
|
+
*
|
|
5
|
+
* A linked worktree has its own working tree, so `<worktree>/.orchestrator/
|
|
6
|
+
* metrics/<x>.jsonl` is a different file from the main checkout's. Which one is
|
|
7
|
+
* right depends on whether the repo versions the ledger:
|
|
8
|
+
*
|
|
9
|
+
* - **gitignored** (this plugin repo, `.gitignore:57`): the worktree copy is
|
|
10
|
+
* lost when the worktree is removed, and every reader that maps to the main
|
|
11
|
+
* checkout never sees it. Measured 2026-10-22: `eval.jsonl` stranded in
|
|
12
|
+
* `_wt/session-orchestrator/so-66c`, `skill-invocations.jsonl` and
|
|
13
|
+
* `eval.jsonl` in `so-1e`. → the main checkout's copy.
|
|
14
|
+
* - **versioned** (27 fleet repos track `learnings`/`sessions`/… under
|
|
15
|
+
* `.orchestrator/metrics/`, counted 2026-10-21T23:06Z; the bootstrap
|
|
16
|
+
* template calls them durable project data): the worktree copy travels
|
|
17
|
+
* with the branch and reaches main through the merge. Writing the main
|
|
18
|
+
* checkout's copy instead would dirty another session's working tree and
|
|
19
|
+
* leave the branch without the record. → the worktree's own copy.
|
|
20
|
+
*
|
|
21
|
+
* `git check-ignore` (index-aware: a tracked file is never reported ignored)
|
|
22
|
+
* decides between the two. It is spawned ONLY inside a linked worktree — a
|
|
23
|
+
* plain checkout, a non-git dir and a test tmpdir resolve spawn-free to the
|
|
24
|
+
* local path, as before — and memoised per process.
|
|
25
|
+
*
|
|
26
|
+
* Only a worktree's TOP LEVEL maps. A root below it (a caller's cwd in
|
|
27
|
+
* `scripts/` or in an ignored `.orchestrator/tmp/…`) keeps the local path, as
|
|
28
|
+
* before #1722: `git check-ignore` would answer for the ignored subdirectory,
|
|
29
|
+
* not for the ledger, and the mapped path would land inside the main
|
|
30
|
+
* checkout's copy of that subdirectory.
|
|
31
|
+
*
|
|
32
|
+
* Not used for `events.jsonl` and `sessions.jsonl`: those map unconditionally
|
|
33
|
+
* through {@link resolveEventsLedgerRoot} (owner decision on #1514 point 1,
|
|
34
|
+
* and #1584), versioned or not.
|
|
35
|
+
*
|
|
36
|
+
* Ceiling: a failed or timed-out `git check-ignore` (exit ≥ 128, no git, 2 s)
|
|
37
|
+
* keeps the local path — the pre-#1722 behaviour, never a write into a foreign
|
|
38
|
+
* working tree on an unmeasured answer. Revisit if a `*.jsonl` is found growing
|
|
39
|
+
* inside a linked worktree of a repo that ignores it.
|
|
40
|
+
*
|
|
41
|
+
* @module scripts/lib/ledger-path
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { spawnSync } from 'node:child_process';
|
|
45
|
+
import path from 'node:path';
|
|
46
|
+
import { dotGitAncestor, getProjectDir, resolveEventsLedgerRoot } from './platform.mjs';
|
|
47
|
+
|
|
48
|
+
/** @type {Map<string, boolean>} `<root>\0<rel>` → ignored in that worktree */
|
|
49
|
+
const ignoredMemo = new Map();
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The `git` child must answer for `root`: an ambient `GIT_DIR` outranks `-C`
|
|
53
|
+
* (see `scripts/lib/git-config-drift.mjs`), and a hook can run inside a git
|
|
54
|
+
* hook that exports it.
|
|
55
|
+
*/
|
|
56
|
+
function gitEnv() {
|
|
57
|
+
const env = {};
|
|
58
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
59
|
+
if (!key.startsWith('GIT_')) env[key] = value;
|
|
60
|
+
}
|
|
61
|
+
return env;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isIgnoredIn(root, relPath) {
|
|
65
|
+
const key = `${root}\0${relPath}`;
|
|
66
|
+
if (ignoredMemo.has(key)) return ignoredMemo.get(key);
|
|
67
|
+
const r = spawnSync('git', ['-C', root, 'check-ignore', '-q', '--', relPath], {
|
|
68
|
+
env: gitEnv(),
|
|
69
|
+
stdio: 'ignore',
|
|
70
|
+
timeout: 2000,
|
|
71
|
+
});
|
|
72
|
+
const ignored = r.status === 0;
|
|
73
|
+
ignoredMemo.set(key, ignored);
|
|
74
|
+
return ignored;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The repo root the ledger `relPath` lives under for a caller rooted at `root`:
|
|
79
|
+
* the main checkout when `root` is a linked worktree that ignores the ledger,
|
|
80
|
+
* else `root` itself (resolved). Path guards that require a target inside the
|
|
81
|
+
* project validate against THIS root, so a mapped ledger passes and a path
|
|
82
|
+
* outside it is still refused.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} relPath ledger path relative to the repo root
|
|
85
|
+
* @param {string} [root] repo root the caller works in; default {@link getProjectDir}
|
|
86
|
+
* @returns {string} absolute root
|
|
87
|
+
*/
|
|
88
|
+
export function resolveLedgerRoot(relPath, root) {
|
|
89
|
+
const base = path.resolve(typeof root === 'string' && root.trim() !== '' ? root : getProjectDir());
|
|
90
|
+
const mainRoot = resolveEventsLedgerRoot(base);
|
|
91
|
+
if (mainRoot === base || dotGitAncestor(base) !== base) return base;
|
|
92
|
+
return isIgnoredIn(base, relPath) ? mainRoot : base;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Absolute path of the ledger `relPath` for a writer or reader rooted at `root`.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} relPath ledger path relative to the repo root, e.g.
|
|
99
|
+
* `.orchestrator/metrics/eval.jsonl`; an absolute path is returned unchanged
|
|
100
|
+
* (an explicit override stays exactly what the caller asked for)
|
|
101
|
+
* @param {string} [root] repo root the caller works in; default
|
|
102
|
+
* {@link getProjectDir} (the launch dir in a hook, the cwd's project in a CLI)
|
|
103
|
+
* @returns {string}
|
|
104
|
+
*/
|
|
105
|
+
export function resolveLedgerPath(relPath, root) {
|
|
106
|
+
if (path.isAbsolute(relPath)) return relPath;
|
|
107
|
+
return path.join(resolveLedgerRoot(relPath, root), relPath);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Test seam: forget memoised `git check-ignore` answers. */
|
|
111
|
+
export function _resetLedgerPathMemo() {
|
|
112
|
+
ignoredMemo.clear();
|
|
113
|
+
}
|
|
@@ -79,12 +79,13 @@ import { sweepExpiredLearnings } from './learnings/expiry-sweep.mjs';
|
|
|
79
79
|
import { shouldDispatchAutoDialectic } from './auto-dialectic.mjs';
|
|
80
80
|
import { shouldDispatchAutoDream } from './auto-dream.mjs';
|
|
81
81
|
import { resolveMemoryDir } from './memory-paths.mjs';
|
|
82
|
-
import {
|
|
83
|
-
import {
|
|
82
|
+
import { resolveLedgerPath } from './ledger-path.mjs';
|
|
83
|
+
import { readCanonicalSessions, isMeasuredSession } from './sessions-canonical.mjs';
|
|
84
84
|
import {
|
|
85
85
|
daysUntilGeneratedRuleExpiry,
|
|
86
86
|
listMachineGeneratedRules,
|
|
87
87
|
} from './instruction-budget-guard.mjs';
|
|
88
|
+
import { planRuleExpirySweep } from './reconcile/rule-expiry-sweep.mjs';
|
|
88
89
|
|
|
89
90
|
/**
|
|
90
91
|
* How many signals this probe knows about. The banner denominator is this
|
|
@@ -233,7 +234,7 @@ function readLastEvolveRun(repoRoot) {
|
|
|
233
234
|
*/
|
|
234
235
|
function readLastHousekeeping(repoRoot) {
|
|
235
236
|
try {
|
|
236
|
-
const entries =
|
|
237
|
+
const entries = readCanonicalSessions({ repoRoot }).filter(isMeasuredSession);
|
|
237
238
|
let max = null;
|
|
238
239
|
for (const e of entries) {
|
|
239
240
|
if (e?.session_type !== 'housekeeping') continue;
|
|
@@ -275,6 +276,40 @@ function isClaudePlatform(platform) {
|
|
|
275
276
|
* lastHousekeeping: string|null,
|
|
276
277
|
* }>}
|
|
277
278
|
*/
|
|
279
|
+
/**
|
|
280
|
+
* Generated rule files the expiry sweep can never act on from THIS host: the
|
|
281
|
+
* sweep's plan at the end of the warning horizon keeps the file whole because
|
|
282
|
+
* not one of its entries resolves to a learning in the local store. The
|
|
283
|
+
* learnings store is host-local and untracked, so a host whose store lacks the
|
|
284
|
+
* ids (measured 2026-10-19 on the M5: 6 records, 0 of 77 rule ids) was told to
|
|
285
|
+
* run a command that sweeps nothing. A failing plan (unreadable store) yields
|
|
286
|
+
* an empty set, i.e. the pre-existing recommendation.
|
|
287
|
+
*
|
|
288
|
+
* @param {string} repoRoot
|
|
289
|
+
* @param {number} now epoch ms
|
|
290
|
+
* @returns {Promise<Set<string>>} rule file names
|
|
291
|
+
*/
|
|
292
|
+
async function unsweepableRuleFiles(repoRoot, now) {
|
|
293
|
+
try {
|
|
294
|
+
const plan = await planRuleExpirySweep({
|
|
295
|
+
repoRoot,
|
|
296
|
+
now: new Date(now + GENERATED_RULE_EXPIRY_HORIZON_DAYS * 86_400_000).toISOString(),
|
|
297
|
+
});
|
|
298
|
+
return new Set(
|
|
299
|
+
plan.plans
|
|
300
|
+
.filter(
|
|
301
|
+
(p) =>
|
|
302
|
+
p.action === 'keep' &&
|
|
303
|
+
p.substantivePairs > 0 &&
|
|
304
|
+
p.unresolvedPairIds.length === p.substantivePairs,
|
|
305
|
+
)
|
|
306
|
+
.map((p) => p.file),
|
|
307
|
+
);
|
|
308
|
+
} catch {
|
|
309
|
+
return new Set();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
278
313
|
export async function computeMaintenanceDue(opts = {}) {
|
|
279
314
|
const repoRoot =
|
|
280
315
|
typeof opts.repoRoot === 'string' && opts.repoRoot.length > 0 ? opts.repoRoot : process.cwd();
|
|
@@ -323,7 +358,8 @@ export async function computeMaintenanceDue(opts = {}) {
|
|
|
323
358
|
if (nudge.nudge === true) {
|
|
324
359
|
// HR-106: report what the rule JUDGED. `computeReconcileNudge` nudges on
|
|
325
360
|
// a BACKLOG — rule-eligible learnings minus those already materialized
|
|
326
|
-
// in the sidecar or `.claude/rules/`
|
|
361
|
+
// in the sidecar, the verdict ledger (#1710) or `.claude/rules/`
|
|
362
|
+
// (`countReconcileBacklog`, #1380;
|
|
327
363
|
// before #1380 it counted all eligible ones) — never on a date — so
|
|
328
364
|
// printing `lastRunAt` here put today's date next to the word "due" and
|
|
329
365
|
// read as "last run today and already due again". Measured 2026-09-09
|
|
@@ -339,10 +375,9 @@ export async function computeMaintenanceDue(opts = {}) {
|
|
|
339
375
|
|
|
340
376
|
// --- sweep (S2) ----------------------------------------------------------
|
|
341
377
|
try {
|
|
342
|
-
const metrics = path.join(repoRoot, '.orchestrator', 'metrics');
|
|
343
378
|
const res = await sweepExpiredLearnings({
|
|
344
|
-
filePath:
|
|
345
|
-
archivePath:
|
|
379
|
+
filePath: resolveLedgerPath('.orchestrator/metrics/learnings.jsonl', repoRoot),
|
|
380
|
+
archivePath: resolveLedgerPath('.orchestrator/metrics/learnings-archive.jsonl', repoRoot),
|
|
346
381
|
now: opts.now,
|
|
347
382
|
dryRun: true,
|
|
348
383
|
});
|
|
@@ -426,11 +461,27 @@ export async function computeMaintenanceDue(opts = {}) {
|
|
|
426
461
|
// finding. The sweep (#1377) is the mechanical half — prose out, the
|
|
427
462
|
// provenance pair kept as `markers only` — and an operator who is told
|
|
428
463
|
// only which files expired has to rediscover that the command exists.
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
)
|
|
464
|
+
const unsweepable = await unsweepableRuleFiles(repoRoot, now);
|
|
465
|
+
const sweepable = expiring.filter((r) => !unsweepable.has(r.file));
|
|
466
|
+
const stuck = expiring.filter((r) => unsweepable.has(r.file));
|
|
467
|
+
const parts = [];
|
|
468
|
+
if (sweepable.length > 0) {
|
|
469
|
+
parts.push(
|
|
470
|
+
`${sweepable.map((r) => `${r.file} ${r.expiresAt}`).join(', ')} → ` +
|
|
471
|
+
'node scripts/sweep-expired-rules.mjs (dry-run first)',
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
if (stuck.length > 0) {
|
|
475
|
+
// The sweep resolves every entry's date through its learning in the
|
|
476
|
+
// HOST-LOCAL learnings store; with none of a file's ids there it keeps
|
|
477
|
+
// the file whole, so recommending the command would be a no-op.
|
|
478
|
+
parts.push(
|
|
479
|
+
`${stuck.map((r) => `${r.file} ${r.expiresAt}`).join(', ')} → not sweepable on this host ` +
|
|
480
|
+
'(none of their learning ids are in .orchestrator/metrics/learnings.jsonl), ' +
|
|
481
|
+
'sweep on the host that holds them',
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
markDue('generated-rules-expiring', parts.join('; '));
|
|
434
485
|
}
|
|
435
486
|
}
|
|
436
487
|
} catch {
|
|
@@ -28,6 +28,8 @@ import { readDreamSignals } from './auto-dream.mjs';
|
|
|
28
28
|
import { readPeerCards } from './peer-cards/reader.mjs';
|
|
29
29
|
import { surfaceTopN, decayOptsFromConfig } from './learnings/surface.mjs';
|
|
30
30
|
import { countSessionsInJsonl } from './sessions-canonical.mjs';
|
|
31
|
+
import { resolveEventsLedgerRoot } from './platform.mjs';
|
|
32
|
+
import { resolveLedgerPath } from './ledger-path.mjs';
|
|
31
33
|
|
|
32
34
|
// ---------------------------------------------------------------------------
|
|
33
35
|
// Constants
|
|
@@ -346,7 +348,7 @@ export async function readBannerInputs({ repoRoot, memoryDir, learningsPath, now
|
|
|
346
348
|
: resolveMemoryDir(repoRoot);
|
|
347
349
|
const learningsFile = typeof learningsPath === 'string' && learningsPath.length > 0
|
|
348
350
|
? learningsPath
|
|
349
|
-
:
|
|
351
|
+
: resolveLedgerPath('.orchestrator/metrics/learnings.jsonl', repoRoot);
|
|
350
352
|
const clock = now instanceof Date ? now : new Date();
|
|
351
353
|
|
|
352
354
|
// --- Top learnings ---------------------------------------------------------
|
|
@@ -385,11 +387,8 @@ export async function readBannerInputs({ repoRoot, memoryDir, learningsPath, now
|
|
|
385
387
|
try {
|
|
386
388
|
const signals = await readDreamSignals({ repoRoot, memoryDir: memDir });
|
|
387
389
|
const memoryFiles = await countMemoryFiles(memDir);
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
// computed the same way, so we re-derive locally to keep this dependency
|
|
391
|
-
// surface lean and explicit.
|
|
392
|
-
const sessionsPath = path.join(repoRoot, '.orchestrator', 'metrics', 'sessions.jsonl');
|
|
390
|
+
// Use the same main-checkout ledger as the cleanup signals above.
|
|
391
|
+
const sessionsPath = path.join(resolveEventsLedgerRoot(repoRoot), '.orchestrator', 'metrics', 'sessions.jsonl');
|
|
393
392
|
const sessionsEver = await countSessionIdentities(sessionsPath);
|
|
394
393
|
|
|
395
394
|
let daysSinceCleanup = null;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { readFile, readdir } from 'node:fs/promises';
|
|
19
19
|
import { existsSync } from 'node:fs';
|
|
20
20
|
import path from 'node:path';
|
|
21
|
+
import { resolveLedgerPath } from '../ledger-path.mjs';
|
|
21
22
|
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
23
24
|
// Path helpers
|
|
@@ -30,7 +31,9 @@ import path from 'node:path';
|
|
|
30
31
|
* @returns {string}
|
|
31
32
|
*/
|
|
32
33
|
function proposalsJsonlPath(repoRoot) {
|
|
33
|
-
|
|
34
|
+
// #1722: same root as store.mjs — a linked worktree of a repo that ignores
|
|
35
|
+
// the queue reads the main checkout's copy.
|
|
36
|
+
return resolveLedgerPath('.orchestrator/metrics/proposals.jsonl', repoRoot);
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
/**
|
|
@@ -40,7 +43,7 @@ function proposalsJsonlPath(repoRoot) {
|
|
|
40
43
|
* @returns {string}
|
|
41
44
|
*/
|
|
42
45
|
function metricsDir(repoRoot) {
|
|
43
|
-
return path.
|
|
46
|
+
return path.dirname(proposalsJsonlPath(repoRoot));
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
// ---------------------------------------------------------------------------
|
|
@@ -47,6 +47,7 @@ import path from 'node:path';
|
|
|
47
47
|
import { appendLearning } from '../learnings/io.mjs';
|
|
48
48
|
import { validatePathInsideProject } from '../path-utils.mjs';
|
|
49
49
|
import { atomicWriteWithBackup } from '../io.mjs';
|
|
50
|
+
import { resolveLedgerRoot } from '../ledger-path.mjs';
|
|
50
51
|
|
|
51
52
|
// ---------------------------------------------------------------------------
|
|
52
53
|
// Path constants (relative to repoRoot)
|
|
@@ -219,7 +220,7 @@ export async function writeApproved({ approved, repoRoot, sessionId, ...rest })
|
|
|
219
220
|
return { written: 0, errors: [] };
|
|
220
221
|
}
|
|
221
222
|
|
|
222
|
-
const learningsResult = validateSinkTarget(LEARNINGS_REL, repoRoot);
|
|
223
|
+
const learningsResult = validateSinkTarget(LEARNINGS_REL, resolveLedgerRoot(LEARNINGS_REL, repoRoot));
|
|
223
224
|
if (!learningsResult.ok) {
|
|
224
225
|
return { written: 0, errors: [`path-safety: ${learningsResult.reason} (${LEARNINGS_REL})`] };
|
|
225
226
|
}
|
|
@@ -262,7 +263,8 @@ export async function archiveRejected({ rejected, repoRoot, reason }) {
|
|
|
262
263
|
return { archived: 0, errors: [] };
|
|
263
264
|
}
|
|
264
265
|
|
|
265
|
-
|
|
266
|
+
// The rejected log sits beside the queue it records (#1722): same mapped root.
|
|
267
|
+
const rejectedResult = validateSinkTarget(REJECTED_LOG_REL, resolveLedgerRoot(PROPOSALS_REL, repoRoot));
|
|
266
268
|
if (!rejectedResult.ok) {
|
|
267
269
|
return { archived: 0, errors: [`path-safety: ${rejectedResult.reason} (${REJECTED_LOG_REL})`] };
|
|
268
270
|
}
|
|
@@ -346,7 +348,7 @@ export async function archiveRejected({ rejected, repoRoot, reason }) {
|
|
|
346
348
|
* @returns {Promise<{ cleared: boolean, summariesCleared: number, reason?: string }>}
|
|
347
349
|
*/
|
|
348
350
|
export async function clearProposalsJsonl({ repoRoot }) {
|
|
349
|
-
const proposalsResult = validateSinkTarget(PROPOSALS_REL, repoRoot);
|
|
351
|
+
const proposalsResult = validateSinkTarget(PROPOSALS_REL, resolveLedgerRoot(PROPOSALS_REL, repoRoot));
|
|
350
352
|
if (!proposalsResult.ok) {
|
|
351
353
|
return { cleared: false, summariesCleared: 0, reason: `path-safety: ${proposalsResult.reason}` };
|
|
352
354
|
}
|
|
@@ -360,7 +362,8 @@ export async function clearProposalsJsonl({ repoRoot }) {
|
|
|
360
362
|
try {
|
|
361
363
|
const preClearContent = readFileSync(proposalsPath, 'utf8');
|
|
362
364
|
if (preClearContent.length > 0) {
|
|
363
|
-
|
|
365
|
+
// Archive and queue never sit in different checkouts (#1722).
|
|
366
|
+
const archiveResult = validateSinkTarget(PROPOSALS_ARCHIVE_REL, resolveLedgerRoot(PROPOSALS_REL, repoRoot));
|
|
364
367
|
if (!archiveResult.ok) {
|
|
365
368
|
return { cleared: false, summariesCleared: 0, reason: `path-safety: ${archiveResult.reason}` };
|
|
366
369
|
}
|
|
@@ -24,6 +24,7 @@ import { serializeProposal } from './schema.mjs';
|
|
|
24
24
|
import { validatePathInsideProject } from '../path-utils.mjs';
|
|
25
25
|
import { tryAcquireFileLock, releaseFileLock } from '../file-lock.mjs';
|
|
26
26
|
import { writeJsonAtomicSync } from '../io.mjs';
|
|
27
|
+
import { resolveLedgerRoot } from '../ledger-path.mjs';
|
|
27
28
|
|
|
28
29
|
// ---------------------------------------------------------------------------
|
|
29
30
|
// Constants
|
|
@@ -40,6 +41,19 @@ const DEFAULT_CONFIDENCE_FLOOR = 0.5;
|
|
|
40
41
|
const DEFAULT_LOCK_TIMEOUT_MS = 1000;
|
|
41
42
|
const LOCK_POLL_MS = 50;
|
|
42
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Root of the whole proposals store (queue, overflow, lock, summaries). One
|
|
46
|
+
* root for all of them, decided by the queue ledger: the lock and the summary
|
|
47
|
+
* files are no `*.jsonl`, so asking `git check-ignore` per file could split
|
|
48
|
+
* the lock from the ledger it guards.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} repoRoot
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
function storeRootFor(repoRoot) {
|
|
54
|
+
return resolveLedgerRoot(PROPOSALS_JSONL, repoRoot);
|
|
55
|
+
}
|
|
56
|
+
|
|
43
57
|
// ---------------------------------------------------------------------------
|
|
44
58
|
// Internal helpers
|
|
45
59
|
// ---------------------------------------------------------------------------
|
|
@@ -160,7 +174,7 @@ function ensureDir(filePath) {
|
|
|
160
174
|
* @returns {string}
|
|
161
175
|
*/
|
|
162
176
|
function lockPathFor(repoRoot) {
|
|
163
|
-
return safePath(repoRoot, PROPOSALS_LOCK);
|
|
177
|
+
return safePath(storeRootFor(repoRoot), PROPOSALS_LOCK);
|
|
164
178
|
}
|
|
165
179
|
|
|
166
180
|
/**
|
|
@@ -169,7 +183,7 @@ function lockPathFor(repoRoot) {
|
|
|
169
183
|
* @returns {string}
|
|
170
184
|
*/
|
|
171
185
|
function jsonlPathFor(repoRoot) {
|
|
172
|
-
return safePath(repoRoot, PROPOSALS_JSONL);
|
|
186
|
+
return safePath(storeRootFor(repoRoot), PROPOSALS_JSONL);
|
|
173
187
|
}
|
|
174
188
|
|
|
175
189
|
/**
|
|
@@ -178,11 +192,14 @@ function jsonlPathFor(repoRoot) {
|
|
|
178
192
|
* @returns {string}
|
|
179
193
|
*/
|
|
180
194
|
function overflowPathFor(repoRoot) {
|
|
181
|
-
return safePath(repoRoot, PROPOSALS_OVERFLOW_JSONL);
|
|
195
|
+
return safePath(storeRootFor(repoRoot), PROPOSALS_OVERFLOW_JSONL);
|
|
182
196
|
}
|
|
183
197
|
|
|
184
198
|
/** Flush a directory entry before depending on its persistence. */
|
|
185
199
|
function syncOverflowDirectory(directory) {
|
|
200
|
+
// Windows rejects fsync on a directory handle (EPERM); NTFS metadata
|
|
201
|
+
// journaling already covers the directory entry there.
|
|
202
|
+
if (process.platform === 'win32') return;
|
|
186
203
|
const fd = fs.openSync(directory, 'r');
|
|
187
204
|
try {
|
|
188
205
|
fs.fsyncSync(fd);
|
|
@@ -294,7 +311,7 @@ function appendOverflow(repoRoot, overflowPath, line) {
|
|
|
294
311
|
fs.appendFileSync(overflowPath, lineBytes);
|
|
295
312
|
return;
|
|
296
313
|
}
|
|
297
|
-
const root = fs.realpathSync(repoRoot);
|
|
314
|
+
const root = fs.realpathSync(storeRootFor(repoRoot));
|
|
298
315
|
const directory = path.dirname(overflowPath);
|
|
299
316
|
const token = randomUUID();
|
|
300
317
|
const archive = safePath(root, path.relative(root, `${overflowPath}.archive-${token}.jsonl`));
|
|
@@ -332,7 +349,7 @@ function summaryPathFor(repoRoot, waveId) {
|
|
|
332
349
|
if (!/^[A-Za-z0-9_-]+$/.test(waveId)) {
|
|
333
350
|
throw new TypeError(`store.mjs: waveId contains invalid characters: ${waveId}`);
|
|
334
351
|
}
|
|
335
|
-
return safePath(repoRoot, `${PROPOSALS_SUMMARY_PREFIX}${waveId}.json`);
|
|
352
|
+
return safePath(storeRootFor(repoRoot), `${PROPOSALS_SUMMARY_PREFIX}${waveId}.json`);
|
|
336
353
|
}
|
|
337
354
|
|
|
338
355
|
/**
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
|
|
34
34
|
import { appendLearning } from './learnings.mjs';
|
|
35
35
|
import { isValidMode } from './recommendations-v0.mjs';
|
|
36
|
+
import { resolveLedgerPath } from './ledger-path.mjs';
|
|
36
37
|
|
|
37
38
|
export const LEARNING_TYPE = 'mode-selector-accuracy';
|
|
38
39
|
export const INITIAL_CONFIDENCE = 0.5;
|
|
@@ -66,7 +67,7 @@ export async function recordAccuracy(opts) {
|
|
|
66
67
|
const recommended = opts?.recommended;
|
|
67
68
|
const chosen = opts?.chosen;
|
|
68
69
|
const sessionId = opts?.sessionId;
|
|
69
|
-
const filePath = opts?.filePath || '.orchestrator/metrics/learnings.jsonl';
|
|
70
|
+
const filePath = opts?.filePath || resolveLedgerPath('.orchestrator/metrics/learnings.jsonl', process.cwd());
|
|
70
71
|
|
|
71
72
|
// No-op when the selector did not render a recommendation (banner suppressed,
|
|
72
73
|
// confidence 0.0, etc). Caller passes recommended=null in that case.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* no-follow-open.mjs — open a regular file for reading without following a
|
|
3
|
+
* symlink in the final path component, on every platform (#1744).
|
|
4
|
+
*
|
|
5
|
+
* Node leaves `fs.constants.O_NOFOLLOW` and `O_NONBLOCK` undefined on win32, so
|
|
6
|
+
* callers that gated their read on those flags silently skipped it there.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { constants as fsConstants, promises as fsPromises } from 'node:fs';
|
|
10
|
+
|
|
11
|
+
const codedError = (message, code) => Object.assign(new Error(message), { code });
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Open `target` read-only without following a final-component symlink.
|
|
15
|
+
*
|
|
16
|
+
* - POSIX (`O_NOFOLLOW` and `O_NONBLOCK` both defined): one
|
|
17
|
+
* `open(O_RDONLY | O_NOFOLLOW | O_NONBLOCK)`; a symlink fails with the
|
|
18
|
+
* kernel's `ELOOP`. Callers keep doing their own stat checks on the handle.
|
|
19
|
+
* - Fallback (either flag missing, i.e. win32): `lstat` first — a symlink
|
|
20
|
+
* (Node reports Windows junctions as symlinks too) throws `code: 'ELOOP'`, a
|
|
21
|
+
* non-regular file throws `code: 'ERR_NOT_REGULAR_FILE'`; then `open('r')`
|
|
22
|
+
* and compare the handle's identity with the lstat. A non-regular file or a
|
|
23
|
+
* different `dev`/`ino` closes the handle and throws `code: 'ERR_FILE_CHANGED'`
|
|
24
|
+
* (the path was swapped between check and open). Bigint stats, because
|
|
25
|
+
* Windows file IDs can exceed 2^53.
|
|
26
|
+
*
|
|
27
|
+
* BV-004 ceiling: the fallback still has a window between lstat and open. On
|
|
28
|
+
* win32 there are no FIFOs in the filesystem namespace, so the open cannot
|
|
29
|
+
* block, and the post-open identity check catches a swap. Revisit when a POSIX
|
|
30
|
+
* platform lacks O_NONBLOCK, or a caller must read a FIFO-capable location on
|
|
31
|
+
* such a platform.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} target
|
|
34
|
+
* @param {{ constants?: Record<string, number|undefined>, fsImpl?: typeof fsPromises }} [options]
|
|
35
|
+
* Injection exists only so the fallback branch is testable on POSIX CI;
|
|
36
|
+
* production callers pass nothing.
|
|
37
|
+
* @returns {Promise<import('node:fs/promises').FileHandle>} caller closes it
|
|
38
|
+
*/
|
|
39
|
+
export async function openNoFollow(target, { constants = fsConstants, fsImpl = fsPromises } = {}) {
|
|
40
|
+
if (typeof constants.O_NOFOLLOW === 'number' && typeof constants.O_NONBLOCK === 'number') {
|
|
41
|
+
return fsImpl.open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
42
|
+
}
|
|
43
|
+
const before = await fsImpl.lstat(target, { bigint: true });
|
|
44
|
+
if (before.isSymbolicLink()) throw codedError(`${target} is a symbolic link`, 'ELOOP');
|
|
45
|
+
if (!before.isFile()) throw codedError(`${target} is not a regular file`, 'ERR_NOT_REGULAR_FILE');
|
|
46
|
+
const handle = await fsImpl.open(target, 'r');
|
|
47
|
+
try {
|
|
48
|
+
const after = await handle.stat({ bigint: true });
|
|
49
|
+
if (!after.isFile() || after.dev !== before.dev || after.ino !== before.ino) {
|
|
50
|
+
throw codedError(`${target} changed between check and open`, 'ERR_FILE_CHANGED');
|
|
51
|
+
}
|
|
52
|
+
} catch (err) {
|
|
53
|
+
await handle.close();
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
return handle;
|
|
57
|
+
}
|
|
@@ -9,7 +9,8 @@ import path from 'node:path';
|
|
|
9
9
|
|
|
10
10
|
import { replaceRegularFile } from '../process-group.mjs';
|
|
11
11
|
import { readTailWindow } from '../tail-window.mjs';
|
|
12
|
-
import { REAPER_DEFAULTS
|
|
12
|
+
import { REAPER_DEFAULTS } from './defaults.mjs';
|
|
13
|
+
import { resolveLedgerPath } from '../ledger-path.mjs';
|
|
13
14
|
import { NOT_A_REGULAR_FILE, appendNoFollow } from './no-follow-append.mjs';
|
|
14
15
|
|
|
15
16
|
/** Relative path of the JSONL kill audit (B5). Gitignored via `.gitignore:55`
|
|
@@ -67,7 +68,8 @@ function isRateDecision(record) {
|
|
|
67
68
|
/** Absolute path of the kill audit for a repo. One constant per path.
|
|
68
69
|
* @param {string} repoRoot @returns {string} */
|
|
69
70
|
export function auditPath(repoRoot) {
|
|
70
|
-
|
|
71
|
+
// #1722: a linked worktree of a repo that ignores the audit maps to the main checkout.
|
|
72
|
+
return resolveLedgerPath(REAPER_AUDIT_RELPATH, repoRoot);
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
/**
|