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
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
|
|
26
26
|
import path from 'node:path';
|
|
27
27
|
import { resolveStateDir } from '../platform.mjs';
|
|
28
|
+
import { resolveOwnLockRootFor } from '../own-lock-root.mjs';
|
|
29
|
+
import { readProcessLocalSessionIds } from '../session-identity/own-session.mjs';
|
|
28
30
|
import { shellQuote } from '../sh-quote.mjs';
|
|
29
31
|
import { detectVcsCommand } from './vcs-detector.mjs';
|
|
30
32
|
import { isSessionCollision } from './tmux-shell.mjs';
|
|
@@ -70,6 +72,11 @@ const STATUS_PANE_POLL_SECONDS = 2;
|
|
|
70
72
|
*
|
|
71
73
|
* Exported so a consumer test can run the render ONCE instead of the poll loop.
|
|
72
74
|
*
|
|
75
|
+
* `repoRoot` (#1730): the agent-status writer (`hooks/subagent-telemetry.mjs`)
|
|
76
|
+
* writes beside the session's OWN lock, which is not always the checkout the
|
|
77
|
+
* second terminal sits in. A given root is written into the command as a
|
|
78
|
+
* literal; without one the pane reads its own `process.cwd()`, as before.
|
|
79
|
+
*
|
|
73
80
|
* Shell-shape notes (both load-bearing):
|
|
74
81
|
* - the `-e` script is wrapped in SINGLE quotes, so the JS below uses only
|
|
75
82
|
* double-quoted strings: inside single quotes no `$`, backtick or interactive
|
|
@@ -77,15 +84,21 @@ const STATUS_PANE_POLL_SECONDS = 2;
|
|
|
77
84
|
* - the module path is resolved from THIS file's own URL (never a hardcoded home
|
|
78
85
|
* path), exactly as the sibling pane commands stay relative-to-cwd.
|
|
79
86
|
*
|
|
87
|
+
* @param {{ repoRoot?: string|null }} [opts]
|
|
80
88
|
* @returns {string} a single shell command; prints the pane body once and exits.
|
|
81
89
|
*/
|
|
82
|
-
export function buildStatusPaneRenderCommand() {
|
|
90
|
+
export function buildStatusPaneRenderCommand({ repoRoot = null } = {}) {
|
|
83
91
|
// `%27` guard: a single quote in the repo path would otherwise end the shell
|
|
84
92
|
// single-quoted `-e` argument.
|
|
85
93
|
const moduleHref = new URL('../agent-status.mjs', import.meta.url).href.replace(/'/g, '%27');
|
|
94
|
+
// Same guard for the root literal: a quote in the path is written as its
|
|
95
|
+
// JSON unicode escape, which the double-quoted JS string reads as the quote.
|
|
96
|
+
const rootExpr = typeof repoRoot === 'string' && repoRoot !== ''
|
|
97
|
+
? JSON.stringify(repoRoot).replace(/'/g, '\\u0027')
|
|
98
|
+
: 'process.cwd()';
|
|
86
99
|
const js = [
|
|
87
100
|
`import {readCurrentStatus} from ${JSON.stringify(moduleHref)};`,
|
|
88
|
-
|
|
101
|
+
`const v=readCurrentStatus({repoRoot:${rootExpr}});`,
|
|
89
102
|
'const ids=Object.keys(v.entries).sort();',
|
|
90
103
|
// An EMPTY channel (no ledger yet, no cache yet) is the normal fresh-repo
|
|
91
104
|
// state, not a degradation — marking it would fire the warning on every
|
|
@@ -141,7 +154,11 @@ async function _renderDefaultLayoutInner({ sessionName, force, projectRoot, vcsC
|
|
|
141
154
|
// file — so the pane can never present a stale `running` as live. The renderer
|
|
142
155
|
// prints its own "no agent-status yet" line when the channel is empty, and is
|
|
143
156
|
// exported (buildStatusPaneRenderCommand) so a test can run it once.
|
|
144
|
-
|
|
157
|
+
// #1730: the root is the one holding this session's own lock, resolved once here; none found → the pane's cwd.
|
|
158
|
+
const own = withStatusPane
|
|
159
|
+
? resolveOwnLockRootFor({ sessionId: readProcessLocalSessionIds()[0] ?? null, cwd: projectRoot })
|
|
160
|
+
: null;
|
|
161
|
+
const pane5Cmd = `while true; do clear; ${buildStatusPaneRenderCommand({ repoRoot: own && own.source !== 'none' ? own.root : null })}; sleep ${STATUS_PANE_POLL_SECONDS}; done`;
|
|
145
162
|
|
|
146
163
|
// 4. Session-collision check
|
|
147
164
|
const collision = isSessionCollision(sessionName);
|
|
@@ -26,6 +26,7 @@ import path from 'node:path';
|
|
|
26
26
|
import { randomBytes } from 'node:crypto';
|
|
27
27
|
|
|
28
28
|
import { digestSha256Short } from '../crypto-digest-utils.mjs';
|
|
29
|
+
import { resolveLedgerPath } from '../ledger-path.mjs';
|
|
29
30
|
import { RUN_ID_PATTERN, assertRunId } from '../test-runner/artifact-paths.mjs';
|
|
30
31
|
|
|
31
32
|
// Re-exported so existing `ux-grill/paths.mjs` importers keep working; the
|
|
@@ -106,6 +107,7 @@ function assertRepoRoot(repoRoot) {
|
|
|
106
107
|
* @throws {TypeError} on an invalid repoRoot or runId (path-traversal guard)
|
|
107
108
|
*/
|
|
108
109
|
export function runDirPath(repoRoot, runId) {
|
|
110
|
+
// ledger-root: per-run artifact dir of this working tree, kept local
|
|
109
111
|
return path.resolve(assertRepoRoot(repoRoot), UX_GRILL_METRICS_DIR, assertRunId(runId));
|
|
110
112
|
}
|
|
111
113
|
|
|
@@ -156,7 +158,9 @@ export function findingsPath(repoRoot, runId) {
|
|
|
156
158
|
* @returns {string}
|
|
157
159
|
*/
|
|
158
160
|
export function runRecordPath(repoRoot) {
|
|
159
|
-
|
|
161
|
+
// #1722: an ignored ledger in a linked worktree maps to the main checkout;
|
|
162
|
+
// a target repo that commits it keeps its own copy.
|
|
163
|
+
return resolveLedgerPath(UX_GRILL_LEDGER, path.resolve(assertRepoRoot(repoRoot)));
|
|
160
164
|
}
|
|
161
165
|
|
|
162
166
|
/**
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
8
8
|
import { join, resolve, dirname } from 'node:path';
|
|
9
|
-
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
10
10
|
import yaml from 'js-yaml';
|
|
11
|
-
import { ALLOWED_MODEL_ALIASES, MODEL_ID_RE } from '../agent-frontmatter.mjs';
|
|
11
|
+
import { ALLOWED_MODEL_ALIASES, MODEL_ID_RE, checkAgentEffort } from '../agent-frontmatter.mjs';
|
|
12
12
|
import { extractInitialFrontmatter } from './frontmatter-block.mjs';
|
|
13
13
|
import { parseFrontmatterDescription } from '../description-surface.mjs';
|
|
14
14
|
|
|
@@ -18,7 +18,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
18
18
|
let _tierModule = null;
|
|
19
19
|
async function getTierModule() {
|
|
20
20
|
if (!_tierModule) {
|
|
21
|
-
_tierModule = await import(join(__dirname, 'tier-inference.mjs'));
|
|
21
|
+
_tierModule = await import(pathToFileURL(join(__dirname, 'tier-inference.mjs')).href);
|
|
22
22
|
}
|
|
23
23
|
return _tierModule;
|
|
24
24
|
}
|
|
@@ -103,13 +103,28 @@ function parseFrontmatterYaml(yamlText) {
|
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
105
|
* Get a scalar field value from frontmatter text.
|
|
106
|
-
* Returns null if field not present.
|
|
106
|
+
* Returns null if field not present or its value is empty. `[ \t]*` instead of
|
|
107
|
+
* `\s*`: an empty `key:` must not read the NEXT line as its value.
|
|
107
108
|
*/
|
|
108
109
|
function getField(fm, name) {
|
|
109
|
-
const m = fm.match(new RegExp(`^${name}
|
|
110
|
+
const m = fm.match(new RegExp(`^${name}:[ \t]*(.+)$`, 'm'));
|
|
110
111
|
return m ? m[1].trim() : null;
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Like getField, but for plain enum-like scalars (`model`, `effort`): strips one
|
|
116
|
+
* pair of surrounding quotes and a trailing YAML comment, so `effort: "medium"`
|
|
117
|
+
* and `effort: medium # reason` read as `medium`. Not for free text such as
|
|
118
|
+
* `description`, where ` #` may be content.
|
|
119
|
+
*/
|
|
120
|
+
function getScalarField(fm, name) {
|
|
121
|
+
const raw = getField(fm, name);
|
|
122
|
+
if (raw === null) return null;
|
|
123
|
+
const quoted = raw.match(/^(["'])(.*)\1(?:\s+#.*)?$/);
|
|
124
|
+
if (quoted) return quoted[2];
|
|
125
|
+
return raw.replace(/(^|\s+)#.*$/, '').trim();
|
|
126
|
+
}
|
|
127
|
+
|
|
113
128
|
/**
|
|
114
129
|
* Check whether a field key is present in frontmatter (even with empty/block value).
|
|
115
130
|
*/
|
|
@@ -323,7 +338,7 @@ if (mdFiles.length === 0) {
|
|
|
323
338
|
// do not re-declare the alias set or the model-ID regex here.
|
|
324
339
|
// Per https://code.claude.com/docs/en/sub-agents — the canonical doc accepts both.
|
|
325
340
|
if (hasField(frontmatter, 'model')) {
|
|
326
|
-
const modelVal =
|
|
341
|
+
const modelVal = getScalarField(frontmatter, 'model');
|
|
327
342
|
if (modelVal === null || (!ALLOWED_MODEL_ALIASES.has(modelVal) && !MODEL_ID_RE.test(modelVal))) {
|
|
328
343
|
const got = modelVal ?? '';
|
|
329
344
|
const aliasList = [...ALLOWED_MODEL_ALIASES].join('|');
|
|
@@ -331,6 +346,22 @@ if (mdFiles.length === 0) {
|
|
|
331
346
|
}
|
|
332
347
|
}
|
|
333
348
|
|
|
349
|
+
// effort: required when model ≠ inherit, enum low|medium|high|xhigh|max (#1614).
|
|
350
|
+
// Rule lives in agent-frontmatter.mjs (checkAgentEffort) — do not re-declare it here.
|
|
351
|
+
const effortError = checkAgentEffort(
|
|
352
|
+
hasField(frontmatter, 'model') ? (getScalarField(frontmatter, 'model') ?? '') : undefined,
|
|
353
|
+
hasField(frontmatter, 'effort') ? (getScalarField(frontmatter, 'effort') ?? '') : undefined,
|
|
354
|
+
);
|
|
355
|
+
if (effortError) fail(`${agentName}: ${effortError.message}`);
|
|
356
|
+
|
|
357
|
+
// omitClaudeMd: optional boolean (#1616) — a typo such as `yes` would be ignored silently.
|
|
358
|
+
if (hasField(frontmatter, 'omitClaudeMd')) {
|
|
359
|
+
const omitVal = getField(frontmatter, 'omitClaudeMd');
|
|
360
|
+
if (omitVal !== 'true' && omitVal !== 'false') {
|
|
361
|
+
fail(`${agentName}: omitClaudeMd must be true or false (got: '${omitVal ?? ''}')`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
334
365
|
// color: canonical Anthropic palette (red|blue|green|yellow|purple|orange|pink|cyan) plus magenta from plugin-dev SKILL.md.
|
|
335
366
|
// Source: https://code.claude.com/docs/en/sub-agents § Supported frontmatter fields.
|
|
336
367
|
if (hasField(frontmatter, 'color')) {
|
|
@@ -72,7 +72,22 @@ function stripLeadingDotSlash(p) {
|
|
|
72
72
|
* @param {boolean} optional - If true, missing conventional path is a PASS
|
|
73
73
|
*/
|
|
74
74
|
function checkComponentPath(field, conventional, optional = false) {
|
|
75
|
-
const
|
|
75
|
+
const value = json[field];
|
|
76
|
+
// hooks/mcpServers also accept inline config objects (plugins-reference §
|
|
77
|
+
// Component path forms). An inline object carries no path to resolve; arrays
|
|
78
|
+
// may mix paths and objects, so each string entry is checked as a path.
|
|
79
|
+
if (Array.isArray(value)) {
|
|
80
|
+
for (const entry of value) {
|
|
81
|
+
if (typeof entry === 'string') checkExplicitPath(field, entry);
|
|
82
|
+
else pass(`${field} inline config entry (no path to resolve)`);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (value && typeof value === 'object') {
|
|
87
|
+
pass(`${field} is an inline config object (no path to resolve)`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const relPath = value || '';
|
|
76
91
|
|
|
77
92
|
if (!relPath) {
|
|
78
93
|
// No explicit value — try conventional auto-discovery
|
|
@@ -87,6 +102,15 @@ function checkComponentPath(field, conventional, optional = false) {
|
|
|
87
102
|
return;
|
|
88
103
|
}
|
|
89
104
|
|
|
105
|
+
checkExplicitPath(field, relPath);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check one explicit component path: must start with "./" and exist.
|
|
110
|
+
* @param {string} field
|
|
111
|
+
* @param {string} relPath
|
|
112
|
+
*/
|
|
113
|
+
function checkExplicitPath(field, relPath) {
|
|
90
114
|
// Explicit value — must start with "./"
|
|
91
115
|
if (!relPath.startsWith('./')) {
|
|
92
116
|
fail(`${field} path does not start with ./: ${relPath}`);
|
|
@@ -889,7 +889,7 @@ function loadModuleVersions(pluginRoot, modulePath, cache) {
|
|
|
889
889
|
|
|
890
890
|
const workingSource = readFileSync(modulePath, 'utf8');
|
|
891
891
|
const working = parseModule(workingSource, modulePath);
|
|
892
|
-
const gitPath = relative.
|
|
892
|
+
const gitPath = relative.replace(/\\/g, '/');
|
|
893
893
|
const env = {};
|
|
894
894
|
for (const key of GIT_ENV_ALLOWLIST) {
|
|
895
895
|
if (process.env[key] !== undefined) env[key] = process.env[key];
|
|
@@ -1027,8 +1027,8 @@ export function inspectGuardRequiresParity(pluginRoot) {
|
|
|
1027
1027
|
result.summary.requires += requires.requires.length;
|
|
1028
1028
|
|
|
1029
1029
|
const modulePath = path.resolve(pluginRoot, MODULE_RELATIVE_PREFIX, ...specifier.segments);
|
|
1030
|
-
const moduleRelative = path.relative(pluginRoot, modulePath);
|
|
1031
|
-
if (moduleRelative.startsWith('..') || path.isAbsolute(moduleRelative) || !moduleRelative.startsWith(`${MODULE_RELATIVE_PREFIX}
|
|
1030
|
+
const moduleRelative = path.relative(pluginRoot, modulePath).replace(/\\/g, '/');
|
|
1031
|
+
if (moduleRelative.startsWith('..') || path.isAbsolute(moduleRelative) || !moduleRelative.startsWith(`${MODULE_RELATIVE_PREFIX}/`)) {
|
|
1032
1032
|
report('dynamic-contract', `${namespace}: specifier resolves outside ${MODULE_RELATIVE_PREFIX}`, specifierValue);
|
|
1033
1033
|
continue;
|
|
1034
1034
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// Stdout: ' PASS: ...' / ' FAIL: ...' lines + 'Results: N passed, M failed'
|
|
11
11
|
|
|
12
12
|
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
13
|
-
import { join } from 'node:path';
|
|
13
|
+
import { basename, join } from 'node:path';
|
|
14
14
|
|
|
15
15
|
// The pi and cursor projections are IMPORTED, not copied: the runtime bridges
|
|
16
16
|
// rewrite `hook_event_name` for every native event, so they are the source of
|
|
@@ -83,16 +83,48 @@ const DOCUMENTED_ASYMMETRIES = {
|
|
|
83
83
|
// shape ("PreToolUse hook returned permissionDecision:deny without a
|
|
84
84
|
// non-empty permissionDecisionReason").
|
|
85
85
|
//
|
|
86
|
-
//
|
|
87
|
-
// Codex
|
|
88
|
-
// `
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
// a
|
|
95
|
-
//
|
|
86
|
+
// 2026-09-06 (0.144.4) this block named the tool-name VOCABULARY as the
|
|
87
|
+
// blocker: Codex's model-facing tools are `shell` / `exec_command` /
|
|
88
|
+
// `unified_exec` / `apply_patch`, so a guard gating on
|
|
89
|
+
// `tool_name === 'Bash'` would run, match nothing and allow everything
|
|
90
|
+
// (#919-P2 class). For the SHELL tool that premise is refuted below; for
|
|
91
|
+
// the Edit/Write/MultiEdit gates (apply_patch) it is still unmeasured.
|
|
92
|
+
//
|
|
93
|
+
// REFUTED AGAIN for the shell tool (#1669), measured 2026-10-20 on
|
|
94
|
+
// codex-cli 0.160.1 with a logging PreToolUse hook under `codex exec`:
|
|
95
|
+
// a shell call arrives as `"tool_name":"Bash"` with
|
|
96
|
+
// `"tool_input":{"command":"<string>"}`, and `matcher: "Bash"` fires.
|
|
97
|
+
// The 2026-09-06 evidence (`strings -a <codex> | grep -c '"Bash"'` -> 0)
|
|
98
|
+
// counted a JSON-quoted literal Rust never stores.
|
|
99
|
+
//
|
|
100
|
+
// DENY IS HONOURED (#1691), measured 2026-10-20 on codex-cli 0.160.1:
|
|
101
|
+
// `codex exec` with pre-bash-destructive-guard as the only PreToolUse hook
|
|
102
|
+
// and the prompt `printf x > CLAUDE.md` logged `Command blocked by
|
|
103
|
+
// PreToolUse hook: Destructive command blocked: '> CLAUDE.md' (rule:
|
|
104
|
+
// redirect-truncate-protected)` and left the file unchanged (the rule
|
|
105
|
+
// protects CLAUDE.md and its Codex alias AGENTS.md alike); the control
|
|
106
|
+
// run with `hooks.PreToolUse=[]` executed it and the file became `x`.
|
|
107
|
+
// Measured payload: `session_id`, `turn_id`, `cwd` (= the session workdir,
|
|
108
|
+
// also the hook's process cwd), `hook_event_name`, `model`,
|
|
109
|
+
// `permission_mode`, `tool_name:"Bash"`, `tool_input.command`,
|
|
110
|
+
// `tool_use_id`, `transcript_path: null`; PostToolUse adds
|
|
111
|
+
// `tool_response: ""`. A real Codex session exports NO CLAUDE_* env.
|
|
112
|
+
//
|
|
113
|
+
// Every Bash hook was then fed that payload by hand under a clean env
|
|
114
|
+
// (PATH + HOME only, SO_PLATFORM=codex, mkdtemp git repo) and wired only
|
|
115
|
+
// where it enforces on it (2026-10-20): destructive-guard (deny on
|
|
116
|
+
// `> CLAUDE.md`), sessions-ledger-guard (deny on `>>
|
|
117
|
+
// .orchestrator/metrics/sessions.jsonl`), issue-budget (cap 1, second
|
|
118
|
+
// create denied and parked), enforce-commands (`.codex/wave-scope.json`
|
|
119
|
+
// `blockedCommands` → deny), memory-propose-audit (event appended,
|
|
120
|
+
// observe-only by design), post-bash-write-verify (warn on an out-of-scope
|
|
121
|
+
// write). They read `tool_input.command`, `cwd` and `session_id`; issue-budget also `tool_use_id`.
|
|
122
|
+
// Wiring contract: tests/hooks/codex-bash-guards-wiring.test.mjs.
|
|
123
|
+
//
|
|
124
|
+
// post-bash-issue-budget-refund is no gap on this list: it runs on
|
|
125
|
+
// PostToolUseFailure, an event hooks-codex.json does not carry, so a
|
|
126
|
+
// failed create on Codex keeps its slot (over-count, the fail-closed
|
|
127
|
+
// direction). Revisit when Codex gains a failure event.
|
|
96
128
|
PreToolUse: [
|
|
97
129
|
// pre-task-scope-disjoint (#1020): NOT ported by construction. The hook
|
|
98
130
|
// matches the `Agent` dispatch tool, which this platform does not have —
|
|
@@ -118,27 +150,17 @@ const DOCUMENTED_ASYMMETRIES = {
|
|
|
118
150
|
// `apply_patch`, a name this hook never matches. Wired, it would run and allow
|
|
119
151
|
// everything: a silent no-op, not enforcement. Gap registered, not faked.
|
|
120
152
|
'pre-edit-sessions-ledger-guard.mjs',
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
'pre-bash-sessions-ledger-guard.mjs',
|
|
153
|
+
// pre-bash-templates-first (#1691): runs on the Codex payload but can
|
|
154
|
+
// only DENY. Both allow routes are unreachable there: the template-Read
|
|
155
|
+
// evidence needs `transcript_path` (hooks/pre-bash-templates-first.mjs:
|
|
156
|
+
// 404-410), which Codex sends as `null` (codex-cli 0.160.1, 2026-10-20),
|
|
157
|
+
// and `/templates-ack` writes an acknowledgement only under a `claude`
|
|
158
|
+
// ancestor process (commands/templates-ack.md:18-23). Fed the Codex
|
|
159
|
+
// payload with `.gitlab/issue_templates/bug.md` present, `glab issue
|
|
160
|
+
// create` was denied with no way to satisfy it. A guard nobody can pass
|
|
161
|
+
// is a blocker, not enforcement; gap registered, not faked.
|
|
131
162
|
'pre-bash-templates-first.mjs',
|
|
132
|
-
'pre-bash-issue-budget.mjs',
|
|
133
|
-
'enforce-commands.mjs',
|
|
134
163
|
],
|
|
135
|
-
// post-bash-write-verify (#942): gates on tool_name === 'Bash', a value
|
|
136
|
-
// Codex never emits (its shell tools are `shell` / `exec_command` /
|
|
137
|
-
// `unified_exec`). Documented exception until a tool-name map exists —
|
|
138
|
-
// a manifest entry today would be a silent no-op, not enforcement. Same
|
|
139
|
-
// #919-P2 class as the PreToolUse block. `post-tool-use.command.input`
|
|
140
|
-
// at 0.144.4 does require `tool_name`, `tool_input` AND `tool_response`,
|
|
141
|
-
// so the port is a rename map, not a payload bridge.
|
|
142
164
|
PostToolUse: [
|
|
143
165
|
'post-edit-validate.mjs',
|
|
144
166
|
'post-tooluse-frontend-slop.mjs',
|
|
@@ -147,7 +169,6 @@ const DOCUMENTED_ASYMMETRIES = {
|
|
|
147
169
|
// tool_input.file_path, neither of which any Codex bridge delivers.
|
|
148
170
|
// A manifest entry would be a silent no-op, not enforcement.
|
|
149
171
|
'post-edit-import-probe.mjs',
|
|
150
|
-
'post-bash-write-verify.mjs',
|
|
151
172
|
],
|
|
152
173
|
SubagentStart: ['subagent-telemetry.mjs'],
|
|
153
174
|
SubagentStop: ['subagent-telemetry.mjs', 'post-subagent-discovery-validator.mjs'],
|
|
@@ -247,7 +268,7 @@ function loadJson(filePath, required = true) {
|
|
|
247
268
|
try {
|
|
248
269
|
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
249
270
|
} catch (e) {
|
|
250
|
-
fail(`${filePath
|
|
271
|
+
fail(`${basename(filePath)}: ${e.message}`);
|
|
251
272
|
process.exit(1);
|
|
252
273
|
}
|
|
253
274
|
}
|
|
@@ -85,7 +85,9 @@ if (hooksPath && hooksPath.endsWith('.json')) {
|
|
|
85
85
|
console.log('');
|
|
86
86
|
console.log('--- Check 5b: mcpServers JSON validity ---');
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
// mcpServers may also be an inline object or an array (plugins-reference §
|
|
89
|
+
// Component path forms); only a .json path string is a file to parse here.
|
|
90
|
+
const mcpPath = typeof pluginData.mcpServers === 'string' ? pluginData.mcpServers : '';
|
|
89
91
|
if (mcpPath && mcpPath.endsWith('.json')) {
|
|
90
92
|
const mcpAbs = join(pluginRoot, mcpPath.replace(/^\.\//, ''));
|
|
91
93
|
if (existsSync(mcpAbs)) {
|
|
@@ -134,6 +134,7 @@ import { readLearnings } from '../learnings/io.mjs';
|
|
|
134
134
|
// the emitter's formula.
|
|
135
135
|
import { learningKeyOf } from '../learnings/kebab.mjs';
|
|
136
136
|
import { isMainModule } from '../is-main-module.mjs';
|
|
137
|
+
import { resolveLedgerPath } from '../ledger-path.mjs';
|
|
137
138
|
|
|
138
139
|
/** Directory holding the rule corpus, relative to the plugin root. */
|
|
139
140
|
const RULES_REL = path.join('.claude', 'rules');
|
|
@@ -272,8 +273,8 @@ async function indexStore(absolutePath) {
|
|
|
272
273
|
export async function inspectLearningProvenance(pluginRoot, opts = {}) {
|
|
273
274
|
/** @type {Finding[]} */
|
|
274
275
|
const findings = [];
|
|
275
|
-
const livePath = opts.livePath ??
|
|
276
|
-
const archivePath = opts.archivePath ??
|
|
276
|
+
const livePath = opts.livePath ?? resolveLedgerPath(LIVE_STORE_REL, pluginRoot);
|
|
277
|
+
const archivePath = opts.archivePath ?? resolveLedgerPath(ARCHIVE_STORE_REL, pluginRoot);
|
|
277
278
|
|
|
278
279
|
/** @type {Inspection} */
|
|
279
280
|
const result = {
|
|
@@ -61,10 +61,16 @@ export const SCAN_DIRS = Object.freeze(['skills', 'commands', 'agents', 'docs',
|
|
|
61
61
|
/** Lines on either side of an R2 operator searched for an anchor. */
|
|
62
62
|
const WINDOW = 3;
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
/**
|
|
65
|
+
* The protected store filenames: the live store plus the #1516 inbox and its
|
|
66
|
+
* receipts — append-only stores written only through `learnings/io.mjs`.
|
|
67
|
+
*/
|
|
68
|
+
const STORE = String.raw`learnings(?:-inbox(?:-ingested)?)?\.jsonl`;
|
|
69
|
+
|
|
70
|
+
const MENTION_RE = new RegExp(STORE);
|
|
71
|
+
const ANCHOR_RE = new RegExp(String.raw`${STORE}|\b(?:the|learnings) store\b|\bresult back\b|\batomic rewrite\b`, 'i');
|
|
72
|
+
const SHELL_REDIRECT_RE = new RegExp(String.raw`(?:^|[\s|;&(])(>{1,2})[ \t]*["'\`]?[^\s"'\`<>]*${STORE}\b`, 'dg');
|
|
73
|
+
const TEE_RE = new RegExp(String.raw`\btee\b(?:[ \t]+-{1,2}[\w-]+)*[ \t]+["'\`]?[^\s"'\`|]*${STORE}\b`, 'g');
|
|
68
74
|
const PROSE_OPERATOR_RE = /`>{1,2}`/g;
|
|
69
75
|
const ANY_OPERATOR_RE = /`>{1,2}`|(?:^|[\s|;&(])>{1,2}|\btee\b/;
|
|
70
76
|
const PROHIBITION_RE = /\b(NEVER|[Nn]ever|DO NOT|[Dd]o not|[Dd]on't|FORBIDDEN|forbidden|verboten)\b/;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Check: every line under `scripts/`, `hooks/` and `skills/` that builds a path
|
|
4
|
+
* into `.orchestrator/metrics` routes it through a ledger resolver (GitLab
|
|
5
|
+
* #1722). BLOCKING.
|
|
6
|
+
*
|
|
7
|
+
* ## Why
|
|
8
|
+
*
|
|
9
|
+
* The ledgers under `.orchestrator/metrics/` are gitignored in this repo
|
|
10
|
+
* (`.gitignore:57`). A writer that joins the path to the cwd, `getProjectDir()`
|
|
11
|
+
* or an unmapped repo root writes, from a linked worktree, a copy that is lost
|
|
12
|
+
* with the worktree, and a reader built the same way sees an empty ledger.
|
|
13
|
+
* Measured 2026-10-22 before #1722: 43 of 62 sites in 54 unrouted modules,
|
|
14
|
+
* two stranded ledgers in live worktrees (`eval.jsonl`, `skill-invocations.jsonl`).
|
|
15
|
+
* A first, per-FILE version of this check passed while five readers in
|
|
16
|
+
* resolver-calling modules still read the worktree copy (review of the same
|
|
17
|
+
* day) — hence the per-LINE rule below.
|
|
18
|
+
*
|
|
19
|
+
* The resolvers that answer correctly:
|
|
20
|
+
* - `resolveLedgerPath()` / `resolveLedgerRoot()` (`scripts/lib/ledger-path.mjs`)
|
|
21
|
+
* for every ledger except events and sessions — main checkout only when
|
|
22
|
+
* the ledger is gitignored in the worktree, so repos that version it keep
|
|
23
|
+
* it on the branch;
|
|
24
|
+
* - `resolveEventsLedgerRoot()` / `explainEventsLedgerRoot()` /
|
|
25
|
+
* `eventsFilePath()` (`scripts/lib/platform.mjs`, `scripts/lib/events.mjs`)
|
|
26
|
+
* for `events.jsonl` and `sessions.jsonl` (#1514, #1584);
|
|
27
|
+
* - helpers built on them (`resolveLearningsPaths`, `defaultSkillInvocationsPath`).
|
|
28
|
+
*
|
|
29
|
+
* ## Rule
|
|
30
|
+
*
|
|
31
|
+
* A HIT is a non-comment line with a `.orchestrator/metrics` string literal or
|
|
32
|
+
* the segment pair `'.orchestrator', 'metrics'` (also split over two lines,
|
|
33
|
+
* reported on the `'.orchestrator',` line). It passes when
|
|
34
|
+
* (a) a resolver is called on that line or on one of the {@link LOOKBACK}
|
|
35
|
+
* lines above it that belong to the same statement (a multi-line
|
|
36
|
+
* `path.join(` whose root argument comes first),
|
|
37
|
+
* (b) it declares an UPPER_CASE relative constant (string, `path.join(…)` or
|
|
38
|
+
* array of segments) — the join site of the constant is checked by (2), or
|
|
39
|
+
* (c) it, or the line above, carries the marker `ledger-root: <reason>` —
|
|
40
|
+
* for a line that is no ledger access or whose root is already resolved.
|
|
41
|
+
* (2) A line that passes such a constant to `join(`/`resolve(` follows the same
|
|
42
|
+
* (a)/(c) rule.
|
|
43
|
+
* A file on {@link ALLOWLIST} is skipped whole; an allowlisted file without a
|
|
44
|
+
* HIT fails, so the list cannot rot.
|
|
45
|
+
*
|
|
46
|
+
* Ceiling: (2) sees constants declared in the same file only, and (a) trusts
|
|
47
|
+
* any resolver within the lookback window. Revisit if a stranded ledger is
|
|
48
|
+
* traced to an imported constant or to a line the window wrongly covered.
|
|
49
|
+
*
|
|
50
|
+
* @module scripts/lib/validate/check-metrics-ledger-root
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
import { readFileSync } from 'node:fs';
|
|
54
|
+
import path from 'node:path';
|
|
55
|
+
import { enumerateRepoFiles } from './enumerate-repo-files.mjs';
|
|
56
|
+
import { isMainModule } from '../is-main-module.mjs';
|
|
57
|
+
|
|
58
|
+
export const SCAN_DIRS = Object.freeze(['scripts', 'hooks', 'skills']);
|
|
59
|
+
|
|
60
|
+
/** Lines above a HIT in which a resolver call still counts (multi-line joins). */
|
|
61
|
+
export const LOOKBACK = 3;
|
|
62
|
+
|
|
63
|
+
/** A string literal starting with the metrics dir, or the two-segment join. */
|
|
64
|
+
const METRICS_PATH_RE = /['"`]\.orchestrator\/metrics|['"]\.orchestrator['"]\s*,\s*['"]metrics['"]/;
|
|
65
|
+
|
|
66
|
+
/** The two-segment join split over two lines: `'.orchestrator',` ⏎ `'metrics'`. */
|
|
67
|
+
const SPLIT_HEAD_RE = /['"]\.orchestrator['"]\s*,\s*$/;
|
|
68
|
+
const SPLIT_TAIL_RE = /^\s*['"]metrics['"]/;
|
|
69
|
+
|
|
70
|
+
function isHit(lines, i) {
|
|
71
|
+
return METRICS_PATH_RE.test(lines[i]) || (SPLIT_HEAD_RE.test(lines[i]) && SPLIT_TAIL_RE.test(lines[i + 1] ?? ''));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const RESOLVER_CALL_RE =
|
|
75
|
+
/\b(?:resolveLedgerPath|resolveLedgerRoot|resolveEventsLedgerRoot|explainEventsLedgerRoot|eventsFilePath|resolveLearningsPaths|defaultSkillInvocationsPath)\s*\(/;
|
|
76
|
+
|
|
77
|
+
const MARKER_RE = /ledger-root:/;
|
|
78
|
+
|
|
79
|
+
/** `const NAME = '…'`, `= path.join('…'`, `= join('…'`, `= ['…'`, optionally inside Object.freeze( */
|
|
80
|
+
const CONST_DECL_RE =
|
|
81
|
+
/^\s*(?:export\s+)?const\s+([A-Z][A-Z0-9_]*)\s*=\s*(?:Object\.freeze\(\s*)?(?:(?:path\.)?join\(\s*|\[\s*)?['"`]\.orchestrator/;
|
|
82
|
+
|
|
83
|
+
const PATH_FN_RE = /\b(?:join|resolve)\s*\(/;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Files that build a metrics path on purpose without a resolver. Each reason
|
|
87
|
+
* is the census verdict (2026-10-22): INTENDED-LOCAL (the data belongs to this
|
|
88
|
+
* working tree), NOT-A-SINK (the string is no ledger path being opened) or
|
|
89
|
+
* ROUTED-DOWNSTREAM (the path is handed to a callee that resolves each ledger).
|
|
90
|
+
* Prefer the line marker `ledger-root: <reason>` for a single line in a file
|
|
91
|
+
* that otherwise reads ledgers.
|
|
92
|
+
*/
|
|
93
|
+
export const ALLOWLIST = Object.freeze({
|
|
94
|
+
'hooks/on-session-end.mjs':
|
|
95
|
+
'ROUTED-DOWNSTREAM: hands the launch dir\'s metrics dir to the telemetry flush; buildBatch resolves each ledger in it (scripts/lib/telemetry/sync.mjs)',
|
|
96
|
+
'hooks/pre-bash-sessions-ledger-guard.mjs':
|
|
97
|
+
'NOT-A-SINK/INTENDED-LOCAL: matches command text, and probes the exact --repo-root the repair CLI is given',
|
|
98
|
+
'scripts/lib/autopilot/flags.mjs': 'constant only: the relative DEFAULT_JSONL_PATH; the loop resolves it',
|
|
99
|
+
'scripts/lib/convergence-monitor.mjs': 'NOT-A-SINK: a label and help text; the read goes through the events ledger root',
|
|
100
|
+
'scripts/lib/playwright-driver/runner.mjs':
|
|
101
|
+
'INTENDED-LOCAL: guards the per-run artifact dir, which the artifact builder roots in this tree',
|
|
102
|
+
'scripts/lib/project-hygiene.mjs': 'NOT-A-SINK: a git ls-files pathspec over this tree\'s own index',
|
|
103
|
+
'scripts/lib/quality-gate.mjs':
|
|
104
|
+
'INTENDED-LOCAL: verification-failure bundles of this tree\'s gate run, which can carry secrets',
|
|
105
|
+
'scripts/lib/quality-gates-cache.mjs':
|
|
106
|
+
'INTENDED-LOCAL: cache keyed to this tree\'s session start ref and dependency hash',
|
|
107
|
+
'scripts/lib/scope-baseline.mjs': 'NOT-A-SINK: an exclusion glob for drift counting',
|
|
108
|
+
'scripts/lib/ux-grill/manifest.mjs': 'NOT-A-SINK: a .gitignore line for the run-artifact dir',
|
|
109
|
+
'scripts/lib/vault-mirror/retention.mjs': 'NOT-A-SINK: text inside a generated note body',
|
|
110
|
+
'scripts/lib/wave-transcript-tail.mjs': 'NOT-A-SINK: help text',
|
|
111
|
+
'scripts/relocate-vault-corpus.mjs':
|
|
112
|
+
'INTENDED-LOCAL: reads each child of --repos-root as named; mapping a worktree child would count its main twice',
|
|
113
|
+
'scripts/run-migrate-v2-cross-repo.mjs': 'INTENDED-LOCAL: migrates the repos the operator names, as named',
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
function isCommentLine(line) {
|
|
117
|
+
const t = line.trim();
|
|
118
|
+
return t.startsWith('//') || t.startsWith('*') || t.startsWith('/*');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Line `i` plus up to {@link LOOKBACK} lines above it that belong to the SAME
|
|
123
|
+
* statement — the walk stops at a line ending in `;`, `{` or `}`, or at a blank
|
|
124
|
+
* line, so a resolver call in the previous statement does not cover this one.
|
|
125
|
+
*/
|
|
126
|
+
function statementWindow(lines, i) {
|
|
127
|
+
const window = [lines[i]];
|
|
128
|
+
for (let j = i - 1; j >= 0 && window.length <= LOOKBACK; j -= 1) {
|
|
129
|
+
const prev = lines[j];
|
|
130
|
+
if (prev.trim() === '' || /[;{}]\s*$/.test(prev)) break;
|
|
131
|
+
window.unshift(prev);
|
|
132
|
+
}
|
|
133
|
+
return window.join('\n');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** (a) or (c) for line `i`. */
|
|
137
|
+
function covered(lines, i) {
|
|
138
|
+
if (RESOLVER_CALL_RE.test(statementWindow(lines, i))) return true;
|
|
139
|
+
return MARKER_RE.test(lines[i]) || (i > 0 && MARKER_RE.test(lines[i - 1]));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {{ pluginRoot: string, dirs?: readonly string[], allowlist?: Record<string, string> }} opts
|
|
144
|
+
* @returns {{ ok: boolean, findings: Array<{ file: string, line: number, kind: 'unrouted'|'unrouted-constant'|'stale-allowlist', text?: string }>, summary: { filesScanned: number, hits: number, resolved: number, constants: number, marked: number, allowlisted: number } }}
|
|
145
|
+
*/
|
|
146
|
+
export function scanMetricsLedgerRoot({ pluginRoot, dirs = SCAN_DIRS, allowlist = ALLOWLIST }) {
|
|
147
|
+
const files = enumerateRepoFiles({ repoRoot: pluginRoot, dirs: [...dirs], exts: ['.mjs'] });
|
|
148
|
+
const findings = [];
|
|
149
|
+
const seenAllowlisted = new Set();
|
|
150
|
+
const summary = { filesScanned: files.length, hits: 0, resolved: 0, constants: 0, marked: 0, allowlisted: 0 };
|
|
151
|
+
for (const abs of files) {
|
|
152
|
+
const rel = path.relative(pluginRoot, abs).split(path.sep).join('/');
|
|
153
|
+
if (/\.(test|spec)\.mjs$/.test(rel)) continue;
|
|
154
|
+
const lines = readFileSync(abs, 'utf8').split('\n');
|
|
155
|
+
const constNames = [];
|
|
156
|
+
const fileFindings = [];
|
|
157
|
+
let fileHits = 0;
|
|
158
|
+
lines.forEach((line, i) => {
|
|
159
|
+
if (isCommentLine(line) || !isHit(lines, i)) return;
|
|
160
|
+
fileHits += 1;
|
|
161
|
+
const decl = CONST_DECL_RE.exec(line);
|
|
162
|
+
if (decl) {
|
|
163
|
+
constNames.push(decl[1]);
|
|
164
|
+
summary.constants += 1;
|
|
165
|
+
} else if (covered(lines, i)) {
|
|
166
|
+
summary[RESOLVER_CALL_RE.test(statementWindow(lines, i)) ? 'resolved' : 'marked'] += 1;
|
|
167
|
+
} else {
|
|
168
|
+
fileFindings.push({ file: rel, line: i + 1, kind: 'unrouted', text: line.trim().slice(0, 160) });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
if (constNames.length > 0) {
|
|
172
|
+
const uses = new RegExp(`\\b(?:${constNames.join('|')})\\b`);
|
|
173
|
+
lines.forEach((line, i) => {
|
|
174
|
+
if (isCommentLine(line) || CONST_DECL_RE.test(line)) return;
|
|
175
|
+
if (!uses.test(line) || !PATH_FN_RE.test(line) || covered(lines, i)) return;
|
|
176
|
+
fileFindings.push({ file: rel, line: i + 1, kind: 'unrouted-constant', text: line.trim().slice(0, 160) });
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (fileHits === 0) continue;
|
|
180
|
+
summary.hits += fileHits;
|
|
181
|
+
if (Object.hasOwn(allowlist, rel)) {
|
|
182
|
+
seenAllowlisted.add(rel);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
findings.push(...fileFindings);
|
|
186
|
+
}
|
|
187
|
+
for (const rel of Object.keys(allowlist)) {
|
|
188
|
+
if (!seenAllowlisted.has(rel)) findings.push({ file: rel, line: 0, kind: 'stale-allowlist' });
|
|
189
|
+
}
|
|
190
|
+
summary.allowlisted = seenAllowlisted.size;
|
|
191
|
+
return { ok: findings.length === 0, findings, summary };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @param {string} pluginRoot absolute plugin root
|
|
196
|
+
* @returns {number} 0 = clean, 1 = findings
|
|
197
|
+
*/
|
|
198
|
+
export function runCheckMetricsLedgerRoot(pluginRoot) {
|
|
199
|
+
console.log('--- Check: .orchestrator/metrics paths go through a ledger resolver (#1722) ---');
|
|
200
|
+
const inspection = scanMetricsLedgerRoot({ pluginRoot });
|
|
201
|
+
for (const f of inspection.findings) {
|
|
202
|
+
if (f.kind === 'stale-allowlist') {
|
|
203
|
+
console.log(` FAIL: ${f.file} is on the ALLOWLIST but builds no .orchestrator/metrics path any more — drop the entry`);
|
|
204
|
+
} else {
|
|
205
|
+
console.log(
|
|
206
|
+
` FAIL: ${f.file}:${f.line} builds a .orchestrator/metrics path without resolveLedgerPath() ` +
|
|
207
|
+
'(events/sessions: resolveEventsLedgerRoot()) — from a linked worktree it reads or writes a copy ' +
|
|
208
|
+
`lost with the worktree; route it, or mark a non-ledger line with \`// ledger-root: <reason>\`: ${f.text}`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const s = inspection.summary;
|
|
213
|
+
if (inspection.ok) {
|
|
214
|
+
console.log(
|
|
215
|
+
` PASS: ${s.hits} metrics path line(s) in ${s.filesScanned} module(s): ${s.resolved} resolved, ` +
|
|
216
|
+
`${s.constants} relative constant(s), ${s.marked} marked, ${s.allowlisted} allowlisted module(s)`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
console.log('');
|
|
220
|
+
console.log(`Results: ${inspection.ok ? 1 : 0} passed, ${inspection.findings.length} failed`);
|
|
221
|
+
return inspection.ok ? 0 : 1;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (isMainModule(import.meta.url)) {
|
|
225
|
+
const args = process.argv.slice(2).filter((arg) => arg !== '--json');
|
|
226
|
+
const root = path.resolve(args[0] || process.cwd());
|
|
227
|
+
if (process.argv.includes('--json')) {
|
|
228
|
+
const inspection = scanMetricsLedgerRoot({ pluginRoot: root });
|
|
229
|
+
process.stdout.write(`${JSON.stringify(inspection, null, 2)}\n`);
|
|
230
|
+
process.exitCode = inspection.ok ? 0 : 1;
|
|
231
|
+
} else {
|
|
232
|
+
process.exitCode = runCheckMetricsLedgerRoot(root);
|
|
233
|
+
}
|
|
234
|
+
}
|