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
package/scripts/lib/common.mjs
CHANGED
|
@@ -141,11 +141,12 @@ export async function writeJson(filePath, obj) {
|
|
|
141
141
|
* PIPE_BUF (4 KiB).
|
|
142
142
|
* @param {string} filePath
|
|
143
143
|
* @param {unknown} obj
|
|
144
|
+
* @param {{ mode?: number }} [opts] — file mode on creation (e.g. 0o600 for host-private logs, #1632)
|
|
144
145
|
* @returns {Promise<void>}
|
|
145
146
|
*/
|
|
146
|
-
export async function appendJsonl(filePath, obj) {
|
|
147
|
+
export async function appendJsonl(filePath, obj, { mode } = {}) {
|
|
147
148
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
148
|
-
await fs.appendFile(filePath, JSON.stringify(obj) + '\n', 'utf8');
|
|
149
|
+
await fs.appendFile(filePath, JSON.stringify(obj) + '\n', mode === undefined ? 'utf8' : { encoding: 'utf8', mode });
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
// ---------------------------------------------------------------------------
|
|
@@ -16,21 +16,88 @@
|
|
|
16
16
|
// here). We deliberately do NOT import `parseSessionConfig` — calling it from
|
|
17
17
|
// `getCrossRepoProjects` would re-run `_parseCrossRepo` (double-parse). By calling
|
|
18
18
|
// `_parseCrossRepo` directly on the raw content we parse exactly once. (issue #664)
|
|
19
|
+
import { statSync } from 'node:fs';
|
|
19
20
|
import { homedir } from 'node:os';
|
|
20
|
-
import { join } from 'node:path';
|
|
21
|
+
import { isAbsolute, join } from 'node:path';
|
|
22
|
+
import { expandTilde } from '../common.mjs';
|
|
21
23
|
import { readConfigFile } from './io.mjs';
|
|
24
|
+
import { loadHostPaths, resolveHostPath } from './host-paths.mjs';
|
|
22
25
|
import { matchBlockHeader } from './block-header.mjs';
|
|
23
26
|
import { preprocessBlockLines } from './block-preprocess.mjs';
|
|
24
27
|
|
|
25
28
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
29
|
+
* Expand a leading `~` / `~/` to the home directory and accept the result only
|
|
30
|
+
* when it is absolute. A relative value would otherwise resolve against the
|
|
31
|
+
* process cwd — a different root per caller — so it is rejected instead.
|
|
32
|
+
* An existing non-directory (a file) is rejected too (#1567): every sweep under
|
|
33
|
+
* it would find nothing. A path that does not exist yet is accepted, like the
|
|
34
|
+
* ~/Projects default — callers guard with existsSync, and falling back from an
|
|
35
|
+
* unmounted volume to ~/Projects would widen the confinement root.
|
|
28
36
|
*
|
|
29
|
-
* @
|
|
37
|
+
* @param {string} raw — non-blank value from one precedence tier
|
|
38
|
+
* @param {string} source — tier name for the WARN (`env SO_PROJECTS_ROOT` / `owner.yaml paths.projects-root`)
|
|
39
|
+
* @returns {string|null} absolute path, or null when rejected
|
|
30
40
|
*/
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
41
|
+
function expandProjectsRoot(raw, source) {
|
|
42
|
+
const expanded = expandTilde(raw.trim());
|
|
43
|
+
if (!isAbsolute(expanded)) {
|
|
44
|
+
process.stderr.write(
|
|
45
|
+
`cross-repo: ignoring ${source}=${JSON.stringify(raw)} — not an absolute path after ~ expansion; falling back\n`,
|
|
46
|
+
);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
let stat;
|
|
50
|
+
try {
|
|
51
|
+
stat = statSync(expanded, { throwIfNoEntry: false });
|
|
52
|
+
} catch {
|
|
53
|
+
stat = undefined; // unreadable (EACCES/ENOTDIR): treated like a missing path — never throw here
|
|
54
|
+
}
|
|
55
|
+
if (stat && !stat.isDirectory()) {
|
|
56
|
+
process.stderr.write(
|
|
57
|
+
`cross-repo: ignoring ${source}=${JSON.stringify(raw)} — exists but is not a directory; falling back\n`,
|
|
58
|
+
);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return expanded;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the cross-repo confinement root — the directory host-wide sweeps scan
|
|
66
|
+
* and outside which `cross-repo.projects` entries are dropped (#1562).
|
|
67
|
+
*
|
|
68
|
+
* Precedence (highest first):
|
|
69
|
+
* 1. env CROSS_REPO_CONFINEMENT_ROOT — test-only, never set in production;
|
|
70
|
+
* returned verbatim when non-blank (no ~ expansion, no absolute check).
|
|
71
|
+
* 2. env SO_PROJECTS_ROOT — host-local, via resolveHostPath.
|
|
72
|
+
* 3. owner.yaml paths.projects-root — host-local, via resolveHostPath.
|
|
73
|
+
* 4. ~/Projects — default.
|
|
74
|
+
* Tiers 2 and 3 expand a leading `~`; a value that is still not absolute is
|
|
75
|
+
* ignored with one stderr WARN and the next tier is tried. owner.yaml is read
|
|
76
|
+
* only when tiers 1 and 2 yield nothing usable.
|
|
77
|
+
*
|
|
78
|
+
* @param {{ env?: Record<string, string|undefined>, ownerConfig?: object }} [ctx]
|
|
79
|
+
* injection seam for tests, mirroring resolveHostPath; `ownerConfig` omitted →
|
|
80
|
+
* owner.yaml is loaded from disk via loadHostPaths()
|
|
81
|
+
* @returns {string} confinement root
|
|
82
|
+
*/
|
|
83
|
+
export function getConfinementRoot({ env = process.env, ownerConfig } = {}) {
|
|
84
|
+
const v = env.CROSS_REPO_CONFINEMENT_ROOT;
|
|
85
|
+
if (v && v.trim()) return v;
|
|
86
|
+
|
|
87
|
+
const fromEnv = resolveHostPath('projects-root', undefined, { env, ownerConfig: undefined });
|
|
88
|
+
if (typeof fromEnv === 'string') {
|
|
89
|
+
const root = expandProjectsRoot(fromEnv, 'env SO_PROJECTS_ROOT');
|
|
90
|
+
if (root) return root;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const owner = ownerConfig ?? loadHostPaths({ env }).ownerConfig;
|
|
94
|
+
const fromOwner = resolveHostPath('projects-root', undefined, { env: {}, ownerConfig: owner });
|
|
95
|
+
if (typeof fromOwner === 'string') {
|
|
96
|
+
const root = expandProjectsRoot(fromOwner, 'owner.yaml paths.projects-root');
|
|
97
|
+
if (root) return root;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return join(homedir(), 'Projects');
|
|
34
101
|
}
|
|
35
102
|
|
|
36
103
|
// Allowlist: only characters that are safe in shell contexts and file paths.
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* committed default act as a fallback for unconfigured hosts.
|
|
9
9
|
*
|
|
10
10
|
* Precedence (highest first):
|
|
11
|
-
* 1. env-var (SO_VAULT_DIR / SO_BASELINE_PATH
|
|
11
|
+
* 1. env-var (SO_VAULT_DIR / SO_BASELINE_PATH / SO_NAMESPACE_MAP /
|
|
12
|
+
* SO_CONFIDENTIAL_NAMES_FILE / SO_PROJECTS_ROOT — see ENV_KEYS)
|
|
12
13
|
* 2. owner.yaml paths[key] (host-local, never committed — ~/.config/session-orchestrator/owner.yaml)
|
|
13
14
|
* 3. committed Session Config default (the value the committed CLAUDE.md —
|
|
14
15
|
* AGENTS.md on Codex CLI — produced)
|
|
@@ -40,6 +41,9 @@ const ENV_KEYS = /** @type {const} */ ({
|
|
|
40
41
|
'namespace-map-path': 'SO_NAMESPACE_MAP',
|
|
41
42
|
// #728a — host-local confidential customer/repo name list for CP11 leak scanning.
|
|
42
43
|
'confidential-names-file': 'SO_CONFIDENTIAL_NAMES_FILE',
|
|
44
|
+
// #1562 — host-local cross-repo confinement / scan root (default ~/Projects);
|
|
45
|
+
// consumed by getConfinementRoot() in config/cross-repo.mjs.
|
|
46
|
+
'projects-root': 'SO_PROJECTS_ROOT',
|
|
43
47
|
});
|
|
44
48
|
|
|
45
49
|
/**
|
|
@@ -86,7 +90,7 @@ export function loadHostPaths({ env = process.env, ownerLoader = loadOwnerConfig
|
|
|
86
90
|
* (fall through to the next tier). When no override is set, `committedDefault`
|
|
87
91
|
* passes through unchanged — including `null`/`undefined`, preserving back-compat.
|
|
88
92
|
*
|
|
89
|
-
* @param {'vault-dir'|'baseline-path'|'namespace-map-path'|'confidential-names-file'} key — logical path key
|
|
93
|
+
* @param {'vault-dir'|'baseline-path'|'namespace-map-path'|'confidential-names-file'|'projects-root'} key — logical path key
|
|
90
94
|
* @param {string|null|undefined} committedDefault — value the committed Session Config produced
|
|
91
95
|
* @param {{ env?: Record<string, string|undefined>, ownerConfig?: object }} [ctx] — from loadHostPaths()
|
|
92
96
|
* @returns {string|null|undefined} resolved value
|
|
@@ -18,7 +18,7 @@ import { preprocessBlockLinesNoDash } from './block-preprocess.mjs';
|
|
|
18
18
|
*
|
|
19
19
|
* Block shape:
|
|
20
20
|
* remote-hosts:
|
|
21
|
-
* - alias:
|
|
21
|
+
* - alias: build-host # required; becomes `-H <alias>` argv
|
|
22
22
|
* roles-allowed: [test, ui, perf] # subset of ALLOWED_REMOTE_ROLES
|
|
23
23
|
* repo-path: ~/Projects/Alice # optional
|
|
24
24
|
* claude-path: ~/.local/bin/claude # optional
|
|
@@ -54,7 +54,7 @@ export const ALLOWED_REMOTE_ROLES = Object.freeze(['test', 'ui', 'perf']);
|
|
|
54
54
|
// The first character is anchored separately from the rest because a charset
|
|
55
55
|
// cannot express a POSITION: an alias of `-H` passes any hyphen-inclusive
|
|
56
56
|
// allowlist and then reaches argv as an OPTION token, where the CLI swallows the
|
|
57
|
-
// operand behind it. Interior hyphens (`
|
|
57
|
+
// operand behind it. Interior hyphens (`build-host`) stay legal.
|
|
58
58
|
const SAFE_NAME_RE = /^[A-Za-z0-9._][A-Za-z0-9._-]*$/;
|
|
59
59
|
|
|
60
60
|
// `repo-path` / `claude-path` are filesystem paths handed to a remote shell —
|
|
@@ -345,8 +345,11 @@ export function _parseKV(lines) {
|
|
|
345
345
|
* and at any indent. Deliberately NOT imported from block-header.mjs: that
|
|
346
346
|
* matcher answers "does this line open the block for THIS key" for a known key,
|
|
347
347
|
* while here the key is unknown by construction.
|
|
348
|
+
*
|
|
349
|
+
* Group 1 captures the key, for `collectTopLevelKeyLines` below; `.test()`
|
|
350
|
+
* callers are unaffected by the group.
|
|
348
351
|
*/
|
|
349
|
-
const NESTED_BLOCK_HEADER_RE = /^\s*(?:-\s+)?(?:\*\*)?[a-zA-Z][a-zA-Z0-9_.-]
|
|
352
|
+
const NESTED_BLOCK_HEADER_RE = /^\s*(?:-\s+)?(?:\*\*)?([a-zA-Z][a-zA-Z0-9_.-]*):(?:\*\*)?\s*$/;
|
|
350
353
|
|
|
351
354
|
/** A YAML sequence element that is not a `key: value` pair (` - some-value`). */
|
|
352
355
|
const LIST_ITEM_RE = /^\s*-\s+\S/;
|
|
@@ -391,8 +394,67 @@ const COMMENT_RE = /^\s*(?:#|<!--|>)/;
|
|
|
391
394
|
export function collectUnparsableLines(content) {
|
|
392
395
|
if (typeof content !== 'string' || content === '') return [];
|
|
393
396
|
|
|
394
|
-
const lines = content.split(/\r?\n/);
|
|
395
397
|
const unparsable = [];
|
|
398
|
+
for (const { lineNo, line } of liveBlockLines(content)) {
|
|
399
|
+
if (_matchKVLine(line)) continue;
|
|
400
|
+
if (NESTED_BLOCK_HEADER_RE.test(line)) continue;
|
|
401
|
+
if (LIST_ITEM_RE.test(line)) continue;
|
|
402
|
+
|
|
403
|
+
unparsable.push({ line: lineNo, text: sanitizeReportText(line.trim()) });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return unparsable;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Collect the TOP-LEVEL keys the runtime reads from `## Session Config` — the
|
|
411
|
+
* local half of the unknown-key report in `scripts/parse-config.mjs` (#1562).
|
|
412
|
+
*
|
|
413
|
+
* Same block, same walk, same line acceptance as `collectUnparsableLines`
|
|
414
|
+
* (both iterate `liveBlockLines`), so a key inside a multi-line `<!-- -->`
|
|
415
|
+
* comment, a fence, or a `#` line is never collected. A line counts as a
|
|
416
|
+
* top-level key when it starts at column 0 and is either a key/value pair the
|
|
417
|
+
* reader accepts (`_matchKVLine`, both spellings) or a nested-block header
|
|
418
|
+
* (`vault-integration:`); indented lines are children of a nested block.
|
|
419
|
+
*
|
|
420
|
+
* Ceiling (deliberate): a compact YAML sequence written at column 0 under a
|
|
421
|
+
* nested header (`custom-phases:` then `- name: x` unindented) yields `name`
|
|
422
|
+
* here. `extractTopLevelKeys` in `template-keys.mjs` reads that line the
|
|
423
|
+
* same way, so drift-check Check 6 and this collector agree on it. Revisit if
|
|
424
|
+
* a fleet CLAUDE.md is ever measured writing that form.
|
|
425
|
+
*
|
|
426
|
+
* @param {string} content — full markdown document
|
|
427
|
+
* @returns {Array<{ line: number, key: string }>} 1-based document line numbers
|
|
428
|
+
* in document order (a repeated key appears once per line); empty when the
|
|
429
|
+
* block is absent or the content is not a string. Keys are passed through
|
|
430
|
+
* `sanitizeReportText` because the bold spelling admits any character but
|
|
431
|
+
* `*` and `:`, and the one consumer prints them on a terminal.
|
|
432
|
+
*/
|
|
433
|
+
export function collectTopLevelKeyLines(content) {
|
|
434
|
+
if (typeof content !== 'string' || content === '') return [];
|
|
435
|
+
|
|
436
|
+
const keys = [];
|
|
437
|
+
for (const { lineNo, line } of liveBlockLines(content)) {
|
|
438
|
+
if (/^\s/.test(line)) continue;
|
|
439
|
+
const kv = _matchKVLine(line);
|
|
440
|
+
const key = kv ? kv.key : NESTED_BLOCK_HEADER_RE.exec(line)?.[1];
|
|
441
|
+
if (key) keys.push({ line: lineNo, key: sanitizeReportText(key) });
|
|
442
|
+
}
|
|
443
|
+
return keys;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* The lines of the FIRST `## Session Config` block that can carry meaning:
|
|
448
|
+
* everything except blank lines, fences, `#`/`<!--`/`>` documentation lines and
|
|
449
|
+
* the inside of a multi-line HTML comment. The one walk both classifiers share,
|
|
450
|
+
* in the order `_extractConfigSection` applies the same skips — the shared
|
|
451
|
+
* `htmlCommentSkipper` call is what keeps the accept-sets identical.
|
|
452
|
+
*
|
|
453
|
+
* @param {string} content
|
|
454
|
+
* @returns {Generator<{ lineNo: number, line: string }>} 1-based line numbers
|
|
455
|
+
*/
|
|
456
|
+
function* liveBlockLines(content) {
|
|
457
|
+
const lines = content.split(/\r?\n/);
|
|
396
458
|
let inSection = false;
|
|
397
459
|
const skipHtmlComment = htmlCommentSkipper();
|
|
398
460
|
|
|
@@ -406,20 +468,12 @@ export function collectUnparsableLines(content) {
|
|
|
406
468
|
if (!inSection) continue;
|
|
407
469
|
if (/^## /.test(line)) break;
|
|
408
470
|
|
|
409
|
-
// A multi-line HTML comment: everything up to `-->` is documentation.
|
|
410
|
-
// Same call, same order, as `_extractConfigSection` — that shared call is
|
|
411
|
-
// what makes the two accept-sets identical rather than merely similar.
|
|
412
471
|
if (skipHtmlComment(line)) continue;
|
|
413
472
|
|
|
414
473
|
if (!line.trim()) continue;
|
|
415
474
|
if (isCodeFence(line)) continue;
|
|
416
475
|
if (COMMENT_RE.test(line)) continue;
|
|
417
|
-
if (_matchKVLine(line)) continue;
|
|
418
|
-
if (NESTED_BLOCK_HEADER_RE.test(line)) continue;
|
|
419
|
-
if (LIST_ITEM_RE.test(line)) continue;
|
|
420
476
|
|
|
421
|
-
|
|
477
|
+
yield { lineNo: i + 1, line };
|
|
422
478
|
}
|
|
423
|
-
|
|
424
|
-
return unparsable;
|
|
425
479
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* template-keys.mjs — the KNOWN top-level Session Config keys, read from
|
|
3
|
+
* `docs/session-config-template.md`.
|
|
4
|
+
*
|
|
5
|
+
* Two consumers read the same population and must not drift apart:
|
|
6
|
+
* - `skills/claude-md-drift-check/checker.mjs` Check 6 (session-config-parity)
|
|
7
|
+
* diffs a repo's local block against the template's blocks;
|
|
8
|
+
* - `scripts/parse-config.mjs` reports a local top-level key the template
|
|
9
|
+
* never documents — a typo'd key is otherwise absent from the KV map and
|
|
10
|
+
* every consumer silently applies its default (#1562).
|
|
11
|
+
*
|
|
12
|
+
* The template carries TWO literal `## Session Config` lines, both inside fenced
|
|
13
|
+
* ```yaml examples: the FIRST is the starter / minimal baseline (the mandatory
|
|
14
|
+
* keys), the LAST is the opt-in baseline (the full key catalog). Check 6
|
|
15
|
+
* classifies mandatory vs opt-in by exactly that order.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { isSessionConfigHeading, collectTopLevelKeyLines } from './section-extractor.mjs';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extract the YAML block under a `## Session Config` heading from a Markdown
|
|
22
|
+
* document. Tries fenced YAML first (```yaml ... ```) *within the matched
|
|
23
|
+
* heading's own body slice*, then falls back to the raw body up to the next
|
|
24
|
+
* `^## ` heading or EOF.
|
|
25
|
+
*
|
|
26
|
+
* `docs/session-config-template.md` carries TWO literal `## Session Config`
|
|
27
|
+
* lines — both live INSIDE fenced ```yaml example blocks: the starter / "Full
|
|
28
|
+
* minimal baseline" (7 mandatory keys) and the "Full opt-in baseline" (the
|
|
29
|
+
* complete key catalog, a strict superset of the minimal block). By default
|
|
30
|
+
* this function matches the FIRST occurrence (correct for a local CLAUDE.md /
|
|
31
|
+
* AGENTS.md instruction file, which has exactly one real heading). Pass
|
|
32
|
+
* `{ occurrence: 'last' }` to match the LAST occurrence instead — the template
|
|
33
|
+
* call site uses this to reach the opt-in baseline's full key set rather than
|
|
34
|
+
* silently stopping at the 7-key minimal block (issue #785).
|
|
35
|
+
*
|
|
36
|
+
* Returns { body: string, headingLine: number } or null when no heading found.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} content
|
|
39
|
+
* @param {{ occurrence?: 'first'|'last' }} [options]
|
|
40
|
+
* @returns {{ body: string, headingLine: number } | null}
|
|
41
|
+
*/
|
|
42
|
+
export function extractSessionConfigBlock(content, { occurrence = 'first' } = {}) {
|
|
43
|
+
const lines = content.split('\n');
|
|
44
|
+
const headingIdxs = [];
|
|
45
|
+
for (let i = 0; i < lines.length; i++) {
|
|
46
|
+
// SSOT predicate (#968). The previous local `/^##\s+Session Config\b/`
|
|
47
|
+
// was the loosest JS comparator in the repo: no end anchor and `\s+`
|
|
48
|
+
// instead of a single space, so it accepted `## Session Config` (two
|
|
49
|
+
// spaces) and `## Session Config Convention` — both of which the runtime
|
|
50
|
+
// parser rejects. The drift-check's whole job is to report on the block
|
|
51
|
+
// the runtime reads, so matching a heading the runtime cannot see made it
|
|
52
|
+
// audit a section that does not exist.
|
|
53
|
+
if (isSessionConfigHeading(lines[i])) headingIdxs.push(i);
|
|
54
|
+
}
|
|
55
|
+
if (headingIdxs.length === 0) return null;
|
|
56
|
+
|
|
57
|
+
const targetIdx = occurrence === 'last' ? headingIdxs[headingIdxs.length - 1] : headingIdxs[0];
|
|
58
|
+
const headingLine = targetIdx + 1; // 1-based line number for error reporting
|
|
59
|
+
|
|
60
|
+
// Raw body: from the line after the heading up to the next `## ` heading or EOF.
|
|
61
|
+
let endIdx = lines.length;
|
|
62
|
+
for (let i = targetIdx + 1; i < lines.length; i++) {
|
|
63
|
+
if (/^##\s+/.test(lines[i])) { endIdx = i; break; }
|
|
64
|
+
}
|
|
65
|
+
const rawBody = lines.slice(targetIdx + 1, endIdx).join('\n');
|
|
66
|
+
|
|
67
|
+
// Prefer a fenced YAML block found anywhere within the raw body slice
|
|
68
|
+
// (tolerates a blank line between the heading and the fence); fall back to
|
|
69
|
+
// the raw body itself when no fence is present.
|
|
70
|
+
const fenced = rawBody.match(/```ya?ml\r?\n([\s\S]*?)\r?\n```/);
|
|
71
|
+
const body = fenced ? fenced[1] : rawBody;
|
|
72
|
+
|
|
73
|
+
return { body, headingLine };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Extract top-level YAML keys from a YAML body. Only column-0 keys are
|
|
78
|
+
* collected (indented keys are children and ignored).
|
|
79
|
+
*
|
|
80
|
+
* Accepts four equivalent forms consumer CLAUDE.md files write a Session
|
|
81
|
+
* Config key in: bare `key:`, Markdown bullet `- key:`, bold `**key:**`
|
|
82
|
+
* (closing bold before the colon), and the bold-bullet consumer shape
|
|
83
|
+
* `- **key:** value` (bold wraps the key AND colon together, closing bold
|
|
84
|
+
* AFTER the colon). Baseline issue #60: the original bare-only regex
|
|
85
|
+
* extracted zero keys from bullet-form local files, so session-config-parity
|
|
86
|
+
* (drift-check Check 6) either false-positived on every mandatory key (local
|
|
87
|
+
* side reads as empty) or, when the TEMPLATE side also used bullet form,
|
|
88
|
+
* passed vacuously (both sides empty, so the diff was always empty). Sibling
|
|
89
|
+
* precedent: `scripts/lib/config/block-header.mjs` recognizes only header
|
|
90
|
+
* lines, not individual key-value pairs — this function is the per-key
|
|
91
|
+
* counterpart.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} body
|
|
94
|
+
* @returns {string[]} keys in document order (duplicates kept)
|
|
95
|
+
*/
|
|
96
|
+
export function extractTopLevelKeys(body) {
|
|
97
|
+
const keys = [];
|
|
98
|
+
const re = /^(?:-\s+)?(?:\*\*)?([A-Za-z][\w-]*)(?:\*\*)?:/gm;
|
|
99
|
+
let m;
|
|
100
|
+
while ((m = re.exec(body)) !== null) {
|
|
101
|
+
keys.push(m[1]);
|
|
102
|
+
}
|
|
103
|
+
return keys;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The known-key union of a Session Config template: every top-level key of its
|
|
108
|
+
* FIRST `## Session Config` block (starter / mandatory) and its LAST one
|
|
109
|
+
* (opt-in catalog).
|
|
110
|
+
*
|
|
111
|
+
* Returns null — "not measurable", never "no keys" — when the template does not
|
|
112
|
+
* carry two DISTINCT blocks. A single block is not a key catalog (the same
|
|
113
|
+
* guard drift-check Check 6 applies before it warns on local keys): diffing a
|
|
114
|
+
* repo against the seven starter keys alone would report every legitimately
|
|
115
|
+
* adopted opt-in key as unknown.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} content - full template document
|
|
118
|
+
* @returns {Set<string> | null}
|
|
119
|
+
*/
|
|
120
|
+
export function knownTemplateKeys(content) {
|
|
121
|
+
if (typeof content !== 'string') return null;
|
|
122
|
+
const first = extractSessionConfigBlock(content, { occurrence: 'first' });
|
|
123
|
+
const last = extractSessionConfigBlock(content, { occurrence: 'last' });
|
|
124
|
+
if (!first || !last || first.headingLine === last.headingLine) return null;
|
|
125
|
+
return new Set([...extractTopLevelKeys(first.body), ...extractTopLevelKeys(last.body)]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The template's starter (mandatory) keys a local instruction file does NOT
|
|
130
|
+
* carry in its `## Session Config` block — the input of `/bootstrap
|
|
131
|
+
* --retroactive` step 6 (#1567).
|
|
132
|
+
*
|
|
133
|
+
* Why not the validator: `parseSessionConfig()` fills every mandatory field
|
|
134
|
+
* with its default, so `validate-config` never sees an ABSENT key; in a pnpm
|
|
135
|
+
* repo without `test-command` the run silently uses `npm test`. Presence is a
|
|
136
|
+
* property of the FILE, so it is read from the file: local keys come from
|
|
137
|
+
* `collectTopLevelKeyLines()` (the lines the runtime reads, all spellings),
|
|
138
|
+
* the mandatory population from the template's FIRST block — the same
|
|
139
|
+
* population drift-check Check 6 calls mandatory.
|
|
140
|
+
*
|
|
141
|
+
* @param {string} localContent - the repo's CLAUDE.md / AGENTS.md
|
|
142
|
+
* @param {string} templateContent - `docs/session-config-template.md`
|
|
143
|
+
* @returns {string[] | null} missing keys in template order; null when the
|
|
144
|
+
* template has no `## Session Config` block ("not measurable", never "none
|
|
145
|
+
* missing")
|
|
146
|
+
*/
|
|
147
|
+
export function missingStarterKeys(localContent, templateContent) {
|
|
148
|
+
if (typeof templateContent !== 'string') return null;
|
|
149
|
+
const starter = extractSessionConfigBlock(templateContent, { occurrence: 'first' });
|
|
150
|
+
if (!starter) return null;
|
|
151
|
+
const present = new Set(collectTopLevelKeyLines(localContent).map(({ key }) => key));
|
|
152
|
+
return [...new Set(extractTopLevelKeys(starter.body))].filter((key) => !present.has(key));
|
|
153
|
+
}
|
|
@@ -347,6 +347,7 @@ export function _parseResourceThresholds(kv) {
|
|
|
347
347
|
// Unchanged at 5 — but now compared against LIVE PEER SESSIONS rather than
|
|
348
348
|
// the Claude process count. Same number, different denominator: measured
|
|
349
349
|
// firing rate drops from 93.6% to 4.2% (median processes:sessions = 6.0).
|
|
350
|
+
// That 4.2% did not hold: 67.4% M4 / 25.7% M5 re-measured 2026-10-17 (#1650).
|
|
350
351
|
'concurrent-sessions-warn': _coerceInteger(kv, 'concurrent-sessions-warn', 5),
|
|
351
352
|
'ssh-no-docker': _coerceBoolean(kv, 'ssh-no-docker', true),
|
|
352
353
|
};
|
package/scripts/lib/config.mjs
CHANGED
|
@@ -327,7 +327,7 @@ export function parseSessionConfig(mdContent, { hostPaths } = {}) {
|
|
|
327
327
|
// dispatched natively, exactly as before.
|
|
328
328
|
// impl: code-implementer → native Agent dispatch (unchanged)
|
|
329
329
|
// impl: cursor:composer-2.5 → foreign model over the Cursor channel
|
|
330
|
-
// test: ssh:
|
|
330
|
+
// test: ssh:build-host → same Claude, another host (#1160)
|
|
331
331
|
// An unknown prefix is a typo or a channel this build cannot route, and
|
|
332
332
|
// must fail loudly: silently accepting it would dispatch to an agent that
|
|
333
333
|
// does not exist, which surfaces only as an empty wave much later.
|
|
@@ -433,8 +433,8 @@ export function parseSessionConfig(mdContent, { hostPaths } = {}) {
|
|
|
433
433
|
const eventsRotation = _parseEventsRotation(mdContent);
|
|
434
434
|
|
|
435
435
|
// express-path: parsed from full content (standalone top-level block, #214/#1119).
|
|
436
|
-
// Documented in docs/session-config-template.md
|
|
437
|
-
// docs/session-config-reference.md
|
|
436
|
+
// Documented in docs/session-config-template.md § Express Path and
|
|
437
|
+
// docs/session-config-reference.md § Express Path (#214) since #214, but never in the key schema —
|
|
438
438
|
// measured 2026-08-23, a probe carrying `express-path:\n enabled: false` produced
|
|
439
439
|
// 88 keys and none of them was `express-path`. The key was dropped even when it
|
|
440
440
|
// stood in the file, so the documented default was prose rather than a value.
|
|
@@ -20,7 +20,7 @@ import { promises as fs } from 'node:fs';
|
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
import os from 'node:os';
|
|
22
22
|
|
|
23
|
-
import { $, nothrow } from 'zx';
|
|
23
|
+
import { $, nothrow } from './zx.mjs';
|
|
24
24
|
|
|
25
25
|
// Do not spam stdout/stderr with git command echoes.
|
|
26
26
|
$.verbose = false;
|
|
@@ -19,6 +19,7 @@ import { appendFile, readFile, mkdir } from 'node:fs/promises';
|
|
|
19
19
|
import { existsSync } from 'node:fs';
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
import { findProjectRoot } from '../common.mjs';
|
|
22
|
+
import { resolveLedgerPath } from '../ledger-path.mjs';
|
|
22
23
|
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
24
25
|
// Constants
|
|
@@ -56,7 +57,7 @@ export const DEFAULT_STATE_FILE = '.orchestrator/metrics/discovery-triage.jsonl'
|
|
|
56
57
|
function resolveStatePath(stateFilePath) {
|
|
57
58
|
const p = stateFilePath ?? DEFAULT_STATE_FILE;
|
|
58
59
|
if (path.isAbsolute(p)) return p;
|
|
59
|
-
return
|
|
60
|
+
return resolveLedgerPath(p, findProjectRoot());
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
/**
|
|
@@ -29,13 +29,10 @@
|
|
|
29
29
|
* @typedef {{ repoRoot: string, repoName: string, free: boolean, status: 'frei'|'in-progress'|'force-closed', heartbeat: string|null, sessionId: string|null }} Candidate
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
import path from 'node:path';
|
|
33
|
-
|
|
34
32
|
import { scanBacklog } from '../backlog-scan.mjs';
|
|
35
33
|
import { checkCiStatus as realCheckCiStatus } from '../ci-status-banner.mjs';
|
|
36
34
|
import { probe as realProbe, evaluate as realEvaluate, DEFAULT_RESOURCE_THRESHOLDS as CANONICAL_RESOURCE_THRESHOLDS } from '../resource-probe.mjs';
|
|
37
|
-
import { readCanonicalSessions } from '../sessions-canonical.mjs';
|
|
38
|
-
import { isRealSession } from '../session-schema/filters.mjs';
|
|
35
|
+
import { isMeasuredSession, readCanonicalSessions } from '../sessions-canonical.mjs';
|
|
39
36
|
|
|
40
37
|
/** Staleness cap (days). Beyond this, additional age does not raise the score. */
|
|
41
38
|
export const STALENESS_CAP_DAYS = 90;
|
|
@@ -184,13 +181,14 @@ async function defaultFetchPriority(repoRoot, nowMs) {
|
|
|
184
181
|
*/
|
|
185
182
|
async function defaultStaleDaysFor(repoRoot, nowMs) {
|
|
186
183
|
try {
|
|
187
|
-
const
|
|
188
|
-
const records = readCanonicalSessions({ filePath: file });
|
|
184
|
+
const records = readCanonicalSessions({ repoRoot });
|
|
189
185
|
if (records.length === 0) return STALENESS_CAP_DAYS;
|
|
190
186
|
|
|
191
187
|
let latestMs = null;
|
|
192
188
|
for (const rec of records) {
|
|
193
|
-
|
|
189
|
+
// Audit E1: a synthetic backfill stub (even a `status: 'completed'` one)
|
|
190
|
+
// must not make a neglected repo look freshly touched.
|
|
191
|
+
if (!isMeasuredSession(rec)) continue;
|
|
194
192
|
const iso = rec.completed_at || rec.started_at || null;
|
|
195
193
|
if (!iso || typeof iso !== 'string') continue;
|
|
196
194
|
const t = Date.parse(iso);
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
40
|
import { existsSync, statSync, readFileSync } from 'node:fs';
|
|
41
|
-
import { join } from 'node:path';
|
|
42
41
|
import { getProjectDir } from './platform.mjs';
|
|
42
|
+
import { resolveLedgerPath } from './ledger-path.mjs';
|
|
43
43
|
|
|
44
44
|
// Ceiling (BV-004): one process per session that invoked session-start
|
|
45
45
|
// (monitors/monitors.json `when: on-skill-invoke:session-start`), alive until
|
|
@@ -132,7 +132,7 @@ function fingerprint(absPath) {
|
|
|
132
132
|
async function watchLoop(intervalS) {
|
|
133
133
|
// #1517: the PROJECT root, never CLAUDE_PLUGIN_ROOT (the installed plugin
|
|
134
134
|
// directory, whose metrics are not this repo's).
|
|
135
|
-
const absPath =
|
|
135
|
+
const absPath = resolveLedgerPath(STATE_FILE_REL, getProjectDir());
|
|
136
136
|
|
|
137
137
|
// Startup event — confirms the watcher is alive even when nothing has
|
|
138
138
|
// happened yet. Without this, a "no events" stream is indistinguishable
|
|
@@ -931,8 +931,9 @@ export function evaluateSession(opts = {}) {
|
|
|
931
931
|
throw new Error('evaluateSession: model.id is required');
|
|
932
932
|
}
|
|
933
933
|
|
|
934
|
-
|
|
935
|
-
//
|
|
934
|
+
// #1514 point 1 / #1592 — the repo's ONE ledger pair (main checkout for a
|
|
935
|
+
// linked worktree); resolving only events left the session unresolvable.
|
|
936
|
+
const sessionsPath = path.join(resolveEventsLedgerRoot(metricsDir), 'sessions.jsonl');
|
|
936
937
|
const eventsPath = path.join(resolveEventsLedgerRoot(metricsDir), 'events.jsonl');
|
|
937
938
|
// #1209: sessions.jsonl is APPEND-ONLY (the same physical session can carry
|
|
938
939
|
// more than one line — crash-recovery re-appends, #1068 stub/supersede
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* 1. WHICH session? — resolveSession() implements the operator-approved
|
|
9
9
|
* cascade (Decision #2, revised #822): explicit id > newest record (source
|
|
10
10
|
* order) that is either status:'completed' or non-abandoned with completed
|
|
11
|
-
* work.
|
|
11
|
+
* work. The cascade uses only measured records and requires completion
|
|
12
|
+
* evidence; abandoned records remain ineligible for a completed-session eval.
|
|
12
13
|
*
|
|
13
14
|
* 2. WHOSE events? — quality_gate events in events.jsonl do not RELIABLY
|
|
14
15
|
* carry a session_id (measured 2026-10-05, this repo's events.jsonl:
|
|
@@ -26,6 +27,8 @@
|
|
|
26
27
|
* arrays. Determinism is load-bearing for the CLI `--verify` re-evaluation path.
|
|
27
28
|
*/
|
|
28
29
|
|
|
30
|
+
import { isMeasuredSession } from '../sessions-canonical.mjs';
|
|
31
|
+
|
|
29
32
|
/** Thrown by resolveSession when no eligible session can be selected. */
|
|
30
33
|
export class SessionResolutionError extends Error {
|
|
31
34
|
constructor(message) {
|
|
@@ -81,6 +84,7 @@ export function resolveSession(records, sessionId) {
|
|
|
81
84
|
for (let i = records.length - 1; i >= 0; i--) {
|
|
82
85
|
const r = records[i];
|
|
83
86
|
if (!isPlainObject(r)) continue;
|
|
87
|
+
if (!isMeasuredSession(r)) continue;
|
|
84
88
|
if (r.status === 'abandoned') continue;
|
|
85
89
|
if (r.status === 'completed') {
|
|
86
90
|
return { record: r, resolvedVia: 'cascade-completed' };
|
|
@@ -96,7 +100,7 @@ export function resolveSession(records, sessionId) {
|
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
throw new SessionResolutionError(
|
|
99
|
-
'no completed session found (all records
|
|
103
|
+
'no completed session found (all records unmeasured or without evidence of completed work)',
|
|
100
104
|
);
|
|
101
105
|
}
|
|
102
106
|
|
|
@@ -19,6 +19,7 @@ import { mkdirSync, appendFileSync } from 'node:fs';
|
|
|
19
19
|
import path from 'node:path';
|
|
20
20
|
import { validateEvalRecord, normalizeEvalRecord } from './schema.mjs';
|
|
21
21
|
import { readJsonlFile } from '../io.mjs';
|
|
22
|
+
import { resolveLedgerPath } from '../ledger-path.mjs';
|
|
22
23
|
|
|
23
24
|
/** Default repo-relative path for the eval journal. */
|
|
24
25
|
export const DEFAULT_EVAL_JSONL_PATH = '.orchestrator/metrics/eval.jsonl';
|
|
@@ -36,11 +37,11 @@ export const DEFAULT_EVAL_JSONL_PATH = '.orchestrator/metrics/eval.jsonl';
|
|
|
36
37
|
*
|
|
37
38
|
* @param {object} record — candidate session-eval record.
|
|
38
39
|
* @param {{ path?: string }} [opts] — target path (absolute preferred); defaults
|
|
39
|
-
* to DEFAULT_EVAL_JSONL_PATH (
|
|
40
|
+
* to `resolveLedgerPath(DEFAULT_EVAL_JSONL_PATH)` (#1722).
|
|
40
41
|
* @returns {{ ok: true, record: object, path: string }
|
|
41
42
|
* | { ok: false, reason: 'validation'|'fs-error', error: string }}
|
|
42
43
|
*/
|
|
43
|
-
export function appendEvalRecord(record, { path: filePath = DEFAULT_EVAL_JSONL_PATH } = {}) {
|
|
44
|
+
export function appendEvalRecord(record, { path: filePath = resolveLedgerPath(DEFAULT_EVAL_JSONL_PATH) } = {}) {
|
|
44
45
|
let validated;
|
|
45
46
|
try {
|
|
46
47
|
validated = validateEvalRecord(record);
|
|
@@ -71,7 +72,7 @@ export function appendEvalRecord(record, { path: filePath = DEFAULT_EVAL_JSONL_P
|
|
|
71
72
|
* @param {string} [filePath] — absolute path to the eval journal.
|
|
72
73
|
* @returns {object[]} normalized session-eval records, in source order.
|
|
73
74
|
*/
|
|
74
|
-
export function readEvalRecords(filePath = DEFAULT_EVAL_JSONL_PATH) {
|
|
75
|
+
export function readEvalRecords(filePath = resolveLedgerPath(DEFAULT_EVAL_JSONL_PATH)) {
|
|
75
76
|
const rows = readJsonlFile(filePath, { skipInvalid: true });
|
|
76
77
|
return rows.map((r) => normalizeEvalRecord(r));
|
|
77
78
|
}
|
|
@@ -109,8 +109,8 @@ const HEAD_BYTES = 64 * 1024;
|
|
|
109
109
|
* `rg -n "readEventsWithRotations\(|scanEventsBackwards\(|listEventSourcesNewestFirst\(" --glob '!tests/**' scripts hooks skills`
|
|
110
110
|
* → 6 call sites beside the definitions in `events.mjs` (and its internal
|
|
111
111
|
* `listEventSourcesNewestFirst` call); the seventh, `session-close-backfill`,
|
|
112
|
-
* joined with #1498
|
|
113
|
-
* join unlisted.
|
|
112
|
+
* joined with #1498, the eighth, `session-work-trees`, with #1659. A test
|
|
113
|
+
* re-runs that census, so a ninth reader cannot join unlisted.
|
|
114
114
|
*/
|
|
115
115
|
export const EVENTS_WINDOW_READERS = Object.freeze([
|
|
116
116
|
{ label: 'eval/engine', spec: './eval/engine.mjs' },
|
|
@@ -120,6 +120,7 @@ export const EVENTS_WINDOW_READERS = Object.freeze([
|
|
|
120
120
|
{ label: 'instruction-budget-guard', spec: './instruction-budget-guard.mjs' },
|
|
121
121
|
{ label: 'backfill-abandoned-sessions', spec: '../backfill-abandoned-sessions.mjs' },
|
|
122
122
|
{ label: 'session-close-backfill', spec: './session-close-backfill.mjs' },
|
|
123
|
+
{ label: 'session-work-trees', spec: './session-work-trees.mjs' },
|
|
123
124
|
]);
|
|
124
125
|
|
|
125
126
|
/**
|
|
@@ -99,6 +99,16 @@ export const ARCHIVE_NAME_RE =
|
|
|
99
99
|
/** Upper bound of the legacy `.1`..`.N` ring (`max-backups` is capped at 20). */
|
|
100
100
|
export const LEGACY_RING_MAX = 20;
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Per-session work-tree record (#1659): written by hooks/pre-bash-staging-fence.mjs
|
|
104
|
+
* BEFORE a `git commit` runs, payload `{ session_id, tree, start_ref }` — the raw
|
|
105
|
+
* session id, the realpath of the committing tree's toplevel, and that tree's
|
|
106
|
+
* full `HEAD` sha before the commit. Read by scripts/lib/session-work-trees.mjs
|
|
107
|
+
* (first record per tree wins). Lives in this zero-import module so the hook
|
|
108
|
+
* and the reader share the name without either importing the other.
|
|
109
|
+
*/
|
|
110
|
+
export const SESSION_WORK_TREE_EVENT = 'orchestrator.session.work_tree';
|
|
111
|
+
|
|
102
112
|
/** Prefix marking an orchestrator-owned event. */
|
|
103
113
|
export const ORCHESTRATOR_PREFIX = 'orchestrator.';
|
|
104
114
|
|