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
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
* 1. oom-kill — stop events with exitCode 137 or "Out of memory" marker
|
|
10
10
|
* 2. heartbeat-gap — registry sweep.log entries for the same host
|
|
11
11
|
* (session disappeared without a clean stop — freeze proxy)
|
|
12
|
-
* 3. concurrent-session-pressure — session events
|
|
13
|
-
* the
|
|
12
|
+
* 3. concurrent-session-pressure — session-start events inside a start wave:
|
|
13
|
+
* `recent_starts_count` (foreign starts in the last 5 min,
|
|
14
|
+
* emitted since #1650) >= startWaveMin. Re-aimed in #1654
|
|
15
|
+
* off `peer_count`, a standing state (see the detector).
|
|
14
16
|
* 4. disk-full — events with ENOSPC / "no space left" markers
|
|
15
17
|
* 5. thermal-throttle — sustained high CPU load + unexpected slow-wave marker
|
|
16
18
|
*
|
|
@@ -28,8 +30,14 @@
|
|
|
28
30
|
export const DEFAULT_THRESHOLDS = Object.freeze({
|
|
29
31
|
/** Minimum recurrence for a candidate to be emitted (same host_class). */
|
|
30
32
|
minOccurrences: 2,
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Minimum `recent_starts_count` (foreign session starts in the 5 min before
|
|
35
|
+
* this start) to count as concurrent-session pressure. Mirrors START_WAVE_MIN
|
|
36
|
+
* in hooks/on-session-start.mjs (#1650). Replaces `concurrentSessionsWarn`
|
|
37
|
+
* (#1654), which was compared against `peer_count`; a caller still passing
|
|
38
|
+
* that key is ignored on purpose rather than silently fed the old meaning.
|
|
39
|
+
*/
|
|
40
|
+
startWaveMin: 5,
|
|
33
41
|
/** CPU load percentage that marks a thermal-throttle signal. */
|
|
34
42
|
thermalCpuLoadPct: 85,
|
|
35
43
|
/** Heartbeat gap threshold in minutes above which a session is "frozen". */
|
|
@@ -93,15 +101,97 @@ function detectHeartbeatGap(sweepLogEntries, thresholdMin) {
|
|
|
93
101
|
}));
|
|
94
102
|
}
|
|
95
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Fallback wave window for start events that predate the producer writing
|
|
106
|
+
* `recent_starts_window_min` beside `recent_starts_count`. Mirrors the default
|
|
107
|
+
* of `countRecentStarts()` in scripts/lib/session-registry.mjs; the producer's
|
|
108
|
+
* own constant (START_WAVE_WINDOW_MIN in hooks/on-session-start.mjs) is
|
|
109
|
+
* module-private to a hook script and arrives per event instead, so the event
|
|
110
|
+
* field always wins over this value.
|
|
111
|
+
*/
|
|
112
|
+
const FALLBACK_START_WAVE_WINDOW_MIN = 5;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Start-wave detector (#1654). Keyed on `recent_starts_count`, NOT `peer_count`:
|
|
116
|
+
* `peer_count >= 5` held on 880/1305 = 67.4% of M4 and 255/991 = 25.7% of M5
|
|
117
|
+
* starts since 2026-08-24 (#1650) — a standing condition, so every learning
|
|
118
|
+
* derived from it was noise (HR-101). A start wave fired on 2.7% / 5.2%.
|
|
119
|
+
*
|
|
120
|
+
* Events without a numeric `recent_starts_count` (every pre-#1650 record) carry
|
|
121
|
+
* no value on this branch: they are skipped, never read as 0 and never as a hit.
|
|
122
|
+
* The signal name stays `concurrent-session-pressure` so existing learnings'
|
|
123
|
+
* subjects (`concurrent-session-pressure::<host_class>`) remain stable.
|
|
124
|
+
*
|
|
125
|
+
* One hit per WAVE, not per event (#1664): with >= 7 starts in one wave, the
|
|
126
|
+
* 6th and 7th start each see >= 5 foreign starts, so counting events let a
|
|
127
|
+
* single wave satisfy `minOccurrences: 2` on its own. See clusterStartWaves().
|
|
128
|
+
*/
|
|
96
129
|
function detectConcurrentSessionPressure(events, threshold) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
130
|
+
const qualifying = events.filter(
|
|
131
|
+
(e) =>
|
|
132
|
+
e.event === 'orchestrator.session.started' &&
|
|
133
|
+
typeof e.recent_starts_count === 'number' &&
|
|
134
|
+
e.recent_starts_count >= threshold,
|
|
135
|
+
);
|
|
136
|
+
return clusterStartWaves(qualifying).map((wave) => ({
|
|
137
|
+
signal: 'concurrent-session-pressure',
|
|
138
|
+
host_class: wave[0].host_class ?? null,
|
|
139
|
+
timestamp: wave[0].timestamp,
|
|
140
|
+
// Peak count of the wave: same `{ recent_starts_count }` sample shape as
|
|
141
|
+
// the per-event hits this replaces.
|
|
142
|
+
raw: { recent_starts_count: Math.max(...wave.map((e) => e.recent_starts_count)) },
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Cluster qualifying start events into waves, per `host_class` (events without
|
|
148
|
+
* a host share one `null` bucket; aggregateCandidates() drops those hits later).
|
|
149
|
+
* Chain rule: sorted by time, an event joins the current wave when it lies
|
|
150
|
+
* less than the window after the PREVIOUS qualifying event — the window being
|
|
151
|
+
* the span `recent_starts_count` itself looks back over (event field
|
|
152
|
+
* `recent_starts_window_min`, else FALLBACK_START_WAVE_WINDOW_MIN). Events with
|
|
153
|
+
* an unparseable timestamp cannot be placed in any wave and are DROPPED:
|
|
154
|
+
* counting each as its own wave would re-open the per-event over-count.
|
|
155
|
+
*
|
|
156
|
+
* Each wave's hit carries the wave's FIRST timestamp, so downstream
|
|
157
|
+
* `last_seen` means "start of the last wave", not "last qualifying start".
|
|
158
|
+
*
|
|
159
|
+
* Ceiling (BV-004): chaining is unbounded, so qualifying starts that keep
|
|
160
|
+
* arriving less than one window apart for an hour merge into ONE occurrence —
|
|
161
|
+
* a sustained-pressure stretch undercounts rather than overcounts. Revisit if
|
|
162
|
+
* the ledger shows a chained wave on one host spanning more than 30 min, which
|
|
163
|
+
* would mean sustained load, not a burst, and deserves its own count.
|
|
164
|
+
*
|
|
165
|
+
* @param {object[]} qualifying — start events already past the threshold
|
|
166
|
+
* @returns {object[][]} waves, each a non-empty array of events in time order
|
|
167
|
+
*/
|
|
168
|
+
function clusterStartWaves(qualifying) {
|
|
169
|
+
const byHost = new Map();
|
|
170
|
+
for (const e of qualifying) {
|
|
171
|
+
const key = e.host_class ?? null;
|
|
172
|
+
if (!byHost.has(key)) byHost.set(key, []);
|
|
173
|
+
byHost.get(key).push({ e, ms: Date.parse(e.timestamp) });
|
|
174
|
+
}
|
|
175
|
+
const waves = [];
|
|
176
|
+
for (const list of byHost.values()) {
|
|
177
|
+
const timed = list.filter((x) => Number.isFinite(x.ms)).sort((a, b) => a.ms - b.ms);
|
|
178
|
+
let current = null;
|
|
179
|
+
let prevMs = 0;
|
|
180
|
+
for (const x of timed) {
|
|
181
|
+
const windowMin =
|
|
182
|
+
typeof x.e.recent_starts_window_min === 'number' && x.e.recent_starts_window_min > 0
|
|
183
|
+
? x.e.recent_starts_window_min
|
|
184
|
+
: FALLBACK_START_WAVE_WINDOW_MIN;
|
|
185
|
+
if (current && x.ms - prevMs < windowMin * 60_000) {
|
|
186
|
+
current.push(x.e);
|
|
187
|
+
} else {
|
|
188
|
+
current = [x.e];
|
|
189
|
+
waves.push(current);
|
|
190
|
+
}
|
|
191
|
+
prevMs = x.ms;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return waves;
|
|
105
195
|
}
|
|
106
196
|
|
|
107
197
|
function detectDiskFull(events) {
|
|
@@ -210,7 +300,7 @@ export function detectHardwarePatterns({
|
|
|
210
300
|
const hits = [
|
|
211
301
|
...detectOomKill(events),
|
|
212
302
|
...detectHeartbeatGap(sweepLogEntries, t.heartbeatGapMinutes),
|
|
213
|
-
...detectConcurrentSessionPressure(events, t.
|
|
303
|
+
...detectConcurrentSessionPressure(events, t.startWaveMin),
|
|
214
304
|
...detectDiskFull(events),
|
|
215
305
|
...detectThermalThrottle(events, t.thermalCpuLoadPct),
|
|
216
306
|
];
|
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
* orchestrator-layout
|
|
6
6
|
*
|
|
7
7
|
* Stdlib + intra-repo helpers only (node:fs, node:path, ./helpers.mjs,
|
|
8
|
-
* ../../sessions-canonical.mjs).
|
|
8
|
+
* ../../platform.mjs, ../../sessions-canonical.mjs).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { existsSync } from 'node:fs';
|
|
12
12
|
import { join, relative, sep } from 'node:path';
|
|
13
13
|
|
|
14
14
|
import { parseFrontmatter, safeRead, parseJsonl, pass, fail } from './helpers.mjs';
|
|
15
|
+
import { resolveEventsLedgerRoot } from '../../platform.mjs';
|
|
16
|
+
import { resolveLedgerPath } from '../../ledger-path.mjs';
|
|
15
17
|
import { canonicalizeSessions } from '../../sessions-canonical.mjs';
|
|
16
18
|
|
|
17
19
|
export function runCategory1(root) {
|
|
@@ -54,8 +56,12 @@ export function runCategory1(root) {
|
|
|
54
56
|
|
|
55
57
|
// c1.2 sessions-jsonl-growth
|
|
56
58
|
{
|
|
57
|
-
|
|
59
|
+
// #1592: the repo's ONE sessions ledger, as c4.2 reads it — a linked
|
|
60
|
+
// worktree holds no ledger of its own since #1584, so `<root>/…` would
|
|
61
|
+
// report "missing" for a repo that records every session.
|
|
62
|
+
const p = join(resolveEventsLedgerRoot(root), '.orchestrator/metrics/sessions.jsonl');
|
|
58
63
|
const text = safeRead(p);
|
|
64
|
+
// ledger-root: display relPath for the finding, the read above is resolved
|
|
59
65
|
const relPath = '.orchestrator/metrics/sessions.jsonl';
|
|
60
66
|
if (!text) {
|
|
61
67
|
checks.push(fail('sessions-jsonl-growth', 3, relPath,
|
|
@@ -87,8 +93,9 @@ export function runCategory1(root) {
|
|
|
87
93
|
|
|
88
94
|
// c1.3 learnings-jsonl-nonempty
|
|
89
95
|
{
|
|
90
|
-
const p =
|
|
96
|
+
const p = resolveLedgerPath('.orchestrator/metrics/learnings.jsonl', root);
|
|
91
97
|
const text = safeRead(p);
|
|
98
|
+
// ledger-root: display relPath for the finding, the read above is resolved
|
|
92
99
|
const relPath = '.orchestrator/metrics/learnings.jsonl';
|
|
93
100
|
if (!text) {
|
|
94
101
|
checks.push(fail('learnings-jsonl-nonempty', 2, relPath,
|
|
@@ -115,9 +122,14 @@ export function runCategory1(root) {
|
|
|
115
122
|
'.orchestrator',
|
|
116
123
|
'.orchestrator/bootstrap.lock',
|
|
117
124
|
'.orchestrator/policy',
|
|
125
|
+
// ledger-root: existence check only, resolved under resolveEventsLedgerRoot() below
|
|
118
126
|
'.orchestrator/metrics',
|
|
119
127
|
];
|
|
120
|
-
|
|
128
|
+
// #1722: a linked worktree of a repo that ignores its ledgers has no
|
|
129
|
+
// metrics dir of its own — they live in the main checkout.
|
|
130
|
+
const presentPaths = requiredPaths.filter((rp) =>
|
|
131
|
+
existsSync(rp === '.orchestrator/metrics' ? join(resolveEventsLedgerRoot(root), rp) : join(root, rp)),
|
|
132
|
+
);
|
|
121
133
|
if (presentPaths.length === requiredPaths.length) {
|
|
122
134
|
checks.push(pass('orchestrator-layout', 2, 2, '.orchestrator/',
|
|
123
135
|
{ presentPaths },
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* vault-sync-validator, orphaned-session-lock
|
|
6
6
|
*
|
|
7
7
|
* Stdlib + intra-repo helpers only (node:fs, node:path, ./helpers.mjs,
|
|
8
|
-
* ../../
|
|
8
|
+
* ../../platform.mjs, ../../sessions-canonical.mjs,
|
|
9
9
|
* ../../config/section-extractor.mjs).
|
|
10
10
|
*/
|
|
11
11
|
|
|
@@ -13,8 +13,9 @@ import { existsSync } from 'node:fs';
|
|
|
13
13
|
import { join, relative, sep } from 'node:path';
|
|
14
14
|
|
|
15
15
|
import { parseFrontmatter, safeRead, parseJsonl, pass, fail } from './helpers.mjs';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
16
|
+
import { resolveEventsLedgerRoot } from '../../platform.mjs';
|
|
17
|
+
import { resolveLedgerPath } from '../../ledger-path.mjs';
|
|
18
|
+
import { canonicalizeSessions, isMeasuredSession } from '../../sessions-canonical.mjs';
|
|
18
19
|
import { findSessionConfigBlock } from '../../config/section-extractor.mjs';
|
|
19
20
|
|
|
20
21
|
// Default session-lock TTL in hours — mirrors DEFAULT_TTL_HOURS in
|
|
@@ -86,8 +87,9 @@ export function runCategory4(root) {
|
|
|
86
87
|
|
|
87
88
|
// c4.2 sessions-jsonl-recent
|
|
88
89
|
{
|
|
89
|
-
const p = join(root, '.orchestrator/metrics/sessions.jsonl');
|
|
90
|
+
const p = join(resolveEventsLedgerRoot(root), '.orchestrator/metrics/sessions.jsonl');
|
|
90
91
|
const text = safeRead(p);
|
|
92
|
+
// ledger-root: display relPath for the finding, the read above is resolved
|
|
91
93
|
const relPath = '.orchestrator/metrics/sessions.jsonl';
|
|
92
94
|
if (!text) {
|
|
93
95
|
checks.push(fail('sessions-jsonl-recent', 3, relPath,
|
|
@@ -100,13 +102,13 @@ export function runCategory4(root) {
|
|
|
100
102
|
{ latestCompletedAt: null, ageInDays: null },
|
|
101
103
|
'sessions.jsonl is empty'));
|
|
102
104
|
} else {
|
|
103
|
-
// Most-recent
|
|
105
|
+
// Most-recent MEASURED session by completed_at, over the CANONICAL record
|
|
104
106
|
// set (#1209b) — canonicalizeSessions() already collapses duplicate
|
|
105
107
|
// `session_id` lines to the newest occurrence, drops the systemic
|
|
106
108
|
// double-stub twin, and removes any record a surviving one
|
|
107
109
|
// `supersedes`. A raw backward-scan-to-last-real-LINE (the pre-#1209b
|
|
108
110
|
// approach) cannot see this: a duplicate id whose LAST occurrence was
|
|
109
|
-
// retroactively corrected to
|
|
111
|
+
// retroactively corrected to a synthetic stub still let an
|
|
110
112
|
// EARLIER, now-superseded raw line for the same identity count as
|
|
111
113
|
// "recent". Max, not backward-scan, because canonical order is
|
|
112
114
|
// first-appearance order per id, not completed_at order.
|
|
@@ -123,7 +125,7 @@ export function runCategory4(root) {
|
|
|
123
125
|
let lastObj = null;
|
|
124
126
|
let lastMs = -Infinity;
|
|
125
127
|
for (const rec of records) {
|
|
126
|
-
if (!
|
|
128
|
+
if (!isMeasuredSession(rec)) continue;
|
|
127
129
|
const ms = Date.parse(rec?.completed_at);
|
|
128
130
|
if (Number.isFinite(ms) && ms > lastMs) {
|
|
129
131
|
lastMs = ms;
|
|
@@ -154,8 +156,9 @@ export function runCategory4(root) {
|
|
|
154
156
|
|
|
155
157
|
// c4.3 learnings-prunable
|
|
156
158
|
{
|
|
157
|
-
const p =
|
|
159
|
+
const p = resolveLedgerPath('.orchestrator/metrics/learnings.jsonl', root);
|
|
158
160
|
const text = safeRead(p);
|
|
161
|
+
// ledger-root: display relPath for the finding, the read above is resolved
|
|
159
162
|
const relPath = '.orchestrator/metrics/learnings.jsonl';
|
|
160
163
|
if (!text) {
|
|
161
164
|
checks.push(fail('learnings-prunable', 2, relPath,
|
|
@@ -14,6 +14,8 @@ import { execFileSync } from 'node:child_process';
|
|
|
14
14
|
import { safeRead, lineCount, pass, fail } from './helpers.mjs';
|
|
15
15
|
import { resolveInstructionFile } from '../../common.mjs';
|
|
16
16
|
import { listRemotes } from '../../vcs-repo-spec.mjs';
|
|
17
|
+
import { _extractConfigSection, _parseKV } from '../../config/section-extractor.mjs';
|
|
18
|
+
import { parseSourceCommitTrailer } from '../../github-export.mjs';
|
|
17
19
|
|
|
18
20
|
export function runCategory6(root) {
|
|
19
21
|
const checks = [];
|
|
@@ -231,14 +233,32 @@ export function runCategory6(root) {
|
|
|
231
233
|
// git not honouring its own contract — equally unmeasurable, and
|
|
232
234
|
// splitting it off would leave the identical fail-open standing behind a
|
|
233
235
|
// smaller trigger. `aheadCountQuery` records which of the two occurred.
|
|
234
|
-
|
|
236
|
+
// `mirror: curated` (#1631): the published commit is a snapshot, not an
|
|
237
|
+
// ancestor of HEAD, so `github/<branch>..HEAD` would count the whole private
|
|
238
|
+
// history. Count from the snapshot's `Source-Commit` trailer instead; a
|
|
239
|
+
// mirror that carries no trailer yet is still a plain ancestor (pre-export).
|
|
240
|
+
const instrText = instr ? safeRead(instrPath) : null;
|
|
241
|
+
const curated = instrText !== null && _parseKV(_extractConfigSection(instrText)).get('mirror') === 'curated';
|
|
242
|
+
let aheadBase = `github/${mirrorBranch}`;
|
|
243
|
+
let revListOut;
|
|
244
|
+
if (curated) {
|
|
245
|
+
const mirrorCommit = runGit(['cat-file', 'commit', `github/${mirrorBranch}`]);
|
|
246
|
+
aheadBase = (mirrorCommit !== null && parseSourceCommitTrailer(mirrorCommit)) || aheadBase;
|
|
247
|
+
revListOut = mirrorCommit === null ? null : runGit(['rev-list', '--count', `${aheadBase}..HEAD`]);
|
|
248
|
+
} else {
|
|
249
|
+
revListOut = runGit(['rev-list', '--count', `${aheadBase}..HEAD`]);
|
|
250
|
+
}
|
|
235
251
|
const aheadCount = revListOut !== null ? Number.parseInt(revListOut, 10) : NaN;
|
|
252
|
+
const curatedEvidence = curated ? { mirrorMode: 'curated', aheadBase } : {};
|
|
253
|
+
const pushHint = curated
|
|
254
|
+
? 'Run /close on main — session-end § 4.4 publishes the curated snapshot with its checks (never push the export by hand)'
|
|
255
|
+
: 'Run: git push github HEAD';
|
|
236
256
|
|
|
237
257
|
if (Number.isNaN(aheadCount)) {
|
|
238
258
|
const aheadCountQuery = revListOut === null ? 'git-error' : 'unparseable-output';
|
|
239
259
|
checks.push(fail({
|
|
240
260
|
checkId, maxPoints: 2, path: relPath,
|
|
241
|
-
evidence: { hasGithubRemote: true, mirrorBranch, resolvedVia, aheadCount: null, aheadCountQuery },
|
|
261
|
+
evidence: { hasGithubRemote: true, mirrorBranch, resolvedVia, aheadCount: null, aheadCountQuery, ...curatedEvidence },
|
|
242
262
|
message:
|
|
243
263
|
`unable to determine ahead-count vs github/${mirrorBranch} (${aheadCountQuery}) — ` +
|
|
244
264
|
'mirror drift not measurable',
|
|
@@ -246,14 +266,14 @@ export function runCategory6(root) {
|
|
|
246
266
|
} else if (aheadCount === 0) {
|
|
247
267
|
checks.push(pass({
|
|
248
268
|
checkId, points: 2, maxPoints: 2, path: relPath,
|
|
249
|
-
evidence: { hasGithubRemote: true, mirrorBranch, resolvedVia, aheadCount },
|
|
269
|
+
evidence: { hasGithubRemote: true, mirrorBranch, resolvedVia, aheadCount, ...curatedEvidence },
|
|
250
270
|
message: `HEAD is fully mirrored to github/${mirrorBranch}`,
|
|
251
271
|
}));
|
|
252
272
|
} else {
|
|
253
273
|
checks.push(pass({
|
|
254
274
|
checkId, points: 1, maxPoints: 2, path: relPath,
|
|
255
|
-
evidence: { hasGithubRemote: true, mirrorBranch, resolvedVia, aheadCount },
|
|
256
|
-
message: `${aheadCount} local commit(s) not pushed to github mirror (github/${mirrorBranch}).
|
|
275
|
+
evidence: { hasGithubRemote: true, mirrorBranch, resolvedVia, aheadCount, ...curatedEvidence },
|
|
276
|
+
message: `${aheadCount} local commit(s) not pushed to github mirror (github/${mirrorBranch}). ${pushHint}`,
|
|
257
277
|
}));
|
|
258
278
|
}
|
|
259
279
|
}
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
* status field is only 'pass' / 'fail' — partial tiers emit a reduced-points
|
|
22
22
|
* pass() (there is no 'partial' constructor in helpers.mjs).
|
|
23
23
|
*
|
|
24
|
-
* Stdlib
|
|
24
|
+
* Stdlib (node:fs, node:path, node:child_process) plus the auditor's own
|
|
25
|
+
* ../../platform.mjs for the sessions-ledger root. The advisory tally in
|
|
25
26
|
* c9.3 imports the AUDITOR's OWN (trusted) join/score modules in a short-lived
|
|
26
27
|
* worker subprocess and runs them over the TARGET repo's telemetry DATA files —
|
|
27
28
|
* it never imports code rooted at the audited repo (review N2b: RCE-on-audit).
|
|
@@ -34,6 +35,8 @@ import { join } from 'node:path';
|
|
|
34
35
|
import { spawnSync } from 'node:child_process';
|
|
35
36
|
|
|
36
37
|
import { pass, fail } from './helpers.mjs';
|
|
38
|
+
import { resolveEventsLedgerRoot } from '../../platform.mjs';
|
|
39
|
+
import { resolveLedgerPath } from '../../ledger-path.mjs';
|
|
37
40
|
|
|
38
41
|
const TELEMETRY_REL = '.orchestrator/metrics/skill-invocations.jsonl';
|
|
39
42
|
const SESSIONS_REL = '.orchestrator/metrics/sessions.jsonl';
|
|
@@ -80,7 +83,7 @@ export function runCategory9(root) {
|
|
|
80
83
|
// points (with a quoted malformed-line count) when some lines are malformed.
|
|
81
84
|
{
|
|
82
85
|
const checkId = 'skill-telemetry-hygiene';
|
|
83
|
-
const telemetryPath =
|
|
86
|
+
const telemetryPath = resolveLedgerPath(TELEMETRY_REL, root);
|
|
84
87
|
if (!existsSync(telemetryPath)) {
|
|
85
88
|
checks.push(pass({
|
|
86
89
|
checkId, points: 4, maxPoints: 4, path: TELEMETRY_REL,
|
|
@@ -186,7 +189,7 @@ function buildAdvisoryCheck(root) {
|
|
|
186
189
|
|
|
187
190
|
const scorerPath = join(root, SCORER_REL);
|
|
188
191
|
const joinPath = join(root, JOIN_REL);
|
|
189
|
-
const telemetryPath =
|
|
192
|
+
const telemetryPath = resolveLedgerPath(TELEMETRY_REL, root);
|
|
190
193
|
|
|
191
194
|
// No module(s) or no telemetry → advisory surface has nothing to say, but
|
|
192
195
|
// that is a healthy "insufficient signal" state. Pass full points.
|
|
@@ -250,7 +253,10 @@ function buildAdvisoryCheck(root) {
|
|
|
250
253
|
// for caller symmetry. The CODE imported here comes from the auditor's OWN modules
|
|
251
254
|
// (derived from import.meta.url below), never from the audited target (review N2b).
|
|
252
255
|
function computeAdvisoryTally(scorerPath, joinPath, telemetryPath, root) {
|
|
253
|
-
|
|
256
|
+
// #1592: the repo's ONE sessions ledger (main checkout of a linked worktree),
|
|
257
|
+
// matching join.mjs's own default and c4.2 — telemetry stays at `root`, its
|
|
258
|
+
// hook writer is not mapped.
|
|
259
|
+
const sessionsPath = join(resolveEventsLedgerRoot(root), SESSIONS_REL);
|
|
254
260
|
// Import the AUDITOR's OWN trusted modules — derived from THIS file's location,
|
|
255
261
|
// NEVER from the audited `root`. The target's join.mjs/score.mjs are existence-
|
|
256
262
|
// checked by the caller but never imported (would execute target code → N2b).
|
|
@@ -216,7 +216,9 @@ const HOST_ALIASES_MAX = 16;
|
|
|
216
216
|
* @param {number} mode
|
|
217
217
|
*/
|
|
218
218
|
function _writeJsonAtomicSync(filePath, data, mode) {
|
|
219
|
-
|
|
219
|
+
// 0o700: the ledger lives in the private config dir, which must not come out
|
|
220
|
+
// 0777 under umask 000 (#1629).
|
|
221
|
+
mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
220
222
|
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
221
223
|
try {
|
|
222
224
|
// `flag: 'wx'` + `mode` on the CREATE, never a chmod after the rename.
|
|
@@ -486,10 +488,11 @@ async function _readJsonSafe(p) {
|
|
|
486
488
|
}
|
|
487
489
|
}
|
|
488
490
|
|
|
489
|
-
async function _writeJsonAtomic(filePath, data, { mode } = {}) {
|
|
490
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
491
|
+
async function _writeJsonAtomic(filePath, data, { mode, dirMode } = {}) {
|
|
492
|
+
await mkdir(path.dirname(filePath), dirMode === undefined ? { recursive: true } : { recursive: true, mode: dirMode });
|
|
491
493
|
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
492
|
-
|
|
494
|
+
// mode at creation closes the 0666 window before the chmod below (#1632).
|
|
495
|
+
await writeFile(tmp, JSON.stringify(data, null, 2) + '\n', mode === undefined ? 'utf8' : { encoding: 'utf8', mode });
|
|
493
496
|
const { rename } = await import('node:fs/promises');
|
|
494
497
|
await rename(tmp, filePath);
|
|
495
498
|
if (mode !== undefined) {
|
|
@@ -541,6 +544,7 @@ export async function getPrivateHostInfo(projectRoot) {
|
|
|
541
544
|
return cached;
|
|
542
545
|
}
|
|
543
546
|
const fresh = collectPrivateInfo(projectRoot);
|
|
544
|
-
|
|
547
|
+
// dirMode only here: the public twin goes to the repo's .orchestrator/ (#1629).
|
|
548
|
+
await _writeJsonAtomic(f, fresh, { mode: 0o600, dirMode: 0o700 });
|
|
545
549
|
return fresh;
|
|
546
550
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Buffered HTTP response subset for hook networking (#1612). Stdlib requests
|
|
3
|
+
* avoid Undici async-handle teardown crashes on Windows at process.exit().
|
|
4
|
+
* Internal configured endpoints only; no redirects are followed (SEC-014).
|
|
5
|
+
*/
|
|
6
|
+
import http from 'node:http';
|
|
7
|
+
import https from 'node:https';
|
|
8
|
+
|
|
9
|
+
export async function httpRequest(url, {
|
|
10
|
+
method = 'GET', headers = {}, body, timeoutMs = 3000, maxBytes = 1024 * 1024,
|
|
11
|
+
} = {}) {
|
|
12
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) throw new Error('invalid maxBytes');
|
|
13
|
+
const target = new URL(url);
|
|
14
|
+
if (target.protocol !== 'http:' && target.protocol !== 'https:') {
|
|
15
|
+
throw new Error('HTTP request requires http: or https:');
|
|
16
|
+
}
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const transport = target.protocol === 'https:' ? https : http;
|
|
19
|
+
const req = transport.request(target, { method, headers, agent: false }, res => {
|
|
20
|
+
const chunks = [];
|
|
21
|
+
let bytes = 0;
|
|
22
|
+
let overflow = false;
|
|
23
|
+
res.on('data', chunk => {
|
|
24
|
+
if (overflow) return;
|
|
25
|
+
bytes += chunk.length;
|
|
26
|
+
if (bytes > maxBytes) {
|
|
27
|
+
overflow = true;
|
|
28
|
+
chunks.length = 0;
|
|
29
|
+
const error = new Error(`HTTP response exceeded maxBytes (${maxBytes})`);
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
reject(error);
|
|
32
|
+
res.destroy();
|
|
33
|
+
req.destroy(error);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
chunks.push(chunk);
|
|
37
|
+
});
|
|
38
|
+
res.on('error', reject);
|
|
39
|
+
res.on('end', () => {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
if (overflow) return;
|
|
42
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
43
|
+
const status = res.statusCode;
|
|
44
|
+
resolve({
|
|
45
|
+
ok: status >= 200 && status < 300,
|
|
46
|
+
status,
|
|
47
|
+
text: async () => text,
|
|
48
|
+
json: async () => JSON.parse(text),
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
// Wall-clock ceiling includes DNS, connection and body, unlike a socket
|
|
53
|
+
// inactivity timeout. Destroying closes the socket even for a stalled body.
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
req.destroy(new Error('HTTP request timed out'));
|
|
56
|
+
}, timeoutMs);
|
|
57
|
+
req.on('error', error => {
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
reject(error);
|
|
60
|
+
});
|
|
61
|
+
req.on('close', () => clearTimeout(timer));
|
|
62
|
+
req.end(body);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Lightweight directive-budget guard for always-on `.claude/rules/*.md`.
|
|
5
5
|
*
|
|
6
|
-
* Sums the always-on directive count across the rule files that
|
|
7
|
-
*
|
|
8
|
-
* `paths:`
|
|
9
|
-
* always-on and
|
|
6
|
+
* Sums the always-on directive count across the rule files that Claude Code
|
|
7
|
+
* loads always-on: every rule WITHOUT a `paths:` frontmatter key (#1638).
|
|
8
|
+
* Claude Code reads `paths:` only and ignores `globs:` without an error, so a
|
|
9
|
+
* `globs:`-only rule is always-on there and counts here, even though the
|
|
10
|
+
* rule-loader's own wave injection scopes it. Measured through
|
|
11
|
+
* `loadApplicableRules({ scopeKey: 'paths' })`. It warns when the total
|
|
10
12
|
* exceeds a ceiling. "Mechanism over discipline" — the #668 instruction-
|
|
11
13
|
* budget audit recommends this as a silent-now growth ratchet that only
|
|
12
14
|
* fires when NEW always-on directives are added.
|
|
@@ -66,11 +68,13 @@
|
|
|
66
68
|
* - SISTER GUARD / KNOWN DIVERGENCE (#906.3): the projects-baseline repo
|
|
67
69
|
* (resolved via `plan-baseline-path` / owner.yaml `baseline-path`) ships
|
|
68
70
|
* `scripts/check-instruction-budget.sh` under rule CCU-009c. It measures the
|
|
69
|
-
* SAME `.claude/rules/*.md` corpus and its total is NOT comparable to ours
|
|
70
|
-
* it
|
|
71
|
+
* SAME `.claude/rules/*.md` corpus and its total is NOT comparable to ours.
|
|
72
|
+
* As of 2026-07-30 it diverged on BOTH axes, in OPPOSITE directions:
|
|
71
73
|
* (a) MEMBERSHIP — it classifies a file as path-scoped only on a `paths:`
|
|
72
|
-
* frontmatter key, so this repo's `globs:`-scoped rules
|
|
73
|
-
* always-on set: 26 files where
|
|
74
|
+
* frontmatter key, so this repo's then `globs:`-scoped rules stayed
|
|
75
|
+
* inside ITS always-on set: 26 files where we counted 12. That was
|
|
76
|
+
* Claude Code's semantics and ours was wrong; since #1638 this guard
|
|
77
|
+
* also scopes on `paths:` only, so the membership axis is aligned.
|
|
74
78
|
* (b) HEURISTIC — it counts only rule-ID anchors and imperative-keyword
|
|
75
79
|
* BULLET lines outside code fences, where `countDirectives` below
|
|
76
80
|
* counts every bullet, ordered-list item and `##`-or-deeper heading.
|
|
@@ -957,15 +961,16 @@ export function listMachineGeneratedRules(opts = {}) {
|
|
|
957
961
|
* ({@link isMachineGeneratedRule}). This is the population
|
|
958
962
|
* {@link DEFAULT_GENERATED_BYTE_CEILING} is a ceiling FOR: the thing that
|
|
959
963
|
* grows on its own, without anyone deciding to add a rule.
|
|
960
|
-
* - `pathScoped` — every rule with `
|
|
961
|
-
*
|
|
964
|
+
* - `pathScoped` — every rule with a `paths:` key (#1638: Claude Code's only
|
|
965
|
+
* scope key; a `globs:`-only rule is always-on there), i.e. the complement
|
|
966
|
+
* of the always-on set the three tier surfaces measure.
|
|
962
967
|
* A real quantity, and the one this axis USED to report under the name
|
|
963
968
|
* `generated` — see the ceiling's docblock for what that cost.
|
|
964
969
|
*
|
|
965
970
|
* The two overlap but neither contains the other by construction: a
|
|
966
971
|
* hand-written rule can be path-scoped (3 of them are here), and a generated
|
|
967
972
|
* rule could in principle carry a `host-class` activation axis instead of
|
|
968
|
-
* `
|
|
973
|
+
* `paths:`.
|
|
969
974
|
*
|
|
970
975
|
* Neither can reuse `loadApplicableRules`: that loader takes a `scopePaths`
|
|
971
976
|
* list and returns the rules APPLICABLE to it, so with `scopePaths: []` it
|
|
@@ -1005,10 +1010,10 @@ function measureRuleCorpora(rulesDir) {
|
|
|
1005
1010
|
} catch {
|
|
1006
1011
|
continue; // unreadable file — skip, never throw
|
|
1007
1012
|
}
|
|
1008
|
-
let
|
|
1013
|
+
let paths;
|
|
1009
1014
|
let meta;
|
|
1010
1015
|
try {
|
|
1011
|
-
({
|
|
1016
|
+
({ paths, meta } = parseGlobsFrontmatter(content));
|
|
1012
1017
|
} catch {
|
|
1013
1018
|
continue;
|
|
1014
1019
|
}
|
|
@@ -1017,8 +1022,9 @@ function measureRuleCorpora(rulesDir) {
|
|
|
1017
1022
|
generated.files += 1;
|
|
1018
1023
|
generated.bytes += bytes;
|
|
1019
1024
|
}
|
|
1020
|
-
// `
|
|
1021
|
-
|
|
1025
|
+
// `paths === null` → always-on for Claude Code, already counted by the
|
|
1026
|
+
// tier surfaces (which load with `scopeKey: 'paths'` — same split).
|
|
1027
|
+
if (paths !== null) {
|
|
1022
1028
|
pathScoped.files += 1;
|
|
1023
1029
|
pathScoped.bytes += bytes;
|
|
1024
1030
|
}
|
|
@@ -1130,8 +1136,8 @@ function measureRuleCorpora(rulesDir) {
|
|
|
1130
1136
|
* reconciliation PROVENANCE marker (`auto-generated: true` /
|
|
1131
1137
|
* `learning-key` / `expires-at`) — the corpus
|
|
1132
1138
|
* {@link DEFAULT_GENERATED_BYTE_CEILING} judges (#1297).
|
|
1133
|
-
* bySurface.pathScoped === {bytes, files} of every rule with `
|
|
1134
|
-
* (
|
|
1139
|
+
* bySurface.pathScoped === {bytes, files} of every rule with a `paths:`
|
|
1140
|
+
* key (#1638) — the complement of the always-on set, and
|
|
1135
1141
|
* the number an operator reproduces from `ls .claude/rules/` (HR-106).
|
|
1136
1142
|
* Judged against {@link DEFAULT_PATH_SCOPED_BYTE_CEILING} into
|
|
1137
1143
|
* `overPathScopedBudget`, an `overBudget` term since #1316 (see that
|
|
@@ -1207,10 +1213,12 @@ export function computeInstructionBudget(opts = {}) {
|
|
|
1207
1213
|
// — reused, not reimplemented. All three lists are loaded unconditionally
|
|
1208
1214
|
// (not just the one matching `opts.context`) because `bySurface` reports
|
|
1209
1215
|
// all three surfaces regardless of which `context` was requested for the
|
|
1210
|
-
// PRIMARY totals (see doc above).
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1216
|
+
// PRIMARY totals (see doc above). `scopeKey: 'paths'` (#1638): the budget
|
|
1217
|
+
// measures what Claude Code loads, and Claude Code scopes on `paths:` only.
|
|
1218
|
+
const scopeKey = 'paths';
|
|
1219
|
+
allEntries = loadApplicableRules({ rulesDir, scopePaths: [], scopeKey });
|
|
1220
|
+
waveEntries = loadApplicableRules({ rulesDir, scopePaths: [], context: 'wave', scopeKey });
|
|
1221
|
+
coordinatorEntries = loadApplicableRules({ rulesDir, scopePaths: [], context: 'coordinator', scopeKey });
|
|
1214
1222
|
} catch {
|
|
1215
1223
|
return empty;
|
|
1216
1224
|
}
|
|
@@ -1242,7 +1250,7 @@ export function computeInstructionBudget(opts = {}) {
|
|
|
1242
1250
|
// `pathScoped` keeps the old measurement under its honest name. Both are
|
|
1243
1251
|
// disjoint from `totalBytes`, which counts always-on rules exclusively —
|
|
1244
1252
|
// except that a generated rule activated by `host-class` rather than
|
|
1245
|
-
// `
|
|
1253
|
+
// `paths:` would be always-on and therefore counted in both.
|
|
1246
1254
|
...measureRuleCorpora(rulesDir),
|
|
1247
1255
|
};
|
|
1248
1256
|
|
package/scripts/lib/io.mjs
CHANGED
|
@@ -888,13 +888,14 @@ export async function writeJsonAtomic(filePath, value, opts = {}) {
|
|
|
888
888
|
* @param {object} [opts]
|
|
889
889
|
* @param {number} [opts.indent=2] JSON.stringify indent.
|
|
890
890
|
* @param {string} [opts.tmpPrefix='.tmp'] Tmp-file prefix (callers pick their domain prefix).
|
|
891
|
+
* @param {number} [opts.mode] File mode of the tmp file, kept by the rename (#1632).
|
|
891
892
|
* @returns {{ ok: true } | { ok: false, reason: 'fs-error', error: string, code: string|null }}
|
|
892
893
|
* `code` is the failing call's `err.code` (`'ENOSPC'`, `'EACCES'`, …), or `null`
|
|
893
894
|
* when the error carried none (a serialization TypeError, the partial-adapter
|
|
894
895
|
* refusal). Additive (#1032): `error` keeps its exact text.
|
|
895
896
|
*/
|
|
896
897
|
export function writeJsonAtomicSync(filePath, data, opts = {}) {
|
|
897
|
-
const { indent = 2, tmpPrefix = '.tmp' } = opts;
|
|
898
|
+
const { indent = 2, tmpPrefix = '.tmp', mode } = opts;
|
|
898
899
|
|
|
899
900
|
let body;
|
|
900
901
|
try {
|
|
@@ -903,7 +904,7 @@ export function writeJsonAtomicSync(filePath, data, opts = {}) {
|
|
|
903
904
|
return _fsErrorEnvelope(err);
|
|
904
905
|
}
|
|
905
906
|
|
|
906
|
-
const res = atomicWriteWithBackup(filePath, body, { tmpPrefix });
|
|
907
|
+
const res = atomicWriteWithBackup(filePath, body, { tmpPrefix, mode });
|
|
907
908
|
return res.ok ? { ok: true } : res;
|
|
908
909
|
}
|
|
909
910
|
|
|
@@ -980,6 +981,7 @@ export function writeJsonAtomicSync(filePath, data, opts = {}) {
|
|
|
980
981
|
* @param {BufferEncoding} [opts.encoding='utf8'] Encoding for a string `body`.
|
|
981
982
|
* @param {boolean} [opts.backup=false] Snapshot the existing file to `.bak-<ISO>` first.
|
|
982
983
|
* @param {string} [opts.tmpPrefix='.tmp'] Tmp-file prefix (callers pick their domain prefix).
|
|
984
|
+
* @param {number} [opts.mode] File mode of the tmp file, kept by the rename (#1632).
|
|
983
985
|
* @param {Date} [opts.now] Clock seam for the backup stamp (tests).
|
|
984
986
|
* @param {{ mkdirSync?: Function, writeFileSync?: Function, renameSync?: Function,
|
|
985
987
|
* copyFileSync?: Function, existsSync?: Function, unlinkSync?: Function }} [opts.fs]
|
|
@@ -995,6 +997,7 @@ export function atomicWriteWithBackup(filePath, body, opts = {}) {
|
|
|
995
997
|
encoding = 'utf8',
|
|
996
998
|
backup = false,
|
|
997
999
|
tmpPrefix = '.tmp',
|
|
1000
|
+
mode,
|
|
998
1001
|
now = new Date(),
|
|
999
1002
|
fs: injectedFs,
|
|
1000
1003
|
} = opts;
|
|
@@ -1043,7 +1046,7 @@ export function atomicWriteWithBackup(filePath, body, opts = {}) {
|
|
|
1043
1046
|
}
|
|
1044
1047
|
|
|
1045
1048
|
tmpFile = _tmpPathFor(dir, tmpPrefix);
|
|
1046
|
-
fsWriteFile(tmpFile, body, encoding);
|
|
1049
|
+
fsWriteFile(tmpFile, body, mode === undefined ? encoding : { encoding, mode });
|
|
1047
1050
|
fsRename(tmpFile, filePath);
|
|
1048
1051
|
|
|
1049
1052
|
return { ok: true, path: filePath, bytes: _byteLength(body, encoding), backupPath };
|