session-orchestrator 3.24.0 → 4.0.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/.agents/skills/architecture/SKILL.md +18 -0
- package/.agents/skills/autopilot/SKILL.md +17 -0
- package/.agents/skills/bootstrap/SKILL.md +20 -0
- package/.agents/skills/brainstorm/SKILL.md +22 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
- package/.agents/skills/debug/SKILL.md +22 -0
- package/.agents/skills/discovery/SKILL.md +20 -0
- package/.agents/skills/dispatcher/SKILL.md +15 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
- package/.agents/skills/ecosystem-health/SKILL.md +20 -0
- package/.agents/skills/eli5/SKILL.md +20 -0
- package/.agents/skills/eval/SKILL.md +21 -0
- package/.agents/skills/evolve/SKILL.md +21 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
- package/.agents/skills/gitlab-ops/SKILL.md +20 -0
- package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
- package/.agents/skills/grill/SKILL.md +22 -0
- package/.agents/skills/hook-development/SKILL.md +15 -0
- package/.agents/skills/mcp-builder/SKILL.md +15 -0
- package/.agents/skills/memory-cleanup/SKILL.md +21 -0
- package/.agents/skills/mode-selector/SKILL.md +17 -0
- package/.agents/skills/npm-publish/SKILL.md +16 -0
- package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
- package/.agents/skills/persona-panel/SKILL.md +17 -0
- package/.agents/skills/plan/SKILL.md +20 -0
- package/.agents/skills/playwright-driver/SKILL.md +20 -0
- package/.agents/skills/quality-gates/SKILL.md +20 -0
- package/.agents/skills/reconcile/SKILL.md +21 -0
- package/.agents/skills/remote-offload/SKILL.md +20 -0
- package/.agents/skills/repo-audit/SKILL.md +16 -0
- package/.agents/skills/session-end/SKILL.md +20 -0
- package/.agents/skills/session-plan/SKILL.md +20 -0
- package/.agents/skills/session-start/SKILL.md +20 -0
- package/.agents/skills/spinout/SKILL.md +16 -0
- package/.agents/skills/sunset-review/SKILL.md +16 -0
- package/.agents/skills/test-runner/SKILL.md +20 -0
- package/.agents/skills/tmux-layout/SKILL.md +21 -0
- package/.agents/skills/using-orchestrator/SKILL.md +17 -0
- package/.agents/skills/vault-mirror/SKILL.md +15 -0
- package/.agents/skills/vault-sync/SKILL.md +15 -0
- package/.agents/skills/wave-executor/SKILL.md +20 -0
- package/.agents/skills/write-executable-plan/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot.md +2 -2
- package/.cursor/commands/bootstrap.md +1 -1
- package/.cursor/commands/brainstorm.md +1 -1
- package/.cursor/commands/debug.md +1 -1
- package/.cursor/commands/discovery.md +1 -1
- package/.cursor/commands/dispatcher.md +2 -2
- package/.cursor/commands/eli5.md +2 -2
- package/.cursor/commands/eval.md +2 -2
- package/.cursor/commands/evolve.md +1 -1
- package/.cursor/commands/go.md +1 -1
- package/.cursor/commands/grill.md +2 -2
- package/.cursor/commands/memory-cleanup.md +2 -2
- package/.cursor/commands/persona-panel.md +1 -1
- package/.cursor/commands/plan.md +1 -1
- package/.cursor/commands/portfolio.md +1 -1
- package/.cursor/commands/reconcile.md +2 -2
- package/.cursor/commands/release.md +2 -2
- package/.cursor/commands/session.md +2 -2
- package/.cursor/commands/spinout.md +2 -2
- package/.cursor/commands/sunset-review.md +2 -2
- package/.cursor/commands/templates-ack.md +2 -2
- package/.cursor/commands/test.md +2 -2
- package/.cursor/skills/brainstorm/SKILL.md +1 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/quality-gates/SKILL.md +1 -1
- package/.cursor/skills/remote-offload/SKILL.md +1 -1
- package/.orchestrator/policy/blocked-commands.json +121 -0
- package/.orchestrator/policy/ecosystem.schema.json +66 -0
- package/.orchestrator/policy/quality-gates.example.json +16 -0
- package/.orchestrator/policy/quality-gates.schema.json +38 -0
- package/.orchestrator/policy/templates-policy.json +27 -0
- package/.orchestrator/policy/test-profiles.json +47 -0
- package/AGENTS.md +225 -0
- package/CHANGELOG.md +1125 -2
- package/NOTICE +11 -6
- package/README.md +127 -94
- package/agents/eval-judge.md +1 -1
- package/agents/skill-applied-judge.md +1 -1
- package/assets/wave-lifecycle.svg +98 -0
- package/commands/release.md +6 -3
- package/commands/session.md +18 -3
- package/docs/README.md +4 -0
- package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
- package/docs/baseline.md +67 -0
- package/docs/ci-setup.md +108 -62
- package/docs/codex-setup.md +65 -21
- package/docs/components.md +36 -15
- package/docs/cursor-setup.md +6 -2
- package/docs/events-schema.md +9 -6
- package/docs/instruction-delivery.md +62 -0
- package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
- package/docs/migration-v4.md +341 -0
- package/docs/pi-setup.md +6 -1
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +85 -19
- package/docs/scope-collision-guard.md +5 -5
- package/docs/session-config-reference.md +57 -56
- package/docs/session-config-template.md +6 -29
- package/docs/telemetry.md +157 -3
- package/docs/vault-docs-architecture.md +50 -11
- package/hooks/_lib/hook-import-set.json +1487 -0
- package/hooks/_lib/subagent-transcript.mjs +562 -0
- package/hooks/config-protection.mjs +2 -2
- package/hooks/cwd-change-restore.mjs +2 -2
- package/hooks/enforce-commands.mjs +69 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +10 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/loop-guard.mjs +3 -3
- package/hooks/on-session-end.mjs +2 -2
- package/hooks/on-session-start.mjs +103 -2
- package/hooks/on-stop.mjs +36 -11
- package/hooks/operator-steer.mjs +2 -2
- package/hooks/post-bash-write-verify.mjs +85 -0
- package/hooks/post-edit-import-probe.mjs +344 -0
- package/hooks/post-subagent-discovery-validator.mjs +187 -431
- package/hooks/post-tool-batch-wave-signal.mjs +118 -4
- package/hooks/post-tool-failure-corrective-context.mjs +2 -2
- package/hooks/post-tooluse-frontend-slop.mjs +3 -3
- package/hooks/pre-bash-destructive-guard.mjs +39 -13
- package/hooks/skill-invocation-telemetry.mjs +17 -5
- package/hooks/subagent-telemetry.mjs +13 -4
- package/monitors/monitors.json +3 -3
- package/package.json +9 -1
- package/pi/prompts/session.md +2 -2
- package/plugin.json +27 -0
- package/scripts/backfill-abandoned-sessions.mjs +50 -4
- package/scripts/backfill-learnings-from-vault.mjs +9 -3
- package/scripts/dialectic-deriver.mjs +73 -8
- package/scripts/export-hw-learnings.mjs +113 -1
- package/scripts/generate-agents-skills.mjs +378 -0
- package/scripts/generate-cursor-adapter.mjs +45 -8
- package/scripts/generate-hook-import-set.mjs +249 -0
- package/scripts/lib/agent-status.mjs +13 -2
- package/scripts/lib/auto-dream.mjs +38 -36
- package/scripts/lib/autonomy/suitability.mjs +6 -0
- package/scripts/lib/autopilot/loop.mjs +2 -2
- package/scripts/lib/ci-status-banner.mjs +220 -75
- package/scripts/lib/codex/plugin-contract.mjs +82 -6
- package/scripts/lib/config/auto-dream.mjs +2 -1
- package/scripts/lib/config/block-header.mjs +8 -0
- package/scripts/lib/config/block-preprocess.mjs +177 -0
- package/scripts/lib/config/broken-window.mjs +2 -1
- package/scripts/lib/config/cold-start.mjs +2 -1
- package/scripts/lib/config/config-protection.mjs +22 -2
- package/scripts/lib/config/context-coverage.mjs +2 -1
- package/scripts/lib/config/cross-repo.mjs +2 -1
- package/scripts/lib/config/custom-phases.mjs +2 -1
- package/scripts/lib/config/dialectic.mjs +2 -1
- package/scripts/lib/config/discovery-validator.mjs +2 -1
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
- package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
- package/scripts/lib/config/docs-orchestrator.mjs +2 -1
- package/scripts/lib/config/docs-staleness.mjs +2 -1
- package/scripts/lib/config/drift-check.mjs +2 -1
- package/scripts/lib/config/eval.mjs +2 -1
- package/scripts/lib/config/events-rotation.mjs +2 -1
- package/scripts/lib/config/evolve.mjs +8 -2
- package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
- package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
- package/scripts/lib/config/handover-gate.mjs +2 -1
- package/scripts/lib/config/health-endpoints.mjs +7 -2
- package/scripts/lib/config/issue-budget.mjs +2 -1
- package/scripts/lib/config/loop-guard.mjs +2 -1
- package/scripts/lib/config/memory.mjs +2 -1
- package/scripts/lib/config/moc-staleness.mjs +2 -1
- package/scripts/lib/config/persona-gate-wave.mjs +2 -1
- package/scripts/lib/config/private-config-dir.mjs +67 -0
- package/scripts/lib/config/reconcile.mjs +2 -1
- package/scripts/lib/config/remote-hosts.mjs +2 -1
- package/scripts/lib/config/section-extractor.mjs +7 -1
- package/scripts/lib/config/skill-evolution.mjs +2 -1
- package/scripts/lib/config/slopcheck.mjs +2 -1
- package/scripts/lib/config/state-md-lock.mjs +2 -1
- package/scripts/lib/config/templates-first.mjs +2 -1
- package/scripts/lib/config/test.mjs +2 -1
- package/scripts/lib/config/vault-integration.mjs +7 -1
- package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
- package/scripts/lib/config/vault-staleness.mjs +2 -1
- package/scripts/lib/config/vault-sync.mjs +2 -1
- package/scripts/lib/config/verification-auto-fix.mjs +2 -1
- package/scripts/lib/config/wave-reviewers.mjs +2 -1
- package/scripts/lib/config/worktree-orphans.mjs +2 -1
- package/scripts/lib/convergence-monitor.mjs +82 -16
- package/scripts/lib/dispatcher/rank.mjs +124 -48
- package/scripts/lib/ecosystem-health.mjs +16 -2
- package/scripts/lib/eval/engine.mjs +9 -1
- package/scripts/lib/eval/session-resolve.mjs +23 -4
- package/scripts/lib/events.mjs +22 -6
- package/scripts/lib/frontmatter-guard.mjs +131 -13
- package/scripts/lib/gates/gate-full.mjs +26 -0
- package/scripts/lib/gates/gate-helpers.mjs +76 -0
- package/scripts/lib/hardware-pattern-detector.mjs +18 -1
- package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
- package/scripts/lib/host-identity.mjs +50 -11
- package/scripts/lib/instruction-budget-guard.mjs +171 -5
- package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
- package/scripts/lib/learnings/io.mjs +60 -6
- package/scripts/lib/memory-proposals/store.mjs +30 -22
- package/scripts/lib/owner-config-banner.mjs +43 -6
- package/scripts/lib/owner-config-loader.mjs +21 -10
- package/scripts/lib/owner-interview.mjs +3 -3
- package/scripts/lib/owner-yaml.mjs +207 -14
- package/scripts/lib/platform.mjs +108 -15
- package/scripts/lib/plugin-update-banner.mjs +406 -0
- package/scripts/lib/project-hygiene.mjs +38 -2
- package/scripts/lib/qg-command-drift-banner.mjs +50 -12
- package/scripts/lib/quality-gate.mjs +133 -44
- package/scripts/lib/reconcile/emitter.mjs +68 -6
- package/scripts/lib/reconcile/engine.mjs +13 -4
- package/scripts/lib/reconcile/idempotency.mjs +37 -4
- package/scripts/lib/reconcile/writer.mjs +40 -18
- package/scripts/lib/session-close-backfill.mjs +67 -9
- package/scripts/lib/session-id.mjs +12 -23
- package/scripts/lib/session-identity/own-session.mjs +125 -10
- package/scripts/lib/session-lock-shape.mjs +43 -0
- package/scripts/lib/session-lock.mjs +5 -10
- package/scripts/lib/session-registry.mjs +25 -9
- package/scripts/lib/session-schema/constants.mjs +36 -2
- package/scripts/lib/session-schema/validator.mjs +38 -4
- package/scripts/lib/session-start-probes.mjs +18 -1
- package/scripts/lib/sessions-staleness-banner.mjs +18 -11
- package/scripts/lib/skill-health/join.mjs +17 -4
- package/scripts/lib/state-md.mjs +78 -0
- package/scripts/lib/sunset/walker.mjs +6 -0
- package/scripts/lib/telemetry/schema.mjs +181 -9
- package/scripts/lib/telemetry/sync.mjs +368 -12
- package/scripts/lib/validate/check-agents-skills.mjs +327 -0
- package/scripts/lib/validate/check-agents.mjs +3 -3
- package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
- package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
- package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
- package/scripts/lib/validate/check-skill-links.mjs +163 -0
- package/scripts/lib/validate/check-skill-script-paths.mjs +47 -28
- package/scripts/lib/validate/check-unwired-features.mjs +0 -2
- package/scripts/lib/validate/check-validator-registration.mjs +10 -4
- package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
- package/scripts/lib/vault-backfill/template.mjs +63 -6
- package/scripts/lib/vault-mirror/process.mjs +165 -42
- package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
- package/scripts/lib/vault-status/narrative-mirror.mjs +127 -18
- package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
- package/scripts/lib/wave-executor/remote-dispatch.mjs +5 -7
- package/scripts/lib/wave-resource-gate.mjs +8 -2
- package/scripts/lib/wave-sizing.mjs +4 -1
- package/scripts/lib/wave-transcript-tail.mjs +118 -4
- package/scripts/materialize-wave-scope.mjs +12 -5
- package/scripts/memory-propose.mjs +19 -5
- package/scripts/migrate-cold-start-seed.mjs +4 -1
- package/scripts/parse-config.mjs +60 -3
- package/scripts/release.mjs +337 -29
- package/scripts/repair-invalid-sessions.mjs +3 -3
- package/scripts/run-quality-gate.mjs +128 -11
- package/scripts/sweep-expired-learnings.mjs +90 -0
- package/scripts/sync-vault-schema.mjs +3 -1
- package/scripts/telemetry.mjs +2 -2
- package/scripts/validate-plugin.mjs +161 -0
- package/scripts/validate-wave-scope.mjs +28 -8
- package/scripts/wave-scope-binding.mjs +215 -0
- package/skills/_shared/instruction-file-resolution.md +10 -0
- package/skills/_shared/parallel-aware-preamble.md +1 -0
- package/skills/_shared/platform-tools.md +1 -1
- package/skills/_shared/state-ownership.md +1 -1
- package/skills/architecture/SKILL.md +7 -5
- package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
- package/skills/autopilot/SKILL.md +4 -18
- package/skills/claude-md-drift-check/SKILL.md +5 -1
- package/skills/claude-md-drift-check/checker.mjs +62 -2
- package/skills/convergence-monitoring/SIGNALS.md +55 -0
- package/skills/discovery/probes/vault-staleness.mjs +37 -13
- package/skills/discovery/probes-arch.md +20 -18
- package/skills/dispatcher/SKILL.md +3 -2
- package/skills/evolve/SKILL.md +65 -26
- package/skills/frontmatter-guard/SKILL.md +11 -5
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +33 -0
- package/skills/remote-offload/SKILL.md +1 -1
- package/skills/session-end/SKILL.md +18 -905
- package/skills/session-end/phase-3-6-tail.md +10 -3
- package/skills/session-end/plan-verification.md +221 -155
- package/skills/session-end/references/phase-2-quality-gate.md +93 -0
- package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
- package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
- package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
- package/skills/session-end/references/session-summary-template.md +62 -0
- package/skills/session-plan/SKILL.md +49 -0
- package/skills/session-start/SKILL.md +22 -904
- package/skills/session-start/phase-8-5-express-path.md +1 -1
- package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
- package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
- package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
- package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
- package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
- package/skills/vault-sync/validator.mjs +21 -27
- package/skills/wave-executor/SKILL.md +15 -1
- package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
- package/skills/wave-executor/references/wave-loop-review.md +570 -0
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
- package/skills/wave-executor/wave-loop.md +14 -1309
- package/templates/_shared/journey-manifest.md +10 -6
- package/.cursor/commands/autopilot-multi.md +0 -14
- package/.cursor/commands/contract-version-bump.md +0 -14
- package/.cursor/commands/journey-audit.md +0 -14
- package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
- package/.cursor/skills/daily/SKILL.md +0 -12
- package/.cursor/skills/domain-model/SKILL.md +0 -13
- package/.cursor/skills/journey-audit/SKILL.md +0 -13
- package/.cursor/skills/skill-creator/SKILL.md +0 -13
- package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
- package/commands/autopilot-multi.md +0 -74
- package/commands/contract-version-bump.md +0 -28
- package/commands/journey-audit.md +0 -43
- package/pi/prompts/autopilot-multi.md +0 -12
- package/pi/prompts/contract-version-bump.md +0 -12
- package/pi/prompts/journey-audit.md +0 -12
- package/scripts/autopilot-multi.mjs +0 -885
- package/scripts/backfill-learnings-expires.mjs +0 -196
- package/scripts/backfill-learnings.mjs +0 -203
- package/scripts/fleet-instruction-scan.mjs +0 -141
- package/scripts/lib/autopilot/dep-graph.mjs +0 -417
- package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
- package/scripts/lib/webhook-url.mjs +0 -105
- package/scripts/lifecycle-sim-v6.mjs +0 -347
- package/scripts/migrate-learnings-jsonl.mjs +0 -189
- package/scripts/migrate-subagents-jsonl.mjs +0 -196
- package/scripts/upload-social-preview.mjs +0 -316
- package/skills/_shared/model-selection.md +0 -64
- package/skills/contract-version-bump/SKILL.md +0 -219
- package/skills/daily/SKILL.md +0 -222
- package/skills/daily/generate.sh +0 -92
- package/skills/daily/templates/daily.md.tpl +0 -36
- package/skills/journey-audit/SKILL.md +0 -270
- package/skills/skill-creator/SKILL.md +0 -168
- package/skills/ubiquitous-language/SKILL.md +0 -97
- package/skills/vault-sync/package-lock.json +0 -40
- /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
- /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plugin-update-banner.mjs — "is the plugin that is RUNNING behind the plugin
|
|
3
|
+
* that is PUBLISHED?" (#nnn, d7 R2).
|
|
4
|
+
*
|
|
5
|
+
* ── The gap this closes (measured 2026-09-06) ────────────────────────────────
|
|
6
|
+
* The operator's host ran the marketplace-cache copy at **3.19.0**, installed
|
|
7
|
+
* 2026-08-09, while the repo and npm stood at the **3.24** line — five minors, four
|
|
8
|
+
* weeks, and not one warning. Three independent reasons, none of which is a
|
|
9
|
+
* bug in isolation:
|
|
10
|
+
*
|
|
11
|
+
* 1. session-start Phase 4's plugin-freshness probe shells out to
|
|
12
|
+
* `git -C <plugin-dir> log -1`. A marketplace cache is a FILE COPY, so it
|
|
13
|
+
* answers `fatal: not a git repository` (and 28 d < the 30 d threshold
|
|
14
|
+
* anyway).
|
|
15
|
+
* 2. `classifyVersionMismatch()` in `bootstrap-lock-freshness.mjs` downgrades
|
|
16
|
+
* everything below a MAJOR jump to `info` — five minors is "no action
|
|
17
|
+
* required" BY DESIGN.
|
|
18
|
+
* 3. `bootstrap.lock`'s `refreshed-plugin-version` was stamped from the
|
|
19
|
+
* CHECKOUT's package.json while 3.19.0 was the code actually loaded.
|
|
20
|
+
*
|
|
21
|
+
* All three share one root: **no code anywhere compared installed against
|
|
22
|
+
* available.** Measured the same day —
|
|
23
|
+
* `grep -rln "registry.npmjs.org\|dist-tags" scripts/ hooks/ skills/` returned
|
|
24
|
+
* exactly one file, `scripts/release.mjs`, which is the PUBLISHER.
|
|
25
|
+
*
|
|
26
|
+
* ── Two contract rules that are load-bearing ─────────────────────────────────
|
|
27
|
+
* **Fail silent, never optimistic (#1031).** Offline, non-2xx, malformed JSON,
|
|
28
|
+
* timeout, unusable cache — every one of them returns `null`, which means "no
|
|
29
|
+
* statement". None of them may collapse into "you are up to date": five
|
|
30
|
+
* distinct failure states rendering as one all-clear is the exact defect #1031
|
|
31
|
+
* names, and here it would re-create the four silent weeks above.
|
|
32
|
+
*
|
|
33
|
+
* **Installed = the code that is RUNNING, not the checkout.** `pluginRoot`
|
|
34
|
+
* defaults to this module's own package root (`../..` from `scripts/lib/`), so
|
|
35
|
+
* the answer cannot disagree with the loaded bytes. Reading
|
|
36
|
+
* `$CLAUDE_PLUGIN_ROOT` instead is what produced reason (3) above: it resolved
|
|
37
|
+
* to the checkout while the cache copy was executing.
|
|
38
|
+
*
|
|
39
|
+
* @module scripts/lib/plugin-update-banner
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { readFileSync } from 'node:fs';
|
|
43
|
+
import { join, resolve } from 'node:path';
|
|
44
|
+
|
|
45
|
+
import { writeJsonAtomicSync } from './io.mjs';
|
|
46
|
+
import { readPluginVersionFromPackageJson } from './bootstrap-lock-freshness.mjs';
|
|
47
|
+
|
|
48
|
+
/** npm registry document for the `latest` dist-tag of this package. */
|
|
49
|
+
export const REGISTRY_URL = 'https://registry.npmjs.org/session-orchestrator/latest';
|
|
50
|
+
|
|
51
|
+
/** Filename of the per-repo latest-version cache inside `cacheDir`. */
|
|
52
|
+
export const CACHE_FILENAME = 'plugin-latest.json';
|
|
53
|
+
|
|
54
|
+
/** Cache lifetime: one fetch per day per repo, at most. */
|
|
55
|
+
export const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
56
|
+
|
|
57
|
+
/** Registry request budget. A session start must never wait on the network. */
|
|
58
|
+
export const FETCH_TIMEOUT_MS = 2000;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Env kill-switches, in the order they are checked.
|
|
62
|
+
*
|
|
63
|
+
* `SO_DISABLE_UPDATE_CHECK` is this probe's own switch; the other two are the
|
|
64
|
+
* standard offline flags the telemetry path already honours
|
|
65
|
+
* (`scripts/lib/telemetry/consent.mjs`, `.claude/rules/cross-session-messaging.md`
|
|
66
|
+
* § CSM-005). This probe talks to a PUBLIC REGISTRY on session start, so it has
|
|
67
|
+
* to be at least as easy to silence as telemetry is — an operator who set
|
|
68
|
+
* `DO_NOT_TRACK` did not consent to a per-session npm request either.
|
|
69
|
+
*/
|
|
70
|
+
export const KILL_SWITCH_ENV_KEYS = [
|
|
71
|
+
'SO_DISABLE_UPDATE_CHECK',
|
|
72
|
+
'DO_NOT_TRACK',
|
|
73
|
+
'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Truthiness for an env kill-switch: set, non-empty, not `0`, not `false`.
|
|
78
|
+
*
|
|
79
|
+
* Deliberately a local four-liner rather than an import: the equivalent in
|
|
80
|
+
* `telemetry/consent.mjs` is module-private (not exported), and widening its
|
|
81
|
+
* visibility to reuse four lines would couple this probe to the telemetry
|
|
82
|
+
* module's internals for no gain.
|
|
83
|
+
*
|
|
84
|
+
* @param {unknown} raw
|
|
85
|
+
* @returns {boolean}
|
|
86
|
+
*/
|
|
87
|
+
function isTruthyFlag(raw) {
|
|
88
|
+
if (raw === undefined || raw === null) return false;
|
|
89
|
+
const t = String(raw).trim();
|
|
90
|
+
if (t === '' || t === '0') return false;
|
|
91
|
+
return t.toLowerCase() !== 'false';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {Record<string, string|undefined>} env
|
|
96
|
+
* @returns {string|null} the name of the first kill-switch that is set, else null.
|
|
97
|
+
*/
|
|
98
|
+
function firstActiveKillSwitch(env) {
|
|
99
|
+
for (const key of KILL_SWITCH_ENV_KEYS) {
|
|
100
|
+
if (isTruthyFlag(env?.[key])) return key;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Is the update check switched off for this process?
|
|
107
|
+
*
|
|
108
|
+
* Exported because the SessionStart hook gates the `plugin_version_latest`
|
|
109
|
+
* event field on it as well: one switch, one meaning. An operator who sets
|
|
110
|
+
* `SO_DISABLE_UPDATE_CHECK=1` has turned the FEATURE off, not merely the
|
|
111
|
+
* request — reading a leftover cache file into the session record afterwards
|
|
112
|
+
* would be the same surprise as a "disabled" telemetry path that still writes.
|
|
113
|
+
*
|
|
114
|
+
* @param {Record<string,string|undefined>} [env]
|
|
115
|
+
* @returns {boolean}
|
|
116
|
+
*/
|
|
117
|
+
export function isUpdateCheckDisabled(env = process.env) {
|
|
118
|
+
return firstActiveKillSwitch(env) !== null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Parse a semver-ish string into `[major, minor, patch]`.
|
|
123
|
+
* @param {unknown} v
|
|
124
|
+
* @returns {[number, number, number]|null} null when unparseable.
|
|
125
|
+
*/
|
|
126
|
+
function parseSemver(v) {
|
|
127
|
+
if (typeof v !== 'string') return null;
|
|
128
|
+
const m = v.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
129
|
+
if (!m) return null;
|
|
130
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The default `pluginRoot`: the package root of the module that is executing.
|
|
135
|
+
*
|
|
136
|
+
* `scripts/lib/plugin-update-banner.mjs` → `../..`. See the module docstring
|
|
137
|
+
* for why this is NOT `$CLAUDE_PLUGIN_ROOT`.
|
|
138
|
+
*
|
|
139
|
+
* @returns {string}
|
|
140
|
+
*/
|
|
141
|
+
function ownPluginRoot() {
|
|
142
|
+
return resolve(import.meta.dirname, '..', '..');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The version of the plugin that is RUNNING.
|
|
147
|
+
*
|
|
148
|
+
* Exported so the SessionStart hook can stamp `plugin_version_installed` on
|
|
149
|
+
* every `orchestrator.session.started` record without re-deriving "which root
|
|
150
|
+
* is the running one" — the question reason (3) in the module docstring got
|
|
151
|
+
* wrong. Delegates to `bootstrap-lock-freshness.mjs`'s reader rather than
|
|
152
|
+
* opening package.json a second time.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} [pluginRoot] Override for tests / callers that already know
|
|
155
|
+
* a specific install root.
|
|
156
|
+
* @returns {string|null} null when package.json is absent or unparseable.
|
|
157
|
+
*/
|
|
158
|
+
export function readInstalledPluginVersion(pluginRoot) {
|
|
159
|
+
const root = typeof pluginRoot === 'string' && pluginRoot.length > 0 ? pluginRoot : ownPluginRoot();
|
|
160
|
+
return readPluginVersionFromPackageJson(root);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Read the cached latest-version record, honouring the 24 h TTL.
|
|
165
|
+
*
|
|
166
|
+
* Exported because the SessionStart hook needs the resolved `latest` for the
|
|
167
|
+
* `orchestrator.session.started` event even on the runs where
|
|
168
|
+
* {@link checkPluginUpdate} returns `null` — most importantly the
|
|
169
|
+
* installed-equals-latest run, which is the DENOMINATOR of the probe's firing
|
|
170
|
+
* rate (`.claude/rules/host-resources.md` HR-105). A verdict whose non-firing
|
|
171
|
+
* case leaves no record is unfalsifiable, which is how the old rule set fired
|
|
172
|
+
* on 99% of starts for four months undetected.
|
|
173
|
+
*
|
|
174
|
+
* @param {{cacheDir?: string, now?: number}} [opts]
|
|
175
|
+
* @returns {{version: string, fetchedAt: string}|null} null when absent, stale,
|
|
176
|
+
* unreadable, or malformed — never throws.
|
|
177
|
+
*/
|
|
178
|
+
export function readCachedLatest({ cacheDir, now = Date.now() } = {}) {
|
|
179
|
+
if (typeof cacheDir !== 'string' || cacheDir.length === 0) return null;
|
|
180
|
+
try {
|
|
181
|
+
const parsed = JSON.parse(readFileSync(join(cacheDir, CACHE_FILENAME), 'utf8'));
|
|
182
|
+
if (typeof parsed?.version !== 'string' || typeof parsed?.fetched_at !== 'string') return null;
|
|
183
|
+
const fetchedMs = Date.parse(parsed.fetched_at);
|
|
184
|
+
if (Number.isNaN(fetchedMs)) return null;
|
|
185
|
+
// A future-dated stamp (clock skew, hand-edited file) is treated as stale
|
|
186
|
+
// rather than trusted forever: `now - fetchedMs` would be negative and pass
|
|
187
|
+
// the TTL test for as long as the skew lasts.
|
|
188
|
+
const ageMs = now - fetchedMs;
|
|
189
|
+
if (ageMs < 0 || ageMs >= CACHE_TTL_MS) return null;
|
|
190
|
+
return { version: parsed.version, fetchedAt: parsed.fetched_at };
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Resolve the published `latest` version: fresh cache first, network second.
|
|
198
|
+
*
|
|
199
|
+
* @param {{cacheDir: string, now: number, fetchImpl: Function}} opts
|
|
200
|
+
* @returns {Promise<string|null>} null on ANY failure (see the module docstring).
|
|
201
|
+
*/
|
|
202
|
+
async function resolveLatestVersion({ cacheDir, now, fetchImpl }) {
|
|
203
|
+
const cached = readCachedLatest({ cacheDir, now });
|
|
204
|
+
if (cached !== null) return cached.version;
|
|
205
|
+
|
|
206
|
+
// Declared without an initialiser on purpose: every path out of the try
|
|
207
|
+
// either returns or assigns, so a `= null` seed would be dead (no-useless-assignment).
|
|
208
|
+
let version;
|
|
209
|
+
try {
|
|
210
|
+
const res = await fetchImpl(REGISTRY_URL, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
211
|
+
if (!res || res.ok !== true) return null;
|
|
212
|
+
const body = await res.json();
|
|
213
|
+
if (typeof body?.version !== 'string' || body.version.length === 0) return null;
|
|
214
|
+
version = body.version;
|
|
215
|
+
} catch {
|
|
216
|
+
// Offline, DNS failure, timeout, malformed JSON — all indistinguishable
|
|
217
|
+
// here and all equally NOT a statement about freshness. Nothing is written:
|
|
218
|
+
// a failed fetch must never leave a cache entry a later run would read as
|
|
219
|
+
// an answer (#1031).
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Best-effort persistence. A write failure costs one extra request next
|
|
224
|
+
// session; it must not discard a verdict we already measured correctly.
|
|
225
|
+
writeJsonAtomicSync(
|
|
226
|
+
join(cacheDir, CACHE_FILENAME),
|
|
227
|
+
{ version, fetched_at: new Date(now).toISOString() },
|
|
228
|
+
{ tmpPrefix: '.plugin-latest.tmp' },
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
return version;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The four platforms this probe has a SOURCED update recipe for. Each string
|
|
236
|
+
* is quoted from the doc that actually documents it, never invented:
|
|
237
|
+
*
|
|
238
|
+
* - claude: README.md §"Upgrade" (`/plugin update ...`).
|
|
239
|
+
* - codex: docs/codex-setup.md §"Refresh and Explicit Cache Invalidation" —
|
|
240
|
+
* the short-form marketplace install path, labelled Recommended
|
|
241
|
+
* there (`codex plugin marketplace upgrade` + `codex plugin add`).
|
|
242
|
+
* - cursor: README.md's per-platform install row + docs/cursor-setup.md —
|
|
243
|
+
* re-running the same install script is the documented recipe
|
|
244
|
+
* (README §"Upgrade": "...followed by the same install
|
|
245
|
+
* script you originally ran").
|
|
246
|
+
* - pi: the PRIMARY install path is `pi install npm:session-orchestrator`
|
|
247
|
+
* (docs/pi-setup.md §"Option 1", `docs/pi-setup.md:16`), not the
|
|
248
|
+
* checkout — so the instruction leads with re-running that exact
|
|
249
|
+
* documented command. There is still no separately-documented
|
|
250
|
+
* `pi update`/upgrade subcommand anywhere in this repo (checked
|
|
251
|
+
* again here), so this is a repeat of the documented INSTALL
|
|
252
|
+
* command, never an invented one — same honesty bar as the rest
|
|
253
|
+
* of this table. `--settings-only` only rewrites
|
|
254
|
+
* `.pi/settings.json`; it never touches an npm-installed copy, so
|
|
255
|
+
* the checkout + `pi-install.mjs --settings-only` recipe (README
|
|
256
|
+
* §"Upgrade", docs/pi-setup.md §"Option 2/3") is named only as the
|
|
257
|
+
* fallback for a dev-fallback registration, never as the primary
|
|
258
|
+
* remedy.
|
|
259
|
+
*
|
|
260
|
+
* @type {Record<"claude"|"codex"|"cursor"|"pi", string>}
|
|
261
|
+
*/
|
|
262
|
+
const PLATFORM_UPDATE_INSTRUCTIONS = {
|
|
263
|
+
claude: '/plugin update session-orchestrator@kanevry, then restart Claude Code',
|
|
264
|
+
codex:
|
|
265
|
+
'run `codex plugin marketplace upgrade kanevry && codex plugin add session-orchestrator@kanevry` ' +
|
|
266
|
+
'(docs/codex-setup.md), then restart Codex',
|
|
267
|
+
cursor:
|
|
268
|
+
'run `git pull && npm install && node scripts/cursor-install.mjs <project-dir>` ' +
|
|
269
|
+
'(docs/cursor-setup.md), then reload Cursor',
|
|
270
|
+
pi:
|
|
271
|
+
'run `pi install npm:session-orchestrator` again (docs/pi-setup.md, the primary path); ' +
|
|
272
|
+
'dev-fallback checkout installs instead run ' +
|
|
273
|
+
'`git pull && npm install && node scripts/pi-install.mjs <project-dir> --settings-only`, ' +
|
|
274
|
+
'then restart Pi',
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Fallback instruction for any platform value this probe has no sourced
|
|
279
|
+
* recipe for — a future harness, a malformed override, or (see
|
|
280
|
+
* {@link resolvePlatformFromEnv}) an environment with no platform signal at
|
|
281
|
+
* all reaching a code path other than the default. Never omits an actionable
|
|
282
|
+
* step: the one universal truth about this package is that it publishes to
|
|
283
|
+
* npm (`docs/npm-publish` runbook), so that is what the fallback names.
|
|
284
|
+
* @type {string}
|
|
285
|
+
*/
|
|
286
|
+
const GENERIC_UPDATE_INSTRUCTION = 'run `npm update -g session-orchestrator`';
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Best-effort platform guess from environment variables alone — the first
|
|
290
|
+
* two precedence steps of `scripts/lib/platform.mjs`'s `detectPlatform()`
|
|
291
|
+
* (an explicit `SO_PLATFORM` override, then each harness's own compatibility
|
|
292
|
+
* env var), duplicated locally rather than imported.
|
|
293
|
+
*
|
|
294
|
+
* Deliberately NOT an import: this module sits behind the SessionStart
|
|
295
|
+
* hook's own lazy `await import()`, and `hooks/_lib/hook-import-set.json`
|
|
296
|
+
* tracks its closure — adding platform.mjs as a dependency here is a
|
|
297
|
+
* wave-scope change, not a two-line addition, so the fallback stays local
|
|
298
|
+
* exactly like `isTruthyFlag` above (same rationale, same precedent).
|
|
299
|
+
* `detectPlatform()`'s third step — a filesystem walk from `cwd` for marker
|
|
300
|
+
* directories — is left out on purpose: it needs `node:fs`/`node:path`
|
|
301
|
+
* traversal this probe has no other reason to carry, and every non-Claude
|
|
302
|
+
* harness already sets its own compat env var, so the walk would only ever
|
|
303
|
+
* re-confirm the same `'claude'` default this function already falls back to.
|
|
304
|
+
*
|
|
305
|
+
* @param {Record<string,string|undefined>} env
|
|
306
|
+
* @returns {"claude"|"codex"|"cursor"|"pi"}
|
|
307
|
+
*/
|
|
308
|
+
function resolvePlatformFromEnv(env) {
|
|
309
|
+
const explicit = String(env?.SO_PLATFORM ?? '').trim().toLowerCase();
|
|
310
|
+
if (explicit === 'claude' || explicit === 'codex' || explicit === 'cursor' || explicit === 'pi') {
|
|
311
|
+
return explicit;
|
|
312
|
+
}
|
|
313
|
+
if (String(env?.CLAUDE_PLUGIN_ROOT ?? '').trim() !== '') return 'claude';
|
|
314
|
+
if (String(env?.CODEX_PLUGIN_ROOT ?? '').trim() !== '') return 'codex';
|
|
315
|
+
if (String(env?.CURSOR_RULES_DIR ?? '').trim() !== '') return 'cursor';
|
|
316
|
+
if (String(env?.PI_PLUGIN_ROOT ?? '').trim() !== '') return 'pi';
|
|
317
|
+
return 'claude';
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Compare the running plugin version against the published `latest` and, when
|
|
322
|
+
* it is a MINOR or MAJOR behind, produce one operator-facing banner line.
|
|
323
|
+
*
|
|
324
|
+
* Patch-only drift is deliberately silent: a patch is by definition a fix with
|
|
325
|
+
* no surface change, and a banner that fires on every patch release is a
|
|
326
|
+
* banner an operator learns to skip (`.claude/rules/host-resources.md` HR-101 —
|
|
327
|
+
* a signal may only warn if it is rare).
|
|
328
|
+
*
|
|
329
|
+
* The message is English by default (this package's docs — README, CHANGELOG
|
|
330
|
+
* — are English; German is an `owner.yaml` per-operator tonality signal, not a
|
|
331
|
+
* package default, and this probe reaches third-party npm/marketplace
|
|
332
|
+
* consumers who never opted into either) and its remedy instruction is
|
|
333
|
+
* platform-aware: a Codex/Cursor/Pi consumer has no `/plugin update` command,
|
|
334
|
+
* so the CLAUDE-only remedy text is wrong for three of the four harnesses
|
|
335
|
+
* that reach this probe.
|
|
336
|
+
*
|
|
337
|
+
* @param {object} [opts]
|
|
338
|
+
* @param {string} [opts.pluginRoot] Package root to read `version` from.
|
|
339
|
+
* Defaults to the RUNNING module's own root — see the module docstring.
|
|
340
|
+
* @param {string} [opts.cacheDir] Directory holding `plugin-latest.json`,
|
|
341
|
+
* normally `<repoRoot>/.orchestrator/runtime`. Absent → `null`, no request:
|
|
342
|
+
* without a cache this probe would issue one npm request per session start.
|
|
343
|
+
* @param {Record<string,string|undefined>} [opts.env] Defaults to `process.env`.
|
|
344
|
+
* @param {number} [opts.now] Injected clock (ms). Defaults to `Date.now()`.
|
|
345
|
+
* @param {Function} [opts.fetchImpl] Injected fetch. Defaults to `globalThis.fetch`.
|
|
346
|
+
* @param {"claude"|"codex"|"cursor"|"pi"} [opts.platform] The harness driving
|
|
347
|
+
* this session, when the caller already knows it (the SessionStart hook
|
|
348
|
+
* computes this once and could pass it straight through). Falls back to
|
|
349
|
+
* {@link resolvePlatformFromEnv} on `opts.env` when omitted — never throws,
|
|
350
|
+
* never leaves the instruction generic just because the caller didn't wire
|
|
351
|
+
* the parameter through yet.
|
|
352
|
+
* @returns {Promise<{severity: 'warn', message: string, installed: string, latest: string}|null>}
|
|
353
|
+
* `null` means NO STATEMENT — never "up to date".
|
|
354
|
+
*/
|
|
355
|
+
export async function checkPluginUpdate({
|
|
356
|
+
pluginRoot,
|
|
357
|
+
cacheDir,
|
|
358
|
+
env = process.env,
|
|
359
|
+
now = Date.now(),
|
|
360
|
+
fetchImpl,
|
|
361
|
+
platform,
|
|
362
|
+
} = {}) {
|
|
363
|
+
if (isUpdateCheckDisabled(env)) return null;
|
|
364
|
+
if (typeof cacheDir !== 'string' || cacheDir.length === 0) return null;
|
|
365
|
+
|
|
366
|
+
const installed = readInstalledPluginVersion(pluginRoot);
|
|
367
|
+
const installedParts = parseSemver(installed);
|
|
368
|
+
if (installedParts === null) return null;
|
|
369
|
+
|
|
370
|
+
const fetcher = typeof fetchImpl === 'function' ? fetchImpl : globalThis.fetch;
|
|
371
|
+
if (typeof fetcher !== 'function') return null;
|
|
372
|
+
|
|
373
|
+
const latest = await resolveLatestVersion({ cacheDir, now, fetchImpl: fetcher });
|
|
374
|
+
const latestParts = parseSemver(latest);
|
|
375
|
+
if (latestParts === null) return null;
|
|
376
|
+
|
|
377
|
+
const [instMajor, instMinor] = installedParts;
|
|
378
|
+
const [latestMajor, latestMinor] = latestParts;
|
|
379
|
+
|
|
380
|
+
let count;
|
|
381
|
+
let unit;
|
|
382
|
+
if (latestMajor > instMajor) {
|
|
383
|
+
count = latestMajor - instMajor;
|
|
384
|
+
unit = count === 1 ? 'Major' : 'Majors';
|
|
385
|
+
} else if (latestMajor === instMajor && latestMinor > instMinor) {
|
|
386
|
+
count = latestMinor - instMinor;
|
|
387
|
+
unit = count === 1 ? 'Minor' : 'Minors';
|
|
388
|
+
} else {
|
|
389
|
+
// Equal, patch-only drift, or the installed build is AHEAD of the registry
|
|
390
|
+
// (a local checkout between releases — the normal state in this repo).
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const resolvedPlatform =
|
|
395
|
+
typeof platform === 'string' && platform.length > 0 ? platform : resolvePlatformFromEnv(env);
|
|
396
|
+
const instruction = PLATFORM_UPDATE_INSTRUCTIONS[resolvedPlatform] ?? GENERIC_UPDATE_INSTRUCTION;
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
severity: 'warn',
|
|
400
|
+
message:
|
|
401
|
+
`⚠ session-orchestrator ${installed} installed, ${latest} available ` +
|
|
402
|
+
`(${count} ${unit} behind) — ${instruction}.`,
|
|
403
|
+
installed,
|
|
404
|
+
latest,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
@@ -122,6 +122,42 @@ export function checkReleaseHygiene(repoRoot, driftCommits = DEFAULT_RELEASE_DRI
|
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* The plugin's own runtime output under `.orchestrator/` is not a stray.
|
|
127
|
+
*
|
|
128
|
+
* Measured on a fresh consumer repo: the very first `SessionStart` hook writes
|
|
129
|
+
* `.orchestrator/` state, and the banner rendered on that same run accused the
|
|
130
|
+
* operator of 5 files "neither tracked nor ignored" — all 5 written seconds
|
|
131
|
+
* earlier by the tool that was complaining (`find <consumer>/.orchestrator -type f`
|
|
132
|
+
* returned exactly the 5 reported files). A first-run accusation about the
|
|
133
|
+
* tool's own output is the single loudest false positive this probe can emit.
|
|
134
|
+
*
|
|
135
|
+
* Excluding here rather than having bootstrap append a `.gitignore` line is
|
|
136
|
+
* deliberate, and it does NOT weaken the check for genuine strays:
|
|
137
|
+
*
|
|
138
|
+
* - `.orchestrator/` is only PARTLY ignorable. `.orchestrator/policy/*.json`
|
|
139
|
+
* and `.orchestrator/steering/*.md` are meant to be TRACKED — `checkStaleArtifacts`
|
|
140
|
+
* below depends on exactly that. A blanket ignore line would tell git to
|
|
141
|
+
* ignore files the plugin wants versioned, which is a worse defect than the
|
|
142
|
+
* one it fixes, and it would mutate a consumer repo uninvited.
|
|
143
|
+
* - Untracked mass under `.orchestrator/` is already covered, by a check built
|
|
144
|
+
* for it: `checkStaleArtifacts` (H3) counts untracked AGED files there with
|
|
145
|
+
* its own retained-artifact carve-outs. Nothing goes unwatched.
|
|
146
|
+
* - Everything outside `.orchestrator/` is untouched, which is where a real
|
|
147
|
+
* "nothing currently decides" stray lives.
|
|
148
|
+
*
|
|
149
|
+
* Prefix match on the porcelain path. `git status --porcelain` quotes a path only
|
|
150
|
+
* when it carries special characters, so the optional leading quote is stripped
|
|
151
|
+
* before comparing.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} porcelainPath path field of a `?? ` porcelain line
|
|
154
|
+
* @returns {boolean}
|
|
155
|
+
*/
|
|
156
|
+
function isOwnRuntimeArtifact(porcelainPath) {
|
|
157
|
+
const p = porcelainPath.replace(/^"/, '');
|
|
158
|
+
return p === '.orchestrator' || p.startsWith('.orchestrator/');
|
|
159
|
+
}
|
|
160
|
+
|
|
125
161
|
/**
|
|
126
162
|
* H2 — Ignored working-tree ballast.
|
|
127
163
|
*
|
|
@@ -150,7 +186,7 @@ export function checkIgnoredBallast(repoRoot, ballastMb = DEFAULT_BALLAST_MB) {
|
|
|
150
186
|
let untrackedUnignored = 0;
|
|
151
187
|
for (const line of ignored.split('\n').filter(Boolean)) {
|
|
152
188
|
if (line.startsWith('!! ')) ignoredPaths.push(line.slice(3));
|
|
153
|
-
else if (line.startsWith('?? ')) untrackedUnignored++;
|
|
189
|
+
else if (line.startsWith('?? ') && !isOwnRuntimeArtifact(line.slice(3))) untrackedUnignored++;
|
|
154
190
|
}
|
|
155
191
|
|
|
156
192
|
// Size only the top-level ignored entries — recursing every path would cost
|
|
@@ -183,7 +219,7 @@ export function checkIgnoredBallast(repoRoot, ballastMb = DEFAULT_BALLAST_MB) {
|
|
|
183
219
|
findings.push({
|
|
184
220
|
check: 'untracked-unignored',
|
|
185
221
|
fixable: false,
|
|
186
|
-
message: `${untrackedUnignored} file(s) are neither tracked nor ignored — either commit them or add them to .gitignore, since nothing currently decides`,
|
|
222
|
+
message: `${untrackedUnignored} file(s) are neither tracked nor ignored (excluding the plugin's own .orchestrator/ runtime output) — either commit them or add them to .gitignore, since nothing currently decides`,
|
|
187
223
|
});
|
|
188
224
|
}
|
|
189
225
|
|
|
@@ -9,21 +9,31 @@
|
|
|
9
9
|
* Used by session-start Phase 4 alongside other freshness/drift probes.
|
|
10
10
|
*
|
|
11
11
|
* Config-read path: this module reads Session Config exclusively via
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
14
|
-
* which means absent `*-command` keys cannot trigger spurious
|
|
15
|
-
* `PROJECT_DEFAULTS`. Drift is only reported when a value is
|
|
16
|
-
* AND differs from the corresponding `PROJECT_DEFAULTS` entry.
|
|
12
|
+
* `loadCommandsFromSessionConfigDetailed` from `./quality-gate.mjs`. Its
|
|
13
|
+
* `commands` half is a partial object (missing keys are absent — no default
|
|
14
|
+
* substitution), which means absent `*-command` keys cannot trigger spurious
|
|
15
|
+
* drift against `PROJECT_DEFAULTS`. Drift is only reported when a value is
|
|
16
|
+
* explicitly set AND differs from the corresponding `PROJECT_DEFAULTS` entry.
|
|
17
|
+
*
|
|
18
|
+
* THREE return states (#1031 follow-up), not two — the same contract
|
|
19
|
+
* `ci-status-banner.mjs` established:
|
|
20
|
+
* - `null` — the config was READ and nothing drifts
|
|
21
|
+
* (including "this repo has no Session
|
|
22
|
+
* Config at all").
|
|
23
|
+
* - `{severity:'warn', message}` — real drift, one line per deviating key.
|
|
24
|
+
* - `{severity:'warn', message, degraded}` — the config could NOT be read.
|
|
25
|
+
* Previously this collapsed onto `null`, i.e. onto "no drift" — an
|
|
26
|
+
* all-clear derived from a measurement that never happened.
|
|
17
27
|
*
|
|
18
28
|
* Cross-references:
|
|
19
29
|
* - .claude/rules/quality-gates-autofix.md § Session Config Command Injection
|
|
20
|
-
* - scripts/lib/quality-gate.mjs `
|
|
30
|
+
* - scripts/lib/quality-gate.mjs `loadCommandsFromSessionConfigDetailed()`
|
|
21
31
|
*
|
|
22
32
|
* @see #525 (Pattern 4 Auto-Fix-Loop residuals)
|
|
23
33
|
* @see #526 (Pattern 4 banner ecosystem coherence)
|
|
24
34
|
*/
|
|
25
35
|
|
|
26
|
-
import {
|
|
36
|
+
import { loadCommandsFromSessionConfigDetailed } from './quality-gate.mjs';
|
|
27
37
|
|
|
28
38
|
/**
|
|
29
39
|
* Default *-command values for the session-orchestrator plugin.
|
|
@@ -37,11 +47,28 @@ export const PROJECT_DEFAULTS = Object.freeze({
|
|
|
37
47
|
test: 'npm test',
|
|
38
48
|
});
|
|
39
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Build the third return state: the config could not be read.
|
|
52
|
+
*
|
|
53
|
+
* Distinct from `null` on purpose — `null` in the banner contract reads as
|
|
54
|
+
* "checked, all clear", which a failed read has not established.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} reason — a `CONFIG_READ_DEGRADED_REASONS` member.
|
|
57
|
+
* @returns {{severity: 'warn', message: string, degraded: string}}
|
|
58
|
+
*/
|
|
59
|
+
function degradedBanner(reason) {
|
|
60
|
+
return {
|
|
61
|
+
severity: 'warn',
|
|
62
|
+
message: `\u26a0 Session Config command drift could not be checked (${reason})`,
|
|
63
|
+
degraded: reason,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
40
67
|
/**
|
|
41
68
|
* Check for *-command drift.
|
|
42
69
|
*
|
|
43
70
|
* Reads Session Config from CLAUDE.md (or AGENTS.md) in `repoRoot` via
|
|
44
|
-
* `
|
|
71
|
+
* `loadCommandsFromSessionConfigDetailed` and compares any resolved `lint`,
|
|
45
72
|
* `typecheck`, and `test` values against PROJECT_DEFAULTS. Returns null
|
|
46
73
|
* when no drift is detected, or when the config-read returns no recognised
|
|
47
74
|
* `*-command` keys at all (graceful no-op).
|
|
@@ -49,22 +76,33 @@ export const PROJECT_DEFAULTS = Object.freeze({
|
|
|
49
76
|
* Important: missing keys do NOT trigger drift. Only explicit overrides
|
|
50
77
|
* that differ from PROJECT_DEFAULTS produce a banner.
|
|
51
78
|
*
|
|
79
|
+
* A FAILED read (`degraded`) is reported rather than silently treated as
|
|
80
|
+
* "no drift" — see the module header. A repo that simply carries no Session
|
|
81
|
+
* Config is NOT a failed read and stays silent.
|
|
82
|
+
*
|
|
52
83
|
* Marked `async` for backward compatibility with existing callers that
|
|
53
84
|
* `await` the result; internally the helper is synchronous.
|
|
54
85
|
*
|
|
55
86
|
* @param {object} [opts]
|
|
56
87
|
* @param {string} [opts.repoRoot] — project root (defaults to process.cwd()).
|
|
57
|
-
* @returns {Promise<null | {severity: 'warn', message: string}>}
|
|
88
|
+
* @returns {Promise<null | {severity: 'warn', message: string, degraded?: string}>}
|
|
58
89
|
*/
|
|
59
90
|
export async function checkQgCommandDrift(opts = {}) {
|
|
60
91
|
const repoRoot = opts.repoRoot ?? process.cwd();
|
|
61
92
|
|
|
62
|
-
let
|
|
93
|
+
let detailed;
|
|
63
94
|
try {
|
|
64
|
-
|
|
95
|
+
detailed = loadCommandsFromSessionConfigDetailed(repoRoot);
|
|
65
96
|
} catch {
|
|
66
|
-
|
|
97
|
+
// The loader is documented never to throw. If it does anyway, that IS a
|
|
98
|
+
// failed read — say so rather than reporting an all-clear.
|
|
99
|
+
return degradedBanner('parse-error');
|
|
100
|
+
}
|
|
101
|
+
if (!detailed || typeof detailed !== 'object') return degradedBanner('parse-error');
|
|
102
|
+
if (typeof detailed.degraded === 'string' && detailed.degraded) {
|
|
103
|
+
return degradedBanner(detailed.degraded);
|
|
67
104
|
}
|
|
105
|
+
const cfg = detailed.commands;
|
|
68
106
|
if (!cfg || typeof cfg !== 'object') return null;
|
|
69
107
|
|
|
70
108
|
const drifts = [];
|