session-orchestrator 5.1.0 → 5.2.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/autopilot/SKILL.md +1 -0
- package/.agents/skills/bootstrap/SKILL.md +2 -0
- package/.agents/skills/brainstorm/SKILL.md +3 -0
- package/.agents/skills/close/SKILL.md +17 -0
- package/.agents/skills/debug/SKILL.md +2 -0
- package/.agents/skills/discovery/SKILL.md +2 -1
- package/.agents/skills/dispatcher/SKILL.md +2 -0
- package/.agents/skills/eli5/SKILL.md +2 -0
- package/.agents/skills/eval/SKILL.md +1 -0
- package/.agents/skills/evolve/SKILL.md +2 -1
- package/.agents/skills/go/SKILL.md +18 -0
- package/.agents/skills/grill/SKILL.md +2 -0
- package/.agents/skills/harness-audit/SKILL.md +16 -0
- package/.agents/skills/memory-cleanup/SKILL.md +1 -0
- package/.agents/skills/persona-panel/SKILL.md +1 -0
- package/.agents/skills/plan/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +17 -0
- package/.agents/skills/reconcile/SKILL.md +1 -0
- package/.agents/skills/release/SKILL.md +18 -0
- package/.agents/skills/repo-audit/SKILL.md +1 -0
- package/.agents/skills/spinout/SKILL.md +1 -0
- package/.agents/skills/sunset-review/SKILL.md +2 -0
- package/.agents/skills/test/SKILL.md +17 -0
- package/.agents/skills/ux-grill/SKILL.md +2 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/autopilot/SKILL.md +5 -4
- package/.codex-plugin/skills/bootstrap/SKILL.md +8 -4
- package/.codex-plugin/skills/brainstorm/SKILL.md +11 -4
- package/.codex-plugin/skills/close/SKILL.md +3 -3
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +2 -0
- package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/debug/SKILL.md +11 -4
- package/.codex-plugin/skills/discovery/SKILL.md +8 -4
- package/.codex-plugin/skills/dispatcher/SKILL.md +4 -4
- package/.codex-plugin/skills/eli5/SKILL.md +9 -4
- package/.codex-plugin/skills/eval/SKILL.md +9 -4
- package/.codex-plugin/skills/evolve/SKILL.md +9 -4
- package/.codex-plugin/skills/go/SKILL.md +3 -3
- package/.codex-plugin/skills/grill/SKILL.md +11 -4
- package/.codex-plugin/skills/harness-audit/SKILL.md +4 -3
- package/.codex-plugin/skills/memory-cleanup/SKILL.md +9 -4
- package/.codex-plugin/skills/npm-publish/SKILL.md +2 -0
- package/.codex-plugin/skills/npm-publish/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/persona-panel/SKILL.md +5 -5
- package/.codex-plugin/skills/plan/SKILL.md +8 -4
- package/.codex-plugin/skills/portfolio/SKILL.md +3 -3
- package/.codex-plugin/skills/reconcile/SKILL.md +9 -4
- package/.codex-plugin/skills/release/SKILL.md +3 -3
- package/.codex-plugin/skills/repo-audit/SKILL.md +6 -4
- package/.codex-plugin/skills/spinout/SKILL.md +4 -4
- package/.codex-plugin/skills/sunset-review/SKILL.md +5 -4
- package/.codex-plugin/skills/test/SKILL.md +3 -3
- package/.codex-plugin/skills/ux-grill/SKILL.md +11 -4
- package/.cursor/commands/autopilot.md +4 -4
- package/.cursor/commands/bootstrap.md +5 -4
- package/.cursor/commands/brainstorm.md +5 -4
- package/.cursor/commands/close.md +4 -3
- package/.cursor/commands/convergence-monitoring.md +13 -0
- package/.cursor/commands/debug.md +4 -4
- package/.cursor/commands/discovery.md +4 -4
- package/.cursor/commands/dispatcher.md +4 -4
- package/.cursor/commands/eli5.md +4 -4
- package/.cursor/commands/eval.md +4 -4
- package/.cursor/commands/evolve.md +4 -4
- package/.cursor/commands/go.md +4 -3
- package/.cursor/commands/grill.md +4 -4
- package/.cursor/commands/harness-audit.md +3 -3
- package/.cursor/commands/memory-cleanup.md +4 -4
- package/.cursor/commands/npm-publish.md +13 -0
- package/.cursor/commands/persona-panel.md +4 -4
- package/.cursor/commands/plan.md +5 -4
- package/.cursor/commands/portfolio.md +3 -3
- package/.cursor/commands/reconcile.md +4 -4
- package/.cursor/commands/release.md +4 -3
- package/.cursor/commands/repo-audit.md +4 -4
- package/.cursor/commands/spinout.md +4 -4
- package/.cursor/commands/sunset-review.md +4 -4
- package/.cursor/commands/test.md +3 -3
- package/.cursor/commands/ux-grill.md +4 -4
- package/.cursor/rules/010-session-workflow.mdc +2 -2
- package/.cursor/skills/bootstrap/SKILL.md +1 -0
- package/.cursor/skills/close/SKILL.md +13 -0
- package/.cursor/skills/debug/SKILL.md +0 -1
- package/.cursor/skills/discovery/SKILL.md +0 -1
- package/.cursor/skills/dispatcher/SKILL.md +0 -1
- package/.cursor/skills/eli5/SKILL.md +0 -1
- package/.cursor/skills/evolve/SKILL.md +0 -1
- package/.cursor/skills/go/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +0 -1
- package/.cursor/skills/harness-audit/SKILL.md +12 -0
- package/.cursor/skills/portfolio/SKILL.md +12 -0
- package/.cursor/skills/release/SKILL.md +13 -0
- package/.cursor/skills/repo-audit/SKILL.md +0 -1
- package/.cursor/skills/sunset-review/SKILL.md +0 -1
- package/.cursor/skills/test/SKILL.md +12 -0
- package/.cursor/skills/ux-grill/SKILL.md +0 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +1 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +61 -0
- package/README.md +11 -9
- package/commands/session.md +10 -0
- package/docs/ci-setup.md +53 -0
- package/docs/codex-setup.md +1 -1
- package/docs/components.md +11 -6
- package/docs/events-schema.md +4 -1
- package/docs/install.md +16 -0
- package/docs/persona-panel.md +1 -1
- package/docs/pi-setup.md +1 -1
- package/docs/rule-authoring.md +83 -14
- package/docs/scope-collision-guard.md +2 -0
- package/docs/session-config-reference.md +6 -4
- package/hooks/_lib/hook-import-set.json +46 -6
- package/hooks/_lib/subagent-paths.mjs +15 -0
- package/hooks/_lib/vcs-create-matcher.mjs +217 -62
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/on-session-end.mjs +14 -2
- package/hooks/on-stop.mjs +43 -1
- package/hooks/post-bash-write-verify.mjs +3 -0
- package/hooks/pre-auq-clarity.mjs +3 -0
- package/hooks/pre-bash-issue-budget.mjs +103 -17
- package/hooks/pre-task-scope-disjoint.mjs +152 -3
- package/hooks/skill-invocation-telemetry.mjs +2 -1
- package/package.json +2 -1
- package/pi/prompts/autopilot.md +3 -3
- package/pi/prompts/bootstrap.md +3 -3
- package/pi/prompts/brainstorm.md +3 -3
- package/pi/prompts/close.md +2 -2
- package/pi/prompts/convergence-monitoring.md +11 -0
- package/pi/prompts/debug.md +3 -3
- package/pi/prompts/discovery.md +3 -3
- package/pi/prompts/dispatcher.md +3 -3
- package/pi/prompts/eli5.md +3 -3
- package/pi/prompts/eval.md +3 -3
- package/pi/prompts/evolve.md +3 -3
- package/pi/prompts/go.md +2 -2
- package/pi/prompts/grill.md +3 -3
- package/pi/prompts/harness-audit.md +2 -3
- package/pi/prompts/memory-cleanup.md +3 -3
- package/pi/prompts/npm-publish.md +11 -0
- package/pi/prompts/persona-panel.md +3 -3
- package/pi/prompts/plan.md +3 -3
- package/pi/prompts/portfolio.md +2 -2
- package/pi/prompts/reconcile.md +3 -3
- package/pi/prompts/release.md +3 -3
- package/pi/prompts/repo-audit.md +3 -4
- package/pi/prompts/session.md +1 -1
- package/pi/prompts/spinout.md +3 -3
- package/pi/prompts/sunset-review.md +3 -3
- package/pi/prompts/templates-ack.md +1 -1
- package/pi/prompts/test.md +3 -3
- package/pi/prompts/ux-grill.md +3 -3
- package/scripts/archive-closed-prds.mjs +2 -2
- package/scripts/auq-audit.mjs +2 -3
- package/scripts/backfill-abandoned-sessions.mjs +57 -3
- package/scripts/backfill-evidence-digest.mjs +2 -1
- package/scripts/backfill-learnings-from-vault.mjs +2 -2
- package/scripts/check-package-manager.mjs +2 -2
- package/scripts/ci/assert-vitest-green.mjs +2 -1
- package/scripts/emit-session.mjs +2 -3
- package/scripts/export-hw-learnings.mjs +2 -1
- package/scripts/express-path.mjs +1 -1
- package/scripts/gc-stale-worktrees.mjs +2 -1
- package/scripts/generate-codex-skills.mjs +48 -4
- package/scripts/generate-cursor-adapter.mjs +173 -9
- package/scripts/generate-hook-import-set.mjs +12 -27
- package/scripts/generate-pi-prompts.mjs +183 -13
- package/scripts/github-protection-audit.mjs +2 -3
- package/scripts/lib/agent-frontmatter.mjs +23 -1
- package/scripts/lib/claude-md-budget-lint.mjs +2 -5
- package/scripts/lib/command-blocker.mjs +133 -5
- package/scripts/lib/config/drift-check.mjs +19 -0
- package/scripts/lib/convergence-monitor.mjs +2 -2
- package/scripts/lib/cursor-hook-bridge.mjs +2 -2
- package/scripts/lib/description-surface.mjs +2 -5
- package/scripts/lib/dispatcher/cli.mjs +2 -1
- package/scripts/lib/ecosystem-wizard.mjs +2 -1
- package/scripts/lib/fetch-baseline.mjs +3 -8
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +2 -1
- package/scripts/lib/gitlab-portfolio/cli.mjs +2 -1
- package/scripts/lib/instruction-budget-guard.mjs +186 -46
- package/scripts/lib/is-main-module.mjs +82 -0
- package/scripts/lib/locks/index.mjs +32 -25
- package/scripts/lib/maintenance-due-banner.mjs +69 -3
- package/scripts/lib/peer-discovery.mjs +2 -5
- package/scripts/lib/playwright-driver/runner.mjs +2 -1
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +642 -0
- package/scripts/lib/rules-sync.mjs +2 -5
- package/scripts/lib/scope-echo.mjs +392 -7
- package/scripts/lib/session-close-backfill.mjs +58 -6
- package/scripts/lib/state-md.mjs +84 -3
- package/scripts/lib/sunset/walker.mjs +31 -4
- package/scripts/lib/tests-src-ratio.mjs +2 -6
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +2 -1
- package/scripts/lib/user-invocable-skills.mjs +185 -0
- package/scripts/lib/validate/check-banner-parity.mjs +2 -2
- package/scripts/lib/validate/check-cursor-adapter.mjs +2 -2
- package/scripts/lib/validate/check-dead-bridge.mjs +2 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +2 -2
- package/scripts/lib/validate/check-entry-guard.mjs +366 -0
- package/scripts/lib/validate/check-guard-requires-parity.mjs +2 -2
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +2 -2
- package/scripts/lib/validate/check-learning-provenance.mjs +2 -2
- package/scripts/lib/validate/check-skill-links.mjs +27 -6
- package/scripts/lib/validate/check-skill-script-paths.mjs +2 -2
- package/scripts/lib/validate/check-test-git-config-target.mjs +2 -2
- package/scripts/lib/validate/check-unicode-safety.mjs +2 -2
- package/scripts/lib/validate/check-untracked-test-deps.mjs +2 -2
- package/scripts/lib/validate/check-unwired-features.mjs +91 -7
- package/scripts/lib/validate/check-validator-registration.mjs +2 -2
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +2 -2
- package/scripts/lib/validate-vendored-rules.mjs +35 -9
- package/scripts/lib/wave-transcript-tail.mjs +2 -2
- package/scripts/lock-reaper.mjs +2 -1
- package/scripts/materialize-wave-scope.mjs +87 -4
- package/scripts/migrate-sessions-jsonl.mjs +2 -1
- package/scripts/migrate-vault-paths.mjs +2 -3
- package/scripts/release.mjs +80 -35
- package/scripts/relocate-vault-corpus.mjs +2 -3
- package/scripts/repair-invalid-sessions.mjs +2 -2
- package/scripts/session-shape.mjs +2 -2
- package/scripts/site-numbers.mjs +35 -11
- package/scripts/sweep-expired-rules.mjs +216 -0
- package/scripts/validate-plugin.mjs +9 -0
- package/scripts/vault-consolidate.mjs +2 -2
- package/scripts/vault-mirror.mjs +2 -3
- package/scripts/wave-scope-binding.mjs +2 -3
- package/skills/_shared/bootstrap-gate.md +1 -1
- package/skills/_shared/monitor-patterns.md +1 -1
- package/skills/_shared/research-evidence.md +53 -0
- package/skills/_shared/state-ownership.md +3 -0
- package/skills/autopilot/SKILL.md +58 -4
- package/skills/bootstrap/SKILL.md +51 -1
- package/skills/brainstorm/SKILL.md +16 -0
- package/skills/claude-md-drift-check/checker.mjs +49 -11
- package/{commands/close.md → skills/close/SKILL.md} +9 -3
- package/skills/debug/SKILL.md +10 -0
- package/skills/discovery/SKILL.md +24 -1
- package/skills/discovery/probes-session.md +2 -2
- package/skills/dispatcher/SKILL.md +38 -7
- package/skills/eli5/SKILL.md +11 -0
- package/skills/eval/SKILL.md +14 -0
- package/skills/evolve/SKILL.md +8 -1
- package/skills/evolve/references/evolve-dialectic-mode.md +6 -2
- package/{commands/go.md → skills/go/SKILL.md} +9 -1
- package/skills/grill/SKILL.md +19 -0
- package/{commands/harness-audit.md → skills/harness-audit/SKILL.md} +7 -2
- package/skills/hook-development/SKILL.md +46 -41
- package/skills/memory-cleanup/SKILL.md +7 -0
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/persona-panel/SKILL.md +56 -1
- package/skills/persona-panel/persona-format.md +1 -1
- package/skills/plan/SKILL.md +28 -1
- package/{commands/portfolio.md → skills/portfolio/SKILL.md} +8 -2
- package/skills/reconcile/SKILL.md +10 -0
- package/{commands/release.md → skills/release/SKILL.md} +16 -2
- package/skills/repo-audit/SKILL.md +7 -0
- package/skills/session-end/plan-verification.md +2 -2
- package/skills/session-plan/SKILL.md +1 -1
- package/skills/session-start/SKILL.md +5 -4
- package/skills/session-start/phase-8-5-express-path.md +6 -6
- package/skills/session-start/references/phase-1-5-session-continuity.md +1 -1
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
- package/skills/spinout/SKILL.md +12 -1
- package/skills/sunset-review/SKILL.md +13 -0
- package/{commands/test.md → skills/test/SKILL.md} +10 -4
- package/skills/ux-grill/SKILL.md +19 -1
- package/skills/wave-executor/SKILL.md +7 -4
- package/skills/wave-executor/references/wave-executor-state-init.md +13 -1
- package/skills/wave-executor/references/wave-loop-dispatch.md +3 -1
- package/skills/wave-executor/references/wave-loop-review.md +17 -1
- package/commands/autopilot.md +0 -80
- package/commands/bootstrap.md +0 -56
- package/commands/brainstorm.md +0 -48
- package/commands/debug.md +0 -36
- package/commands/discovery.md +0 -32
- package/commands/dispatcher.md +0 -59
- package/commands/eli5.md +0 -33
- package/commands/eval.md +0 -28
- package/commands/evolve.md +0 -10
- package/commands/grill.md +0 -45
- package/commands/memory-cleanup.md +0 -26
- package/commands/persona-panel.md +0 -121
- package/commands/plan.md +0 -15
- package/commands/reconcile.md +0 -23
- package/commands/repo-audit.md +0 -24
- package/commands/spinout.md +0 -15
- package/commands/sunset-review.md +0 -27
- package/commands/ux-grill.md +0 -51
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ux-grill
|
|
3
3
|
description: "Use when a running web app's UX has to be audited reproducibly rather than by feel — a deterministic mechanical pass over routes and viewports (axe, target size, horizontal overflow, page title, scripted journeys) followed by a screenshot-grounded interrogation of the operator, journey by journey, with two persona lenses from the target repo's manifest. Triggered by \"grill the UX\", \"roast the dashboard\", \"UX-Audit\", \"/ux-grill\". Bootstraps its own manifest from a loopback URL on the first run, so it never requires a hand-written file to start."
|
|
4
|
-
disable-model-invocation: true
|
|
5
4
|
---
|
|
6
5
|
|
|
7
6
|
# ux-grill
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "session-orchestrator",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"description": "A repeatable Plan, Go, Close workflow for AI coding sessions: /session reads your repo and agrees the scope, /go runs the work in waves with a quality gate between each, /close verifies and commits. Runs on Claude Code, Codex CLI, Cursor and Pi.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Bernhard Goetzendorfer"
|
|
@@ -124,7 +124,7 @@
|
|
|
124
124
|
"severity": "block",
|
|
125
125
|
"target-denylist": ["~/.claude/projects/*/memory/**","~/.codex/projects/*/memory/**","~/.cursor/projects/*/memory/**"],
|
|
126
126
|
"modes": ["truncate","append"],
|
|
127
|
-
"rationale": "The harness auto-memory directory is injected as TRUSTED project context into every later session, yet lives outside git diff, CI review, check-owner-leakage (git ls-files only) and gitleaks. #1352 closed the Edit/Write lane for dispatched agents; this closes the shell-redirect lane, append included. NAMED CEILING (BV-004), every case measured 2026-09-13: (a) redirect-shaped by construction — `tee -a` and an in-process write (node -e fs.appendFileSync) are NOT covered; (b) `eval
|
|
127
|
+
"rationale": "The harness auto-memory directory is injected as TRUSTED project context into every later session, yet lives outside git diff, CI review, check-owner-leakage (git ls-files only) and gitleaks. #1352 closed the Edit/Write lane for dispatched agents; this closes the shell-redirect lane, append included. NAMED CEILING (BV-004), every case measured 2026-09-13: (a) redirect-shaped by construction — `tee -a` and an in-process write (node -e fs.appendFileSync) are NOT covered; (b) NAMED CEILING: `eval` with a non-literal payload (`eval \"$CMD\"`, `eval $(…)`) is unreachable — the command text does not exist at hook time (`unresolved` class, #641). Literal `eval '… > target'`, `dd of=target` and `env -S <str> …` are covered since #1366; (c) a cwd-relative target after a directory change (`cd ~ && … > .claude/projects/*/memory/…`) is out of reach — the hook does not know the chain's cwd and a relative target is judged repo-root-relative, which is not lexically solvable; (d) `$HOME` / `${HOME}` / `$(…)` / backticks in the target fall into the pre-existing `unresolved` class (#641), where the hook warns and allows rather than guessing — for a home directory that is the most natural spelling. (e) the payload-evaluation budget is SHARED and finite (`MAX_PAYLOAD_EVALUATIONS` = 32 in `scripts/lib/command-blocker.mjs`): once it is spent, redirect-target collection returns `{unresolved, reason: 'budget-exhausted'}`, which this rule treats like every other unresolved target — warn and ALLOW. Measured 2026-09-16: 40 `eval 'echo fN'` fillers followed by `eval 'echo x > ~/.claude/…/MEMORY.md'` → ALLOW plus `⚠ unresolved redirect target (budget-exhausted)`. Not a regression of #1366 — `bash -c` fillers exhaust the same budget at HEAD (#988 T2) — but a standing ceiling of the same class as (d), and an attacker-controllable one: the filler count is free. Revisit together with (d) if the unresolved class is ever made blocking. Revisit if the policy grows a non-redirect target class or the payload traversal learns redirects. Home-anchored entries are matched against the expanded ABSOLUTE path (#1362) because relativizeAgainstRoot discards every out-of-repo target by design.",
|
|
128
128
|
"sources": ["#1352","#1362"]
|
|
129
129
|
}
|
|
130
130
|
]
|
package/AGENTS.md
CHANGED
|
@@ -31,7 +31,7 @@ These are the non-obvious, mistake-causing facts that must load every session. E
|
|
|
31
31
|
Phase 4 of session-start invokes `scripts/lib/ci-status-banner.mjs` via `checkCiStatus({ repoRoot })` to render a 🚨 banner when CI is red on HEAD.
|
|
32
32
|
Never claim CI green from `npm test` alone — the 8-pipeline silent regression (2026-05-09 → 2026-05-10, fixed in deep-2) is the cautionary tale. <!-- consistency:exempt:runtime-only -->
|
|
33
33
|
- **Destructive-Command Guard is active in main + subagent waves on harnesses with a compatible command hook.** On Claude Code, and through the supported Cursor and Pi bridges, `hooks/pre-bash-destructive-guard.mjs` applies `.orchestrator/policy/blocked-commands.json`:
|
|
34
|
-
**
|
|
34
|
+
**11 of its 15 rules are `severity: block`, the other 4 only warn** (measured 2026-09-16: `jq '[.rules[].severity]|group_by(.)|map({(.[0]):length})|add' .orchestrator/policy/blocked-commands.json`). On Codex, the destructive-command rules are instructions only because no compatible `PreToolUse` command handler is wired.
|
|
35
35
|
Rule source of truth: [`.claude/rules/parallel-sessions.md`](.claude/rules/parallel-sessions.md) (PSA-003). Per-session bypass via Session Config: `allow-destructive-ops: true` (intentional maintenance only).
|
|
36
36
|
- **Session Config below is runtime-critical.** `scripts/parse-config.mjs` parses the `## Session Config` block; `claude-md-drift-check` Check 6 enforces top-level-key parity against `docs/session-config-template.md`. Edit it like code, not prose — a dropped key changes runtime behaviour.
|
|
37
37
|
- **Live state is not in this file.** Stack: Node 24+, vitest, ESLint 10 (`npm ci` after clone). Test counts, backlog, version, component inventory drift fast — the SSOT is README badges + `.orchestrator/metrics/sessions.jsonl`. Per-session detail lives in the Meta-Vault decisions log (linked above), not here.
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,67 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [5.2.0] - 2026-09-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `scripts/lib/is-main-module.mjs` (`isMainModule(importMetaUrl)`, realpath-resolved on both sides) plus the BLOCKING validator `scripts/lib/validate/check-entry-guard.mjs`, registered in `scripts/validate-plugin.mjs` (#1371). The issue's premise was refuted first: the sweep it recorded as landed on `f1da471f` is unknown to `git log --all`, and the module it depended on did not exist. 51 files carry the new guard (50 from the enumerated sweep plus `scripts/lib/validate/check-unwired-features.mjs` as an in-scope same-pattern site); the hand-rolled `process.argv[1]` comparison came in **15** textual variants, not the 17 the issue claimed. The validator measures **0 fragile of 508** modules on the current tree. Two of its own defects were caught by its own tests before it was registered: a git pathspec `**` that silently skipped 95 files, and a `file://` occurrence inside a comment counted as a guard.
|
|
15
|
+
- `tests/scripts/pack-install-lifecycle.test.mjs`, the `test:pack` script in `package.json` and the CI job `pack-lifecycle` in `.gitlab-ci.yml`, hard-wired into `pipeline-gate`'s `needs` (#1375). It installs the ACTUAL tarball rather than the checkout and asserts the installed `pre-bash-destructive-guard.mjs` denies `rm -rf /` while staying silent on a benign command. Measured end-to-end in W1: `npm pack` 15 s, tarball 3,963,485 B, install 3 s / 67 packages. It is deliberately NOT in `npm test` — it needs the network, so it is its own script and its own job.
|
|
16
|
+
- Six skill mirrors `skills/{close,go,harness-audit,portfolio,release,test}/SKILL.md` (plus the generated `.agents/` and `.cursor/` adapter copies) so the bare commands resolve under `claude -p` (#1370). Two mechanisms were isolated and proven separately in W1, against `claude 2.1.273` with a minimal plugin: `/session` and `/plan` are reserved TERMINAL BUILT-INS (they shadow a plugin even with an empty `CLAUDE_CONFIG_DIR`, so they get the namespaced form documented rather than a mirror), and `Unknown command` hits exactly those `commands/*.md` that have no same-named `skills/<n>/SKILL.md` — a blind prediction confirmed 3/3. Pinned by `tests/commands/headless-bare-command-availability.test.mjs`.
|
|
17
|
+
- The send and receive halves of the FILE-SCOPE chain now JOIN on the digest (#1092). `hooks/pre-task-scope-disjoint.mjs` adds `scope_digest`, `marker_found`, `echo_instruction_present`, `instructed_digest` and `digest_consistent` to `scope_checked`; `scripts/lib/scope-echo.mjs` gains `--verify` (6 kebab-case verdicts plus the degraded `echo-only` and a `malformed_lines` floor-marker); `scripts/materialize-wave-scope.mjs` emits `orchestrator.wave_dispatch.scope_materialized` with `transport_observable`, and `--verify --emit` writes `orchestrator.wave_dispatch.scope_verified`. Before this the halves were unpairable even in principle: 609 send-side against 51 receive-side records whose agent-id sets overlap in ZERO elements. Measured on this session's wave 4: 5/5 `matched`. New E2E suite `tests/hooks/pre-task-scope-injection-e2e.test.mjs` plus `tests/skills/wave-loop-scope-marker.test.mjs`.
|
|
18
|
+
- `resolveSessionStartedAt()` / `resolveSessionIds()` in `scripts/lib/state-md.mjs` and an optional STATE.md `session-id:` key (raw UUID) in the wave-executor state-init template (#1368). `started_at` now comes from `session.lock` instead of being prose-written, and `commands/close.md` falls back to `readLock().session_id`.
|
|
19
|
+
- `STARTED_AT_DRIFT_TOLERANCE_MS` (6 h) in `scripts/lib/session-close-backfill.mjs` (#1368), with a negative test proving a 9 h drift is still rejected.
|
|
20
|
+
- `ownSessionIsEnding` on `backfillAbandonedSession()` plus self-exclusion in `planSessions()` (`scripts/lib/session-close-backfill.mjs`, `scripts/backfill-abandoned-sessions.mjs`, #1376) — the parameter is passed only from `hooks/on-session-end.mjs` under the #863 attestation guard.
|
|
21
|
+
- `sidecar_missing` on `orchestrator.agent.stopped` in `hooks/on-stop.mjs` (#1289). It is the DENOMINATOR for every meta-derived field on that record, so a census of `agent_type_meta` gaps can name how many of them are "sidecar not found" rather than "producer broken".
|
|
22
|
+
- Maintenance-due signal 7, `generated-rules-expiring`, in `scripts/lib/maintenance-due-banner.mjs` (`MAINTENANCE_TOTAL_SIGNALS` 6 → 7, `GENERATED_RULE_EXPIRY_HORIZON_DAYS` = 7), backed by the new exports `isMachineGeneratedRule` / `listMachineGeneratedRules` / `isExpiredGeneratedRule` in `scripts/lib/instruction-budget-guard.mjs` (#1367, instrument half). Measured in W1: `rule-loader.mjs` filters `expires-at` only on READ and no code path ever deletes an expired file (3 historical files sat 16–19 days expired on disk and were removed by hand); the entire generated corpus expires within 57 days. The alarm is a session-start banner, not a vitest case, because a test comparing `expires-at` against TODAY is a calendar time-bomb in a blocking gate — the predicate itself is tested against an injected clock in `tests/rules/generated-corpus-expiry.test.mjs`.
|
|
23
|
+
- `findLoopedIssueCreate()` in `hooks/_lib/vcs-create-matcher.mjs` plus the `xargs` bulk deny in `hooks/pre-bash-issue-budget.mjs`, bound to the causing statement (#1289 B1 and the #1106-class loop lane). Measured at the real hook: `xargs` forms yielded 0 statements, so the fix is local to the matcher, not an intervention in `WRAPPER_UNWRAP`.
|
|
24
|
+
- `scripts/lib/reconcile/rule-expiry-sweep.mjs` (`planRuleExpirySweep`/`applyRuleExpirySweep`) plus CLI `scripts/sweep-expired-rules.mjs` (#1377) — the missing REMOVAL half of the generated-rule TTL: `scripts/lib/rule-loader.mjs` already stops injecting an expired generated rule at read time, but nothing ever removed one from disk. `--dry-run` is the default, `--apply` is the only write path, `--json` emits the full plan. An expired entry's prose block is deleted while its `## Provenance` pair is kept as `markers only` (a same-line HTML comment), because `/reconcile` dedupes on that pair and dropping it would re-propose the learning; a file is deleted only when every pair on it is expired, and every pair is stamped terminal via `markCandidateProcessed` before the `unlink`. Two fail-open branches, both reported rather than guessed: an entry whose `learning-id` does not resolve against `learnings.jsonl` (`unresolvedPairIds`) keeps its file, and a file whose `###` headings do not map 1:1 onto its non-`markers only` pairs is reported `no-1to1-mapping` and left untouched (measured 2026-09-17: the mapping holds in 3 of 7 live files). Writes are refused through a symlink or outside `.claude/rules/` (path confinement) and are atomic. Emits `orchestrator.rules.expiry_sweep_applied` (`docs/events-schema.md`) on the `--apply` path only, with `rewritten`/`deleted`/`stamped`/`write_errors`/`expired_entries`/`files_scanned` always present including as `0`. Wired into maintenance-loop signal 7's banner (row 3, `skills/wave-executor/SKILL.md` + `skills/session-start/SKILL.md`) and documented in `docs/rule-authoring.md` § "The expiry sweep". Live dry run 2026-09-17: 7 files scanned, 0 rewrites (nothing expired yet), 4 `no-1to1-mapping`, 5 of 92 provenance-pair ids unresolvable.
|
|
25
|
+
- `scripts/lib/locks/index.mjs` restored as a one-cycle `export *` deprecation shim (0 named exports, one-time `console.warn`) after having been deleted for 0 importers (see Removed, 5.1.0-cycle) — `package.json` carries no `exports` map, so every packed `scripts/lib/` path is public and a pre-5.1.0 deep-importer of this path would hit a bare runtime "module not found" with no compile-time warning. Covered by `tests/lib/locks/index-shim.test.mjs`; removal deferred to 6.0.0 (`.claude/rules/development.md` § Package Lifecycle & Versioning).
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Every slash command now has exactly ONE definition. 24 `commands/*.md` files were folded into their same-named `skills/<name>/SKILL.md` (frontmatter `user-invocable: true` + `argument-hint` + `disable-model-invocation`, body under `## Invocation`), leaving `commands/session.md` and `commands/templates-ack.md` as the only command files. Reason, measured 2026-09-16 on `claude 2.1.273` with a throwaway plugin: Claude Code registers a `commands/<name>.md` AND a user-invocable `skills/<name>/SKILL.md` both as `/session-orchestrator:<name>`, so every twin was listed twice in the interactive `/` picker; under `claude -p` the twin ran the SKILL body (the command file was dead), and a `user-invocable: false` skill beside a command was recognised but ran 0 turns — so the mirrors from #1370 had made `/close`, `/go`, `/harness-audit`, `/portfolio`, `/release` and `/test` duplicates, and `discovery`, `evolve`, `plan` had been silently dead headless all along. The Cursor and Pi generators now emit a wrapper for `commands/*.md` ∪ skills with explicit `user-invocable: true` (28 slash commands: 2 + 26), the Codex generator emits the `$ARGUMENTS` contract for those skills, and `tests/commands/headless-bare-command-availability.test.mjs` forbids the command/skill twin outright.
|
|
30
|
+
- Rule-corpus consolidation (#1367, #1372): `pathScoped` 122,763 → 109,932 B and `generated` 75,130 → 69,941 B across 7 consolidated files, with **92/92 provenance pairs kept** — the provenance pair is what stops `/reconcile` from re-proposing an absorbed learning, so only the prose was deleted. 9 blocked reconcile proposals were absorbed and 1 rejected as already materialized. `.claude/rules/testing.md` −7,717 B of web-framework prose (Server Action, E2E, Accessibility, half of CI Integration — a Node CLI repo has no call site for any of it) and `.claude/rules/receiving-review.md` −562 B with its pin re-derived to 8,009 (#1038) rather than raised.
|
|
31
|
+
- `DEFAULT_GENERATED_BYTE_CEILING` in `scripts/lib/instruction-budget-guard.mjs` 124,000 → 95,000 (#1308), chosen by replaying all 91 historical states of `.claude/rules/`: the real peak is 99,774 B at `2ae08fe0` (the old docblock claimed 89,763), 124,000 fires 0/91 and 95,000 fires **1/91**. The replay is in the docblock, so the next editor can falsify it (HR-105).
|
|
32
|
+
- `docs/rule-authoring.md` point 3: `paths:` is the CANONICAL scope key for `.claude/rules/` and `globs:` an accepted alias, instead of demanding both as byte-identical mirrors. `scripts/lib/validate-vendored-rules.mjs`'s `paths-frontmatter` probe is re-scoped to `rules/` library sources, where it is a vendoring convention (#742) rather than the loader-compatibility gate it was documented as — `rule-loader.mjs` has accepted `paths:` since #795.
|
|
33
|
+
- `scripts/lib/command-blocker.mjs` traverses into `eval` literals, `dd of=` (with `conv=notrunc` / `oflag=append` classified as append) and `env -S <op> …` (#1366). Measured at the real hook beforehand: six previously-open forms were ALLOW on BOTH redirect rules; they now deny, and benign forms stay ALLOW. `eval "$CMD"` remains a named ceiling (BV-004) — a variable payload is not lexically solvable.
|
|
34
|
+
- `SCOPE_MARKER` in `scripts/lib/scope-echo.mjs` is case-SENSITIVE (#1092). The lowercase match was the cause of this session's 5 `unparseable` W1 dispatches: the Learnings-Index header contains the words "file scope" and was classified as a FILE-SCOPE block.
|
|
35
|
+
- `isCliEntrypoint()` in `scripts/lib/validate/check-unwired-features.mjs` recognises `isMainModule(`, `export *` barrels enter the population, and `reportableRoots` replaces the basename-collision filter (#1293 remainder) — census of unreachable modules 2 → 0.
|
|
36
|
+
- `commands/{close,go,harness-audit,portfolio,release,test}.md` reduced to the entry-contract shape, with the body owned by the new mirrored skill (#1370).
|
|
37
|
+
- STATE.md `started_at` is sourced from the session lock in `skills/wave-executor/references/wave-executor-state-init.md` and `skills/_shared/state-ownership.md` (#1368) — it was 100 % prose-written before, which is what produced the 48-minute drift that made `/close` re-run session-end.
|
|
38
|
+
- Surface counts in `docs/components.md` (skills 44 → 50) and the session-start / audit docs re-measured against the tree: 50 skills, 39 validators, 25 rule files, 685 test files.
|
|
39
|
+
- MR !39 merged as `1c02346a` (research-evidence contract + hook-development corrections). The panel verdict was MERGE WITH EDITS, and both edits land here: `skills/hook-development/SKILL.md` now states where and when the `UserPromptSubmit` field name was checked and that no handler in this plugin exercises either spelling, and `skills/_shared/research-evidence.md` cross-references PSA-006's executed-command transcript for counts, percentages and distributions.
|
|
40
|
+
- `scripts/lib/validate/check-entry-guard.mjs` gains a second oracle for the bare `process.argv[1].endsWith('<x>.mjs')` basename form (plus its `?.`, `String(...)` and `(argv[1] || '')` receiver variants) alongside the existing `===`/`!==` comparison oracle — the two hand-written guards this check was originally built to catch (`scripts/lib/ecosystem-wizard.mjs`, `scripts/lib/fetch-baseline.mjs`, see Fixed) used the bare form, which the comparison oracle could never see, so reverting either file's fix would have been caught by nothing. `ALLOWLIST` is now empty (its one entry, for `fetch-baseline.mjs`'s hybrid guard, no longer applies) but the mechanism is kept rather than deleted.
|
|
41
|
+
- `scripts/lib/validate/check-skill-links.mjs` scans `.cursor/rules` (added to `SCAN_DIRS`) including its `.mdc` extension (new `MD_EXTENSIONS` export) — that surface ships in the npm tarball and, after #1370 folded `commands/go.md`/`commands/close.md` into skills, carried two now-dangling citations of them. Measured 2026-09-17: `.cursor/rules/*.mdc` contains zero `[text](target)` links today, so the widening adds 9 scanned files and 0 checked links — it is a forward guard, not a catch of the backticked-code-span citation class that actually caused the dangling reference (see Fixed for that repair).
|
|
42
|
+
- `skills/claude-md-drift-check/checker.mjs` extracts `countSlashCommands()` (the `commands/*.md` ∪ user-invocable-skills union, deduplicated) as the ONE implementation shared by the `command-count` surface (Check 5) and the `docs-parity` sub-check (Check 10) — the two counters had drifted independently since #1370 folded 24 command files into skills, leaving `docs-parity` on a raw `commands/` `readdirSync()` that undercounted; the check now reports `ok` / exit 0 where it was red on HEAD.
|
|
43
|
+
- `scripts/generate-cursor-adapter.mjs` reads `disable-model-invocation` from the SOURCE skill's own frontmatter (new `disablesModelInvocation()`: emit when the source declares it, OR when the skill is not user-invocable at all) instead of deriving it from `user-invocable` — 6 skills that are both `user-invocable: true` AND operator-only (`bootstrap`, `brainstorm`, `close`, `go`, `plan`, `release`) had silently lost the flag on both their `.cursor/commands/*.md` and `.cursor/skills/*/SKILL.md` wrappers, which would let a Cursor model auto-invoke `/close`, `/go` or `/release`. The generator now delegates to `scripts/lib/user-invocable-skills.mjs`'s `isUserInvocableValue()` (see below) and tolerates a UTF-8 BOM / CRLF frontmatter block, which previously made it parse as "no frontmatter" at all.
|
|
44
|
+
- `scripts/lib/user-invocable-skills.mjs`'s `isUserInvocableValue()` is now the one `user-invocable` normaliser for the Cursor, Pi and Codex generators and every counter — it strips a trailing `# comment` and one layer of matching quotes before comparing, accepts case-insensitive `true`, and WARNs (without accepting) on the YAML-1.1-but-not-1.2 lookalikes `yes`/`y`/`on`/`t`/`1`, which `generate-codex-skills`'s js-yaml `CORE_SCHEMA` reads as plain strings rather than booleans.
|
|
45
|
+
- `scripts/lib/agent-frontmatter.mjs`'s `parseAgentFrontmatter()` no longer hoists a key nested under a top-level mapping (e.g. `metadata:\n user-invocable: true`) up to top level, and strips a leading UTF-8 BOM before matching the frontmatter fence — both previously made a counter see a slash command no generator recognised.
|
|
46
|
+
- `scripts/release.mjs`'s `--check`/`--publish` CI-preflight rows (`ci-green-on-head`, `ci-green-on-head-github`) now query both platforms for the commit actually being released (new exported `evaluateCiPreflightRows(repoRoot, head, opts)`, `sha: head`) instead of letting the GitHub branch fall back to the mirror's own `HEAD` — before `head-pushed-github` proves `github/main == local HEAD`, the GitHub row could read green for a commit GitHub had never seen (measured 2026-09-16 for `3ebf0e9d`).
|
|
47
|
+
- `skills/autopilot/SKILL.md` documents the Headless Driver Wiring (Option A, `scripts/autopilot.mjs`) as its own subsection rather than folded verbatim into the old command text, and corrects its DI table's `resourceEvaluator`/`peerCounter` rows to match the live code: `peerCounter` returns a SESSION count via `detectPeers()`, not a process count (`.claude/rules/host-resources.md` § HR-103), and refreshes the cached probe snapshot `resourceEvaluator` reads rather than each calling `probe()` independently. `skills/dispatcher/SKILL.md` Phase 4 documents `/session` and `/plan` as reserved-terminal-built-in / `disable-model-invocation`-blocked names the coordinator hands to the operator to type verbatim, versus `/discovery`, which carries neither restriction and the coordinator invokes directly.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- Destructive-command policy census in `CLAUDE.md` / `AGENTS.md`, `.orchestrator/steering/structure.md` and `docs/components.md` still said 14 rules / 10 blocking; the 15th rule (`redirect-harness-memory`, landed 2026-09-13) makes it **15 rules, 11 blocking, 4 warning** (measured 2026-09-16 at `68fbebf9`). `README.md` said 44 skills against 50 on disk. Both found by the `/close` discovery scan (`ssot-code-diff`).
|
|
52
|
+
- `skills/session-end/plan-verification.md` § SESSION_START_REF accessor imported a `parseFrontmatter` that `scripts/lib/state-md.mjs` never exported (measured 2026-09-16 at `3fe9d50e`: `typeof m.parseFrontmatter` → `undefined`), so the documented snippet threw and every close silently fell back to `origin/main`. It now uses `parseStateMd(...).frontmatter`, the export that exists.
|
|
53
|
+
- The issue-budget exemption bypass in `hooks/pre-bash-issue-budget.mjs` (security panel, MED): an exempt FIRST statement lifted the `xargs` and loop bulk deny for the whole command.
|
|
54
|
+
- `findRecordedSession()` in `scripts/lib/session-close-backfill.mjs` compared `started_at` to the MILLISECOND (#1368), so `/close` re-ran session-end for a session already in the ledger. With no template writing `session-id:` (0 matches measured), the native-UUID path of the precheck could never fire — the millisecond compare was not an edge case, it was the only path.
|
|
55
|
+
- `hooks/on-session-end.mjs` skipped the backfill whenever a live lock was present, including the session's own (#1376) — exactly one wrong caller, measured; unclosed sessions were left without a ledger record.
|
|
56
|
+
- The docblock of `hooks/pre-task-scope-disjoint.mjs` described a CLASSIFICATION as a decision.
|
|
57
|
+
- The docblock of `scripts/lib/instruction-budget-guard.mjs`: 3 refuted sentences and stale headroom numbers removed.
|
|
58
|
+
- The verdict `injection_missing` is spelled `injection-missing` (`scripts/lib/scope-echo.mjs`, `docs/events-schema.md`, `docs/scope-collision-guard.md`, `scripts/materialize-wave-scope.mjs`) — it was the one snake_case member of an otherwise kebab-case enum. There is no dual-emit: records written before 2026-09-16 carry the old key in `by_verdict`, and `docs/events-schema.md` says so.
|
|
59
|
+
- `README.md`, `site/llms.txt` and `site/llms-full.txt` still said "10 blocking" destructive-command rules after the two prior census passes each corrected a different subset of surfaces (`06836586`: `CLAUDE.md`/`AGENTS.md`/`structure.md`/`docs/components.md`; `1ef26415`: `site/index.html`/`site/de/index.html`/`site/guide/index.html` plus one line each in the two `llms*.txt` files) — the remaining occurrences are now 15 rules / 11 blocking / 4 warning (measured via `jq` over `.orchestrator/policy/blocked-commands.json`, `9e8146b4`). `skills/discovery/probes-session.md`'s remediation recommendation changed from `/bootstrap --retroactive` (which only writes a missing lock and validates nothing, #832 item 8) to `node scripts/validate-plugin.mjs`.
|
|
60
|
+
- The issue-budget loop guard (`hooks/pre-bash-issue-budget.mjs`, `hooks/_lib/vcs-create-matcher.mjs`, #1379): `findLoopedIssueCreates()` (plural — replacing the single-result `findLoopedIssueCreate`, kept as a `@deprecated` wrapper returning its first element) now reports EVERY looped issue-create statement in a command. Previously an exempt FIRST loop lifted the bulk deny for every LATER, non-exempt loop in the same command — `for i in 1 2 3; do glab issue create --label carryover --title x$i; done; for j in 1 2 3; do glab issue create --title junk$j; done` ALLOWed with a ledger count of 1. The `warn`-mode undercount notice is now gated on whether ANY bulk statement in the command was uncountable rather than on the LAST statement's verdict, so a trailing exempt statement no longer silences it. Both the deny and the warn text now name the lane the violation was found on (`loop`/`xargs`/`mixed`) instead of always saying "shell loop body" (which previously misdirected an `xargs`-only violation).
|
|
61
|
+
- `scripts/lib/ecosystem-wizard.mjs` and `scripts/lib/fetch-baseline.mjs` (#1378) now use `isMainModule(import.meta.url)` for their CLI entry guard instead of, respectively, a bare `process.argv[1].endsWith('ecosystem-wizard.mjs')` and a hybrid `===`-plus-`endsWith` comparison — both are symlink-fragile (a same-named file elsewhere satisfies `endsWith`; `import.meta.url` is a realpath, `process.argv[1]` is the path as typed) and were the two hand-written guards `check-entry-guard.mjs` was built to catch (see Changed for the new oracle that closes the remaining gap).
|
|
62
|
+
- `.cursor/rules/010-session-workflow.mdc` cited the deleted `commands/go.md` and `commands/close.md` (folded into skills by #1370, 3ebf0e9d) — now cites `skills/go/SKILL.md` and `skills/close/SKILL.md`.
|
|
63
|
+
|
|
64
|
+
### Removed
|
|
65
|
+
|
|
66
|
+
- `commands/{autopilot,bootstrap,brainstorm,close,debug,discovery,dispatcher,eli5,eval,evolve,go,grill,harness-audit,memory-cleanup,persona-panel,plan,portfolio,reconcile,release,repo-audit,spinout,sunset-review,test,ux-grill}.md` — folded into the same-named skills (see Changed). The `.cursor/commands/`, `pi/prompts/` and `.codex-plugin/skills/` wrappers are regenerated from the skills.
|
|
67
|
+
- `.claude/rules/git-and-worktrees.md` — absorbed into `.claude/rules/toolchain-and-build.md` (4 globs carried over, 21/21 verified against `git ls-files`), and the 17 citation sites swept.
|
|
68
|
+
- The 9 standalone reconcile rule files — absorbed into the thematic consolidated files, provenance pairs intact.
|
|
69
|
+
- The live-repo vacuum guards in `tests/lib/validate/check-unwired-features.test.mjs` and `tests/rules/generated-corpus-expiry.test.mjs`, and the calendar assertion in the expiry test — a test that passes because it found nothing to measure is indistinguishable from a broken one.
|
|
70
|
+
|
|
10
71
|
## [5.1.0] - 2026-09-13
|
|
11
72
|
|
|
12
73
|
### Added
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Session Orchestrator
|
|
2
2
|
|
|
3
3
|
[](LICENSE)
|
|
4
|
-
[](CHANGELOG.md)
|
|
5
5
|
[](https://www.npmjs.com/package/session-orchestrator)
|
|
6
6
|
|
|
7
7
|
**Give your agents a working rhythm.**
|
|
@@ -69,6 +69,8 @@ The first three are the commands `/go` runs between waves and `/close` runs at t
|
|
|
69
69
|
|
|
70
70
|
On Codex the same three are `$session-orchestrator:session feature`, `$session-orchestrator:go`, `$session-orchestrator:close` ([Codex usage](docs/codex-setup.md#usage)). `/plan` and `/evolve` extend the loop; you can start with just these three.
|
|
71
71
|
|
|
72
|
+
In headless Claude Code (`claude -p`), `/session` and `/plan` are reserved terminal-only built-in names and the bare form is refused; use `/session-orchestrator:session` and `/session-orchestrator:plan` there. Every other command keeps its bare form.
|
|
73
|
+
|
|
72
74
|
## How it works
|
|
73
75
|
|
|
74
76
|
When you type `/session feature`:
|
|
@@ -91,7 +93,7 @@ When you type `/session feature`:
|
|
|
91
93
|
.claude/STATE.md # wave progress and deviations (harness-specific directory)
|
|
92
94
|
```
|
|
93
95
|
|
|
94
|
-
The plugin is **
|
|
96
|
+
The plugin is **50 skills, 28 slash commands, 14 typed subagents and 27 hook files across 10 event types**. A slash command is a skill whose frontmatter says `user-invocable: true` (26 of them) or one of the two remaining `commands/*.md` files (`/session`, `/templates-ack`) — one definition per name, so nothing is listed twice in the `/` picker. Skills, commands and agents are Markdown with YAML frontmatter; the code that dispatches, validates and records runs in `scripts/lib/*.mjs` and `hooks/*.mjs`. There is no build step and no compiled artifact — when a session does something you did not expect, you can open the file that decided it. Full inventory: [`docs/components.md`](docs/components.md).
|
|
95
97
|
|
|
96
98
|
## Why it is built this way
|
|
97
99
|
|
|
@@ -99,7 +101,7 @@ The plugin is **44 skills, 26 slash commands, 14 typed subagents and 27 hook fil
|
|
|
99
101
|
- **Checks run between waves, not only at the end.** A mistake caught after wave 2 costs one wave. The same mistake found at `/close` has already been copied into every wave after it. Findings below the configured confidence threshold are not shown to you.
|
|
100
102
|
- **A crash does not lose the session.** `STATE.md` records which wave finished and what deviated from the plan. The next `/session` offers to continue from the last completed wave.
|
|
101
103
|
- **Two sessions in one working copy is treated as a real risk.** Two people, or two of your own sessions, in the same checkout share one git index, one filesystem and one `STATE.md`, and neither can see the other's uncommitted work. A heartbeat session lock, per-agent file-scope manifests, and the PSA rules in [`.claude/rules/parallel-sessions.md`](https://github.com/Kanevry/session-orchestrator/blob/main/.claude/rules/parallel-sessions.md) exist for exactly that case.
|
|
102
|
-
- **Guards run where the harness supports them, and the table below says where it does not.** A destructive-command policy —
|
|
104
|
+
- **Guards run where the harness supports them, and the table below says where it does not.** A destructive-command policy — 11 rules block, 4 warn — and file-scope enforcement run as real hooks on Claude Code, as bridges on Cursor and Pi, and as instructions only on Codex. Details: [`docs/components.md`](docs/components.md#other-surfaces).
|
|
103
105
|
- **What it learns is opt-in and readable.** Every session appends a record. After 5 or more sessions, `/evolve analyze` proposes patterns with a confidence score; you read them and delete the ones you disagree with. Nothing is applied without you.
|
|
104
106
|
- **GitLab and GitHub, both fully.** It detects which one your remote points at and drives issues and merge/pull requests for either.
|
|
105
107
|
|
|
@@ -109,7 +111,7 @@ How this compares to other orchestrators, with measured results kept separate fr
|
|
|
109
111
|
|
|
110
112
|
| Feature | Claude Code | Codex CLI | Cursor IDE | Pi |
|
|
111
113
|
|---|---|---|---|---|
|
|
112
|
-
| All
|
|
114
|
+
| All 28 commands | Native slash commands | Generated skills (`$session-orchestrator:<name>`) | Native `.cursor/commands` slash commands | Prompt templates |
|
|
113
115
|
| Parallel agents | Agent tool | Multi-agent roles | Sequential only | Sequential (parallel planned) |
|
|
114
116
|
| Session persistence | `.claude/STATE.md` | `.codex/STATE.md` | `.cursor/STATE.md` | `.pi/STATE.md` |
|
|
115
117
|
| Scope enforcement | Active PreToolUse hook; blocking in `strict`, reporting in `warn` | Instructions only; no compatible `apply_patch` handler | `preToolUse` + `beforeShellExecution` bridge; scope blocking requires `strict`; `afterFileEdit` is post-hoc | `tool_call` bridge; scope blocking requires `strict` |
|
|
@@ -119,13 +121,13 @@ How this compares to other orchestrators, with measured results kept separate fr
|
|
|
119
121
|
|
|
120
122
|
All four platforms share the same skills, commands and scripts; only the hooks differ, because each harness fires different events. Codex leaves its `PreToolUse` handlers empty because these guards do not yet match its tool names and edit payloads ([why](docs/codex-setup.md#why-our-pretooluse-guards-stay-unwired--the-reason-corrected)). Cursor and Pi have known event-coverage limits — see [`docs/cursor-setup.md`](docs/cursor-setup.md) and [`docs/pi-setup.md`](docs/pi-setup.md).
|
|
121
123
|
|
|
122
|
-
## Recent highlights (v5.
|
|
124
|
+
## Recent highlights (v5.2.0)
|
|
123
125
|
|
|
124
|
-
Highlights of the v5.
|
|
126
|
+
Highlights of the v5.2.0 line:
|
|
125
127
|
|
|
126
|
-
- **
|
|
127
|
-
- **
|
|
128
|
-
- **The
|
|
128
|
+
- **One definition per slash command.** 24 `commands/*.md` twins were folded into their `skills/<name>/SKILL.md`; the `/` picker no longer lists duplicates, `/discovery`, `/evolve` and `/plan` are live under `claude -p` again, and the Cursor, Pi and Codex wrappers are generated from one `user-invocable` reading (`scripts/lib/user-invocable-skills.mjs`) instead of four private ones. Operator-only commands (`/close`, `/go`, `/release`, …) keep `disable-model-invocation` on every generated surface.
|
|
129
|
+
- **Expired generated rules can now be swept.** `node scripts/sweep-expired-rules.mjs` (dry-run by default, `--apply` to write) removes the prose of expired entries from the consolidated `.claude/rules/` files while keeping their provenance pairs, so `/reconcile` does not re-propose the learning. It refuses symlinks and paths outside the rules directory, writes atomically, and reports the files it cannot map 1:1 instead of guessing.
|
|
130
|
+
- **Two guard holes closed.** The issue-budget hook enumerates every shell loop, so an exempt first loop no longer lifts the bulk deny for a second one; the entry-guard validator now also censuses the bare `argv[1].endsWith('<file>.mjs')` form. `release.mjs --check` asks both CI platforms about the release commit's sha rather than GitHub's own HEAD.
|
|
129
131
|
|
|
130
132
|
|
|
131
133
|
Full changes and verification: [CHANGELOG.md](CHANGELOG.md).
|
package/commands/session.md
CHANGED
|
@@ -48,3 +48,13 @@ This read is informational — session-start uses it to populate the resume bann
|
|
|
48
48
|
**Cold-start banner (PRD #500):** If `bootstrap.lock` exists with timestamp older than `cold-start.nudge-after-hours` (default 1h) and `sessions.jsonl` is empty, the SessionStart hook emits a one-time first-session nudge. Auto-silenced once `sessions.jsonl` has ≥ `cold-start.silence-after-sessions` entries (default 1).
|
|
49
49
|
|
|
50
50
|
**Invoke `session-orchestrator:session-start` via the `Skill` tool.** Follow its instructions precisely. Do NOT skip any phase. Do NOT make assumptions — verify everything in code and on the VCS platform.
|
|
51
|
+
|
|
52
|
+
## Headless (`claude -p`)
|
|
53
|
+
|
|
54
|
+
`session` and `plan` are **reserved terminal-only built-in names** in non-interactive sessions — under `claude -p` the bare form answers `"/session isn't available in this environment."`, and no frontmatter or manifest field overrides that (reproduced with an empty `CLAUDE_CONFIG_DIR` and no plugin loaded, claude 2.1.273, measured 2026-09-16). Use the namespaced form, which does resolve:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
claude -p "/session-orchestrator:session deep" --plugin-dir "$PWD"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Interactive sessions are unaffected — `/session` works there as it always has.
|
package/docs/ci-setup.md
CHANGED
|
@@ -366,6 +366,59 @@ Documenting it here for completeness:
|
|
|
366
366
|
job's hardcoded `oauth2:` clone login, so a Project Access Token (or PAT)
|
|
367
367
|
is the variant that delivers on that original reasoning.
|
|
368
368
|
|
|
369
|
+
## `pack-lifecycle` job (#1375)
|
|
370
|
+
|
|
371
|
+
Every other job in `.gitlab-ci.yml` measures the git checkout, where every
|
|
372
|
+
tracked file is present by construction. `pack-lifecycle` is the only one that
|
|
373
|
+
measures what an **npm consumer** actually receives: it runs
|
|
374
|
+
`npm run test:pack`, which packs a tarball, installs it into a throwaway
|
|
375
|
+
consumer package, and then executes the hook out of
|
|
376
|
+
`node_modules/session-orchestrator/` rather than out of this repo.
|
|
377
|
+
|
|
378
|
+
What it proves, in the four assertions of
|
|
379
|
+
`tests/scripts/pack-install-lifecycle.test.mjs`:
|
|
380
|
+
|
|
381
|
+
1. The sha512 of the tarball the test packed is byte-for-byte the `integrity`
|
|
382
|
+
the fresh consumer's own `package-lock.json` recorded — so a tarball mutated
|
|
383
|
+
between pack and install, or a pipeline installing something other than what
|
|
384
|
+
it packed, is caught. (It is not a re-hash of the extracted tree: unpacking
|
|
385
|
+
does not preserve mtimes, so that would differ for non-defect reasons.)
|
|
386
|
+
2. The **installed** `hooks/pre-bash-destructive-guard.mjs` denies `rm -rf /`
|
|
387
|
+
with a single PreToolUse deny envelope on stdout. This is the class the
|
|
388
|
+
repo-checkout suite is blind to: with `.orchestrator/policy/` missing from the
|
|
389
|
+
published artefact the guard warns on stderr, prints nothing, and **exits 0**
|
|
390
|
+
— fail-open, and indistinguishable from a healthy allow by exit code alone
|
|
391
|
+
(measured 2026-09-16: plugin root without the policy file → `exit=0`,
|
|
392
|
+
`stdout=""`; with it → `exit=0`, 634 bytes, `permissionDecision: deny`).
|
|
393
|
+
3. The installed guard still allows a benign command with empty stdout — so a
|
|
394
|
+
packed guard that denies everything is caught too.
|
|
395
|
+
4. Every `$CLAUDE_PLUGIN_ROOT/…` path the installed `hooks/hooks.json` wires (28
|
|
396
|
+
at the time of writing, incl. `hooks/run-node.sh`) exists inside the installed
|
|
397
|
+
tree.
|
|
398
|
+
|
|
399
|
+
`tests/scripts/pack-policy-floor.test.mjs` does **not** subsume this: it reads
|
|
400
|
+
the pack**list** (`npm pack --dry-run --json`), which is a manifest claim about
|
|
401
|
+
names. It never writes a tarball, never installs, and never runs a line out of
|
|
402
|
+
the artefact.
|
|
403
|
+
|
|
404
|
+
**Cost and why it is opt-in locally.** ~20–30 s for the whole job (measured
|
|
405
|
+
2026-09-16: `npm pack` ~15 s, `npm install <tgz>` ~3 s / 67 packages, suite wall
|
|
406
|
+
time 26.8 s). It is the one job that **reaches the npm registry** — the consumer
|
|
407
|
+
install resolves this package's own runtime dependencies — so it is excluded
|
|
408
|
+
from the default `npm test`. `vitest.config.mjs` includes `tests/**` and is
|
|
409
|
+
config-protected, so the gate is an env flag rather than an exclude:
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
npm run test:pack # SO_PACK_TEST=1 vitest --run tests/scripts/pack-install-lifecycle.test.mjs
|
|
413
|
+
npx vitest run tests/scripts/pack-install-lifecycle.test.mjs # without the flag → 4 skipped, exit 0
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
The job is hard-`needs`-ed by `pipeline-gate`, like `test`: deleting it from
|
|
417
|
+
this file is a pipeline-*creation* error rather than a silently narrower gate.
|
|
418
|
+
The trade-off that buys is registry availability — a registry outage fails this
|
|
419
|
+
job, and therefore the pipeline, without any code being wrong. Retry the job;
|
|
420
|
+
do not make it `allow_failure`, which would restore the silent hole.
|
|
421
|
+
|
|
369
422
|
## `pipeline-gate` — the fan-in job
|
|
370
423
|
|
|
371
424
|
The last stage holds one job that depends on every blocking gate. It exists
|
package/docs/codex-setup.md
CHANGED
|
@@ -197,7 +197,7 @@ $session-orchestrator:discovery # run quality probes; optionally add a s
|
|
|
197
197
|
$session-orchestrator:evolve analyze # manage learnings (analyze, review or list)
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
-
These are skill invocations in the Codex prompt, not shell commands. Invoking `go` reads the full canonical `
|
|
200
|
+
These are skill invocations in the Codex prompt, not shell commands. Invoking `go` reads the full canonical `skills/go/SKILL.md`, including its Express Path and prechecks; invoking `close` reads `skills/close/SKILL.md`, including its state and ledger checks before the session-end workflow. Codex's native `/goal` is a separate feature. Typing `/go` or `/close` alone is not a portable invocation contract; select the skill or use its explicit namespaced form.
|
|
201
201
|
|
|
202
202
|
### Manifest Compatibility
|
|
203
203
|
|
package/docs/components.md
CHANGED
|
@@ -7,7 +7,7 @@ Detailed component inventory and architecture reference for Session Orchestrator
|
|
|
7
7
|
```mermaid
|
|
8
8
|
flowchart LR
|
|
9
9
|
USER([Operator]) -->|invokes /session| COORD[Coordinator]
|
|
10
|
-
COORD -->|reads| SK[Skills<br/>
|
|
10
|
+
COORD -->|reads| SK[Skills<br/>50 user-facing]
|
|
11
11
|
COORD -->|invokes| CMD[Commands<br/>26 slash-cmds]
|
|
12
12
|
COORD -->|dispatches| AG[Agents<br/>14 typed sub-agents]
|
|
13
13
|
AG -.->|parallel waves| W1[code-implementer]
|
|
@@ -18,7 +18,7 @@ flowchart LR
|
|
|
18
18
|
COORD -->|writes| METRIC[.orchestrator/metrics/<br/>sessions · learnings · events]
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
## Skills (
|
|
21
|
+
## Skills (50 user-facing)
|
|
22
22
|
|
|
23
23
|
- **Lifecycle:** `session-start`, `session-plan`, `wave-executor`, `session-end`, `quality-gates`, `using-orchestrator`
|
|
24
24
|
- **Authoring:** `mcp-builder`, `hook-development`, `frontmatter-guard`
|
|
@@ -32,9 +32,14 @@ flowchart LR
|
|
|
32
32
|
- **Operator ergonomics:** `eli5` (plain-language restatement of the last answer)
|
|
33
33
|
- **Visualization:** `tmux-layout` (opt-in operator side-channel — [ADR-0007](adr/0007-tmux-visualization-substrate.md))
|
|
34
34
|
|
|
35
|
-
## Commands (
|
|
35
|
+
## Commands (28)
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
A slash command has exactly ONE definition. 26 are skills with explicit `user-invocable: true` in `skills/<name>/SKILL.md` (the same file the model dispatches; `argument-hint` and `disable-model-invocation` live there too), and 2 are plain `commands/*.md` files without a same-named skill. Claude Code registers both shapes as `/session-orchestrator:<name>`, so a name that exists as both a command file and a user-invocable skill is listed twice in the `/` picker — `tests/commands/headless-bare-command-availability.test.mjs` forbids that twin.
|
|
38
|
+
|
|
39
|
+
- **Skills (26):** `/autopilot`, `/bootstrap`, `/brainstorm`, `/close`, `/convergence-monitoring`, `/debug`, `/discovery`, `/dispatcher`, `/eli5`, `/eval`, `/evolve`, `/go`, `/grill`, `/harness-audit`, `/memory-cleanup`, `/npm-publish`, `/persona-panel`, `/plan`, `/portfolio`, `/reconcile`, `/release`, `/repo-audit`, `/spinout`, `/sunset-review`, `/test`, `/ux-grill`.
|
|
40
|
+
- **Command files (2):** `/session` (its skill is `session-start`) and `/templates-ack` (an in-session hook bypass with no skill body).
|
|
41
|
+
|
|
42
|
+
Under `claude -p`, `/session` and `/plan` are reserved terminal built-ins; use `/session-orchestrator:session` and `/session-orchestrator:plan` there.
|
|
38
43
|
|
|
39
44
|
## Agents (14 typed sub-agents)
|
|
40
45
|
|
|
@@ -52,7 +57,7 @@ Codex uses the curated six-event project subset `SessionStart`, `PreToolUse`, `P
|
|
|
52
57
|
|
|
53
58
|
- **Output Styles (3):** `session-report`, `wave-summary`, `finding-report`.
|
|
54
59
|
- **Policy & rules:** `.orchestrator/policy/blocked-commands.json` (destructive-command rules); `.claude/rules/parallel-sessions.md` (PSA-001..PSA-004).
|
|
55
|
-
- **Destructive-command guard.** On Claude Code, the active `hooks/pre-bash-destructive-guard.mjs` applies `.orchestrator/policy/blocked-commands.json` in the main session and in subagent waves. The policy has
|
|
60
|
+
- **Destructive-command guard.** On Claude Code, the active `hooks/pre-bash-destructive-guard.mjs` applies `.orchestrator/policy/blocked-commands.json` in the main session and in subagent waves. The policy has 11 blocking rules (`git reset --hard`, `rm -rf`, `git push --force`, and more) and 4 warning rules. Cursor and Pi use event bridges with documented limits; Codex does not enforce this guard. Scope `enforcement: warn` or `off` does not change the separate destructive-command policy — see [Platform support](../README.md#platform-support). Where the hook is active, bypass it per session only for intentional maintenance by setting `allow-destructive-ops: true` in Session Config. The rule source of truth is [`.claude/rules/parallel-sessions.md`](https://github.com/Kanevry/session-orchestrator/blob/main/.claude/rules/parallel-sessions.md) (PSA-003), vendored to consumer repos via `/bootstrap`.
|
|
56
61
|
- **Import probe.** `hooks/post-edit-import-probe.mjs` (PostToolUse on `Edit`/`Write`/`MultiEdit`) guards the other direction: a hook-reachable helper saved in a broken intermediate state makes *every* Bash/Edit/Write call fail with an internal hook error, host-wide, for every session sharing the working copy. Right after such a file is saved the probe runs ESLint `no-undef` on it (plus a child-process `import()` for `scripts/lib/**`) and reports the blast radius; it never blocks and always exits 0. It only fires for files listed in the committed allowlist [`hooks/_lib/hook-import-set.json`](../hooks/_lib/hook-import-set.json), regenerated by `node scripts/generate-hook-import-set.mjs`. Kill switch: `SO_DISABLED_HOOKS=post-edit-import-probe`.
|
|
57
62
|
- **Codex:** `.codex-plugin/plugin.json` (tracked `+codex.<UTC timestamp>` version), generated command and skill entrypoints under `.codex-plugin/skills/`, compatibility config, agent role definitions, and the public marketplace/add/list lifecycle implemented by `scripts/codex-install.mjs`. `scripts/generate-codex-skills.mjs` produces the name union with commands taking precedence; [Codex usage](codex-setup.md#usage) explains selection. Every installer run refreshes via `plugin add`; hook trust remains an operator decision in a fresh task through `/hooks`.
|
|
58
63
|
- **Cursor:** `.cursor-plugin/plugin.json` registers canonical skills and `.mcp.json` using Cursor's native manifest format. Additional native component discovery is explicitly disabled; `scripts/cursor-install.mjs` supplies the existing command and hook adapters. The former standard root manifest was moved to prevent [Codex manifest interception](codex-setup.md#manifest-compatibility).
|
|
@@ -74,7 +79,7 @@ Surface counts measured 2026-09-06 by this repo's 360° ecosystem probe (`docs/a
|
|
|
74
79
|
|
|
75
80
|
| Axis | session-orchestrator | `open-gsd/gsd-core` |
|
|
76
81
|
|---|---|---|
|
|
77
|
-
| Commands / skills / agents | 26 /
|
|
82
|
+
| Commands / skills / agents | 26 / 50 / 14 | 70 / 71 / 35 |
|
|
78
83
|
| Hook guards | 27 hook files, 10 event types | 28 hooks, incl. write / read / prompt / workflow / secret-read / agent-isolation / worktree-path guards |
|
|
79
84
|
| Cross-session learning | `/evolve` + confidence-scored `learnings.jsonl`; reconcile turns eligible learnings into PROPOSED rules an operator approves one by one | `gsd-extract-learnings`, `gsd-mempalace-*` |
|
|
80
85
|
| Harness coverage | Claude Code, Codex CLI, Cursor IDE, Pi (4) | 44 `capabilities/` directories (pi, hermes, kimi, windsurf, opencode, ollama, …) |
|
package/docs/events-schema.md
CHANGED
|
@@ -118,8 +118,10 @@ namespace we own. The validator + regex live in `scripts/lib/events-schema.mjs`
|
|
|
118
118
|
| `orchestrator.express_path.evaluated` | `scripts/lib/express-path.mjs` (`evaluateExpressPath`, emit in `_emitEvaluated`; name const `EXPRESS_PATH_EVENT`) | once per Phase-8.5 evaluation — **on refusal as well as activation**. Until #1119 this was unrecordable twice over: `scripts/lib/config.mjs` discarded the `express-path` key **even when the block was present** (synthetic probe: 88 keys emitted, none of them this one), and the decision lived only in `skills/session-start/phase-8-5-express-path.md` prose, so it fired only when a coordinator read that prose. Ledger evidence, measured 2026-08-23 @ `34321bc` (a count, so read it as history, not as state): **0** express events at that point, against 22 of the last 30 sessions running with no wave at all — every one of them `housekeeping`, the exact population the path targets. **Payload:** `activated` (always, boolean), `reasons` (always — the BLOCKING codes on refusal, the satisfied ones on activation; nothing short-circuits, so a refusal names every blocker and a reader can tell whether trimming the issue list alone would have helped). **Optional, absent-is-not-zero:** `enabled`, `session_type`, `task_count`, `parallel_agents_required`, plus `session_id`/`semantic_session_id` via `sessionAttribution`. An unmeasured `sessionType` or `taskCount` fails CLOSED (`reasons: ['session-type-unknown','task-count-unknown']`) — defaulting unknown scope to 0 would activate a gate-skipping path on data nobody supplied. **Four inputs, not three:** activation condition 3 carries two clauses (`≤ 3 issues` AND no parallel agents), which both condition matrices list as a non-activating row. **A missing `repoRoot` SKIPS the emit with a stderr WARN** rather than falling through to `SO_PROJECT_DIR` — that is the wave-1 incident of this session (a probe with an unexported var wrote a synthetic record into the real fleet ledger) made structurally impossible; a regression test reproduces it. `events.mjs` is imported lazily so `config.mjs`'s 48-file import graph does not gain `platform.mjs`, which runs filesystem walk-ups at module load |
|
|
119
119
|
| `orchestrator.foreign_dispatch.completed` | `scripts/lib/wave-executor/foreign-dispatch.mjs` (`dispatchForeign`, via `emitEvent(..., {repoRoot})` + `sessionAttribution(repoRoot)`) | once per foreign-model dispatch (#1150) — the replacement for `SubagentStop` telemetry, which cannot fire for a Bash-spawned `cursor-agent` child (no hook in the chain sees it). **Payload:** `model`, `role`, `ok`, `exit_code`, `timed_out`, `duration_s`, `changed_files` (count, tracked-modified ∪ untracked-new — `git diff` alone is blind to new files), `reason` (present on every refusal — `never-foreign-role`, `empty-diff`, `channel-unavailable`, `unsafe-*` — and on the failure classes of a completed run, so no failure class is reasonless), `hook_tampering` (tri-state: `true` = the child repointed/rewrote the shared `.git` hooks path, invalidates the run regardless of `ok`; `false` = fingerprint matched; absent/`null` = not measured, never read as clean), plus `session_id`/`semantic_session_id` via `sessionAttribution` (omitted, never fabricated, without a readable `session.lock`). Emitted on refusals too (`ok:false`), so a blocked dispatch is a record, not a silence |
|
|
120
120
|
| `orchestrator.remote_dispatch.completed` | `scripts/lib/wave-executor/remote-dispatch.mjs` (`dispatchRemote`, via `emitEvent(..., {repoRoot})` + `sessionAttribution(repoRoot)`; name const `REMOTE_DISPATCH_EVENT`) | once per REMOTE-host dispatch over the `offload` CLI (#1160) — the sibling of `foreign_dispatch.completed` on the other channel: that one sends a task to a foreign MODEL on this machine, this one sends a task to Claude on ANOTHER machine. Same reason for existing — a Bash-spawned `offload` child fires no `SubagentStop` hook, so this is the only ledger record a remote dispatch produces. **Payload:** `host` (the `offload` alias, never a hostname or an IP), `role`, `run_id`, `ok`, `exit_code`, `duration_ms`, `patch_files` (COUNT of paths parsed from the returned patch — `+++ b/` plus the `diff --git` header, because a DELETED file's `+++` is `/dev/null`), `patch_bytes`, `reason` (present on every refusal — `never-foreign-role`, `unsafe-run-id`, `unsafe-host`, `unsafe-patch-path` — and on every failure class of a completed run: `usage-config`, `host-unreachable`, `remote-command-failed`, `sync-failed`, `timeout`, `empty-diff`, `rate-limited`, `write-lock-busy`, `channel-unavailable`; absent means success, so no failure class is reasonless), plus `session_id`/`semantic_session_id` via `sessionAttribution` (omitted, never fabricated, without a readable `session.lock`). **Emitted on refusals too** (`ok:false`, `exit_code: null`, `duration_ms: 0`, `patch_files: 0`) — a blocked dispatch is a record, not a silence, and the null exit code is what keeps "refused" distinguishable from "attempted and measured empty". **Deliberately EXCLUDED, pinned by a test:** the prompt text, the patch BODY, and `patch_path` — this payload also travels over the optional Clank webhook with no redaction, and a tmp patch path names the run id and the operator's host |
|
|
121
|
-
| `orchestrator.wave_dispatch.scope_checked` | `hooks/pre-task-scope-disjoint.mjs` (name const `SCOPE_EVENT`; built by `decide()` as `verdict.telemetry`, emitted in `main()` via `emitEvent(..., {repoRoot: projectDir})` + `sessionAttribution(projectDir)`) | PreToolUse `Agent` — **once per dispatch DECISION** (#1092), awaited BEFORE the terminal `emitAllow`/`emitDeny`/`emitWarn`, all of which `process.exit()` and would discard a pending append. **Payload:** `hook`, `agent_id` (the coordinator's `description` + `subagent_type`, clamped to 120 chars), `declared_path_count`, `injected` (a `FILE-SCOPE` declaration was found AND at least one path survived parsing), `shape` (`fenced` \| `inline` \| `none` — which PARSER won, deliberately not a second spelling of `signal`: a fenced block whose lines are prose is `signal: 'unparseable', shape: 'none'`), `signal` (`marker-absent` \| `unparseable` \| `extracted` — the row-5-vs-row-6 distinction of the hook's error-class matrix), `ledger_result` (`no-scope` \| `allow` \| `allow-finished` \| `deny` \| `warn-ledger-corrupt` \| `warn-not-evaluable`), `collision_count` (collisions involving THIS dispatch, live or already-finished — `ledger_result` says which). **Optional, absent-is-not-zero:** `wave` (the number out of `waveKeyOf()`'s `w<N>` segment — **omitted, never `0`**, under the `<session>|w?|?` fallback, same contract as `quality_gate`'s `wave_number`), plus `session_id` / `semantic_session_id` via `sessionAttribution` — omitted, never fabricated, without a readable `session.lock`. **What it proves and what it does not:** that the hook SAW (or did not see) a declaration in the prompt the coordinator handed to the dispatch tool, and what the guard decided — the SEND side. It proves nothing about the block reaching the agent's context or the agent reading it; that receive-side half of #1092 stays open for want of a platform prompt-assembly boundary (`docs/scope-collision-guard.md` § 4.2). **No prompt body and no declared path is in the payload** (issue #1092 acceptance criterion 3) — counts and closed enums only, because this record also travels over the optional Clank webhook with no redaction. Rows 1–4 of the matrix emit nothing (no decision was made) and neither do the two crash rows 2/12 — a hook that fell over cannot describe itself, which is what the `GUARD INACTIVE` stderr banner is for |
|
|
121
|
+
| `orchestrator.wave_dispatch.scope_checked` | `hooks/pre-task-scope-disjoint.mjs` (name const `SCOPE_EVENT`; built by `decide()` as `verdict.telemetry`, emitted in `main()` via `emitEvent(..., {repoRoot: projectDir})` + `sessionAttribution(projectDir)`) | PreToolUse `Agent` — **once per dispatch DECISION** (#1092), awaited BEFORE the terminal `emitAllow`/`emitDeny`/`emitWarn`, all of which `process.exit()` and would discard a pending append. **Payload:** `hook`, `agent_id` (the coordinator's `description` + `subagent_type`, clamped to 120 chars), `declared_path_count`, `injected` (a `FILE-SCOPE` declaration was found AND at least one path survived parsing), `shape` (`fenced` \| `inline` \| `none` — which PARSER won, deliberately not a second spelling of `signal`: a fenced block whose lines are prose is `signal: 'unparseable', shape: 'none'`), `signal` (`marker-absent` \| `unparseable` \| `extracted` — the row-5-vs-row-6 distinction of the hook's error-class matrix), `ledger_result` (`no-scope` \| `allow` \| `allow-finished` \| `deny` \| `warn-ledger-corrupt` \| `warn-not-evaluable`), `collision_count` (collisions involving THIS dispatch, live or already-finished — `ledger_result` says which), `marker_found` (a declaration of any recognised shape was seen — `signal !== 'marker-absent'`; NOT a second spelling of `injected`, which additionally requires a path to have survived), `echo_instruction_present` (an `End your final report with the line: SCOPE-DIGEST: <8hex>` line was found in the SAME prompt). **Optional, absent-is-not-zero — the digest trio (#1092):** `scope_digest` (8-hex `scopeDigest()` over the paths extracted FROM THE PROMPT — the join key `scope-echo --verify` uses; **OMITTED for an empty scope**, never the digest of the empty string, which is a real 8-hex value that would join every marker-absent Discovery dispatch to every other), `instructed_digest` (the 8-hex the echo line names — omitted when no line was found), `digest_consistent` (`scope_digest === instructed_digest` — omitted unless BOTH are present; `false` is agent A's fenced block beside agent B's echo line, caught at dispatch time with no filesystem read). All three are computed inside `scopeDigestFields()`, which is TOTAL by construction: a throwing digest function costs the FIELD, never the verdict. **Why a digest and not `agent_id`:** measured 2026-09-16 over this host's ledger — 609 `scope_checked` against 51 `scope_echo_checked`, agent-id set overlap **zero** (send writes `description` + `subagent_type`, receive writes the coordinator's short handle), so the two halves were unjoinable. **Optional, absent-is-not-zero:** `wave` (the number out of `waveKeyOf()`'s `w<N>` segment — **omitted, never `0`**, under the `<session>|w?|?` fallback, same contract as `quality_gate`'s `wave_number`), plus `session_id` / `semantic_session_id` via `sessionAttribution` — omitted, never fabricated, without a readable `session.lock`. **What it proves and what it does not:** that the hook SAW (or did not see) a declaration in the prompt the coordinator handed to the dispatch tool, and what the guard decided — the SEND side. It proves nothing about the block reaching the agent's context or the agent reading it; that receive-side half of #1092 stays open for want of a platform prompt-assembly boundary (`docs/scope-collision-guard.md` § 4.2). **No prompt body and no declared path is in the payload** (issue #1092 acceptance criterion 3) — counts and closed enums only, because this record also travels over the optional Clank webhook with no redaction. Rows 1–4 of the matrix emit nothing (no decision was made) and neither do the two crash rows 2/12 — a hook that fell over cannot describe itself, which is what the `GUARD INACTIVE` stderr banner is for |
|
|
122
122
|
| `orchestrator.wave_dispatch.scope_echo_checked` | `scripts/lib/scope-echo.mjs` (name const `SCOPE_ECHO_EVENT`; verdict built by `checkScopeEcho()`, payload by `scopeEchoPayload()`, emitted from the CLI's `--emit` path via `emitEvent(..., {repoRoot})` + `sessionAttribution(repoRoot)`) | **coordinator-invoked, post-wave — once per agent** that reported in a wave, at `skills/wave-executor/references/wave-loop-review.md` step 3d-bis, after Edit-Persistence Verify. Never emitted at dispatch time and never by a hook. **Payload:** `agent_id` (the coordinator's agent id, clamped to 120 chars — omitted when not passed), `applicable` (boolean; `false` when the agent's DECLARED file-scope was EMPTY — nothing to echo a digest against — paired with `reason: 'scope-empty'`, so consumers filter this never-instructed population out of the echo rate before computing it), `echoed` (the report carried a well-formed `SCOPE-DIGEST: <8 hex>` marker), `match` (the echoed digest equals the digest of that agent's `<state-dir>/filescopes/wave-<N>/<agent-id>.json`), `expected_digest` / `actual_digest` (8-hex or `null`), `reason` (`echo-absent` \| `digest-mismatch` \| `scope-file-unreadable` \| `scope-empty` — present whenever `match` or `applicable` is false). **Optional, absent-is-not-zero:** `wave` (**omitted, never `0`**, when the caller passes no wave number — same contract as `scope_checked`'s `wave`), plus `session_id` / `semantic_session_id` via `sessionAttribution`. **What it proves and what it does not:** that the agent's final report carried the digest the coordinator injected beside the `FILE-SCOPE` block — the **receive** side of #1092, i.e. the line survived the round trip into the agent's context and back. It does NOT prove the model read, understood or obeyed the scope: the digest stands in the prompt and can be copied without ever reading the paths (the named BV-004 ceiling, `docs/scope-collision-guard.md` § 4.2). It is INFORMATIONAL — `match:false` or `echoed:false` blocks nothing and triggers no re-dispatch. **No path and no prompt body is in the payload** (issue #1092 acceptance criterion 3), same reason as the row above: this record also travels over the optional Clank webhook with no redaction |
|
|
123
|
+
| `orchestrator.wave_dispatch.scope_materialized` | `scripts/materialize-wave-scope.mjs` (name const `SCOPE_MATERIALIZED_EVENT`, defined in `scripts/lib/scope-echo.mjs`; emitted from `main()` after stdout, fire-and-forget with a `.catch`) | **once per `materialize-wave-scope` CLI run**, i.e. once per wave manifest (`wave-loop-scope-manifest.md` § 3.2). Never emitted by the exported `materializeWaveScope()` function — the seam is the CLI, so a library caller writes no record. **Payload:** `wave`, `agent_count` (records that got a per-agent file, i.e. excluding `peer-session-*`), `digest_count` (DISTINCT `scopeDigest()` values among those records' non-empty file arrays — lower than `agent_count` means two agents were handed the identical scope), `transport_observable`, plus `session_id` / `semantic_session_id` via `sessionAttribution`. **`transport_observable` is the DEGRADATION half of #1092:** true iff a `PreToolUse` entry with matcher `Agent` is registered in the plugin's active `hooks/hooks.json`. On Codex / Cursor / Pi it is false BY DESIGN (no `Agent` dispatch tool — the asymmetry is registered in `DOCUMENTED_ASYMMETRIES`), and there a missing `scope_checked` record is **not** evidence of a missing injection; `scope-echo --verify` reads this field and degrades every verdict to `echo-only`. **Fails CLOSED** on any unreadable hooks file: observability we cannot prove would produce false `injection-missing` accusations. `repoRoot` is the state directory's PARENT, never `process.cwd()`, so a run from a subdirectory cannot write into another repo's ledger. Silent on failure — the corpus pins byte-empty stderr on this command's success path |
|
|
124
|
+
| `orchestrator.wave_dispatch.scope_verified` | `scripts/lib/scope-echo.mjs` (name const `SCOPE_VERIFIED_EVENT`; report by `verifyWaveScope()`, payload by `scopeVerifiedPayload()`, emitted from the `--verify --emit` path) | **coordinator-invoked, post-wave — exactly ONCE per wave** (`wave-loop-review.md` step 3d-bis, after the per-agent `--emit` calls). This is the JOIN of the three halves — `scope_checked`, the `<state-dir>/filescopes/wave-<N>/*.json` artefacts, and `scope_echo_checked` — **keyed on the digest, never on `agent_id`** (see the `scope_checked` row for the zero-overlap measurement that forces it). **Payload:** `wave`, `transport_observable`, `dispatches`, `injected`, `echoed`, `malformed_lines`, `by_verdict` (a count per verdict), `digests` (the 8-hex keys the row covers). `malformed_lines` counts the ledger lines the join could not parse (a writer killed mid-append leaves a truncated line — a measured shape here) and is **ALWAYS present, including as `0`**, like the three counters beside it: it is the honesty check on the denominator, because a join that silently dropped half the ledger otherwise writes a record byte-identical to a clean wave (HR-105). `malformed_lines > 0` means every count and verdict in the row is a FLOOR, not a census; the human table says so beside them. **Deliberately NO `agent_id` and no path** — unlike its two halves, this record carries a LIST, and an agent id is a free-form coordinator string that has carried private project slugs; the per-agent verdicts stay on stdout, where they never reach the webhook. **Verdict enum — SIX members** (precedence order, one row per digest, all kebab-case): `duplicate-claim` (≥2 distinct agent ids claimed one digest — agent A's scope reported for agent B) · `echoed-not-injected` (an echo names a digest no dispatch claimed) · `digest-unknown` (no scope file on disk carries it) · `matched` · `injected-not-echoed` (the normal state during a wave, before reports land) · `injection-missing` (a scope file no dispatch claimed and no agent echoed — the omitted-injection case, #1092 AC-2). **Degraded value, NOT a seventh member:** `echo-only` — what EVERY verdict collapses to when `transport_observable` is false; the precedence chain never produces it, so it is absent from `SCOPE_VERDICTS`. **Spelling migration:** `injection-missing` was `injection_missing` (the one snake_case member) until 2026-09-16 — records written before that date may carry the old key in `by_verdict`, and there is no dual-emit, so a consumer reading history must accept both. **Exit 0 for every verdict** — the tool reports; the wave-executor turns `injection-missing` / `duplicate-claim` into a STATE.md deviation, never a block |
|
|
123
125
|
| `orchestrator.hook.import_probe_failed` | `hooks/post-edit-import-probe.mjs` | PostToolUse(Edit\|Write\|MultiEdit), after a module listed in `hooks/_lib/hook-import-set.json` (the committed hook-reachable allowlist) fails the probe. **Payload:** `file` (repo-relative), `check` (`eslint` \| `import` — which of the two checks caught it), `error` (the first offending message: a `no-undef`/fatal ESLint message, or the import diagnostic line), `reachable_from` (the hook entry basenames that import this module — the blast radius, since a throwing helper turns every tool call into "Internal hook error — request blocked" host-wide, #1224), `duration_ms`. Emitted ONLY on failure; a clean edit produces no record, so the event count IS the incident count |
|
|
124
126
|
| `orchestrator.reconcile.completed` | `scripts/lib/reconcile/engine.mjs` (`emitReconcileCompleted`, called from the thin `runReconcile` wrapper; name const `RECONCILE_EVENT`) | one record per `runReconcile` call (#1192) — from the WRAPPER, so all three return points are covered: the empty short-circuit, the normal tail, and the never-throws catch. An inline emit would have missed two of them, including the empty corpus and the error path — the two runs an operator most needs recorded (`.claude/rules/host-resources.md` § HR-105). Same shape as `narrative_mirrored`'s wrapper, and the emit is try/catch-wrapped because `emitEvent` THROWS `EventValidationError`, which would otherwise break `runReconcile`'s never-throws contract. **Payload, all ALWAYS present including as `0`** (each was measured over the whole run, like `mirror_run_completed`'s counters): `trigger` (`skill` | `session-end` | `phase-skip` | `unknown` — written always, so the per-trigger denominator is complete; the two markdown callers depend on a coordinator passing it, hence the honest `unknown` default), `dry_run`, `learnings_total`, `eligible`, `proposals`, `rejected`, `capped`, `already_materialized` (`summary.alreadyMaterialized` — the REAL idempotent-skip count, #484), `candidate_store_merged` (boolean — the `reconcile-candidates.jsonl` idempotency-sidecar merge, derived from `summary.written` at `scripts/lib/reconcile/engine.mjs:791`, which is the engine's ONLY disk write. It is NOT a rule-write signal: the engine never touches `.claude/rules/`, and this event is emitted BEFORE the operator-approval AUQ. For "a rule reached `.claude/rules/`" read `orchestrator.reconcile.rules_written` (row below) — its `rules_written` count is the only field that carries that claim. **Renamed from `written` in #1315** because the old name read as "rule files were written" and was misread on exactly that basis (#1307). The legacy key `written` is STILL emitted, carrying the identical boolean from the same expression so the two can never disagree — deprecated, removal **2027-03-13**, same one-generation dual-emit convention as the `orchestrator.session.stopped` → `orchestrator.turn.stopped` rename (#1234). `schema_version` is NOT bumped: it versions the record envelope, and the payload contract here stays additive), `duration_ms`. **Optional, absent-is-not-zero:** `targets` (the caller's effective target list; absent ⇒ none asserted), `store_records_dropped` (`summary.skipped` — absent ⇒ the candidate store was never INSPECTED: under `dryRun`, on the empty short-circuit, on the error path; a `0` there would be a false all-clear), and `aborted: 'engine-error'` + `reason` (clamped to 300 chars) — present ONLY when the never-throws guard fired; their absence means "ran to the end", never "unknown". **`dry_run` is the discriminator, not the event's absence:** the `phase-skip` caller runs dry on EVERY close and is the highest-volume trigger, so consumers filter `dry_run: false` for real runs. **A missing `repoRoot` SKIPS the emit with a stderr WARN** rather than falling back to `SO_PROJECT_DIR` — most engine tests pass none, and the fallback would append synthetic records to the real fleet ledger on every `npm test` (#1119, same contract as `express_path.evaluated` and `narrative_mirrored`); a regression test pins it. **Engine identities — the counters are NOT a flat partition, and a live payload reads as inconsistent without them** (source: `scripts/lib/reconcile/engine.mjs:64-83`): `learnings_total === proposals + rejected`, and `capped` + `already_materialized` are DIAGNOSTIC SUB-COUNTS *inside* `rejected`, not siblings of it — each capped or already-materialized learning is also counted as rejected. Within the eligible set: `eligible − proposals − capped === already_materialized`. Worked against a live record: `learnings_total 164 = proposals 10 + rejected 154`, and `eligible 102 − proposals 10 − capped 72 = already_materialized 20` — with `capped 72` and `already_materialized 20` both sitting inside those 154. `already_materialized` is computed BEFORE the volume brake, so a terminal learning never consumes a new learning's quota. `session_id` / `semantic_session_id` / `wave` / `schema_version` are stamped by `emitEvent()` |
|
|
125
127
|
| `orchestrator.reconcile.rules_written` | `scripts/lib/reconcile/writer.mjs` (`emitRulesWritten`, called from the tail of `writeApprovedRules`; name const `RULES_WRITTEN_EVENT`) | one record per rule-WRITE pass (#1307) — the companion `orchestrator.reconcile.completed` is emitted by the `runReconcile` wrapper, which runs BEFORE the operator-approval AUQ and before this module is reached at all, so a `dry_run: false` record there proves the engine ran and merged the candidate store, NEVER that a rule reached `.claude/rules/`: an operator who declines every proposal emits a byte-identical record to one who approves five. This event is the one that proves the write. Emitted from the FUNCTION TAIL, so both return points are covered — the normal pass and the lock-acquisition failure (which is a zero-write pass carrying `write_errors: 1`). **Payload, all ALWAYS present including as `0`:** `rules_written` (FILE count, not proposal count — one approved proposal written to two targets counts twice), `approved_proposals` (how many the operator approved), `rejected_archived` (records appended to `.orchestrator/reconcile.rejected.log`), `write_errors` (`result.errors.length`). **Optional, absent-is-not-zero:** `targets` (allowlisted to the CLOSED `TARGET_DIRS` key set — `repo-local` \| `baseline` — because the list originates in operator-authored Session Config and an unknown value would be a verbatim echo of untrusted text; absent ⇒ none in effect). **A ZERO-WRITE PASS IS EMITTED, and the discriminator is a FIELD, never the event's absence** (same convention this table states for `dry_run` one row up, and `.claude/rules/host-resources.md` § HR-105): `rules_written: 0` with `approved_proposals: 0` is *the operator declined everything*, while `rules_written: 0` with a non-zero `approved_proposals` and `write_errors` is *every write was refused by a guard* — outcomes a success-only emitter would collapse into one silence, together with *the writer was never reached*. The ONE case that emits nothing is the caller's true no-op (neither an approved nor a rejected item), which returns before the lock is taken. **The emit is not a write:** the #693 FA2/FA3 brandmauer is unchanged — `writeApprovedRules` is still the only module that writes rule files and still writes only operator-approved items. **A missing `repoRoot` SKIPS the emit silently** (same #1119 contract as the row above), and the whole emit is try/catch-wrapped to stderr because `emitEvent` THROWS `EventValidationError`, which would otherwise break `writeApprovedRules`'s never-throws contract. `session_id` / `semantic_session_id` / `wave` / `schema_version` are stamped by `emitEvent()` |
|
|
@@ -127,6 +129,7 @@ namespace we own. The validator + regex live in `scripts/lib/events-schema.mjs`
|
|
|
127
129
|
| `orchestrator.dialectic.completed` | `scripts/lib/learnings/evolve-telemetry.mjs` (`recordDialecticRun`), called from `scripts/dialectic-deriver.mjs`'s `runDialecticDeriver()` for its FOUR return-based outcomes (`empty-input`, `budget-exceeded`, `would-empty-card`, and the dry-run `ok` success form — all four are values the function itself returns, so it records each one mechanically at the exact return point), and from `skills/evolve/SKILL.md` Step 6.4's apply branch / Step 6.5's two THROWN outcomes (`unknown-model` from `validateModel()`, `subagent-crash` from a failed `dispatchAgent`/`Agent()` call — neither is a `runDialecticDeriver()` return value, so only the skill-prose catcher one layer up can record them; apply-mode success similarly needs the post-merge `mergePeerCard()` stats `runDialecticDeriver()` does not have) | once per `/evolve --dialectic` run (#1200, mechanized #1206). Same fleet-zero gap as `evolve.completed` above, now closed the same way: the pipeline function records itself for every outcome it alone can determine, and only the two throw-based aborts (of Step 6.5's five-slug abort enum) plus apply-mode's merge-dependent success remain a skill-prose call site. **Payload (success):** `mode` (`dry-run` \| `apply`), `user_deltas`, `agent_deltas` — the two modes measure DIFFERENT quantities, so never compare a dry-run delta with an apply delta. Dry-run: `countManagedSections()` on the PROPOSED diff text — non-string or empty → 0; if the body carries `<!-- BEGIN MANAGED: … -->` sentinels → their count; otherwise the count of `## ` headings, fence-aware (headings inside ```` ``` ```` / `~~~` blocks do not count); a non-empty body with no headings → 1. Apply: `mergePeerCard()`'s own `stats.replaced + stats.appended` per target — managed (sentinel) sections of the card actually replaced or newly appended by the merge; hand-authored sections are `preserved` and never counted, `tokens_in`, `tokens_out`, `duration_ms`. **Payload (abort):** `aborted` (`unknown-model` \| `budget-exceeded` \| `would-empty-card` \| `empty-input` \| `subagent-crash`), `duration_ms`. Same #1119 refusal as `emitEvolveCompleted` — `recordDialecticRun()` skips the emit (stderr WARN) without an explicit `repoRoot` |
|
|
128
130
|
| `orchestrator.dialectic.nudge_decided` | **HISTORICAL — no emission since 2026-09-09.** The producer wrapper (`decideAndRecordAutoDialectic` in `scripts/lib/auto-dialectic.mjs`, #1200 part c) was REMOVED in #1288 after its only caller — session-end Phase 3.6.7 — was retired; the session-start `maintenance-due` probe (`scripts/lib/maintenance-due-banner.mjs`) reads the side-effect-free `shouldDispatchAutoDialectic()` instead. | never — no emitter exists. Row kept so records written before 2026-09-09 stay readable. **Payload (historical):** `decided` (boolean, mirrored `trigger`), `reason`, `cadence`, `sessions_since`, `learnings_since`, plus `session_id`/`semantic_session_id` via `sessionAttribution`. |
|
|
129
131
|
| `orchestrator.learnings.sweep_applied` | `scripts/lib/session-end/tail-runner.mjs` (`runExpiredSweep`, emit in `emitSweepApplied`; name const `SWEEP_EVENT`) | once per APPLIED session-end Phase 3.6.4 Expired-Learnings Sweep — emitted only on the write path, never on a plan-skip, a no-plan call, or the never-throws error branch, so a record's presence is proof the active store was actually rewritten. This event exists because the apply path did not: until #723-B4 was wired here, `sweepExpiredLearnings` had no session-end caller at all (census 2026-09-09 — definition, the `dryRun: true` probe in `phase-skip.mjs`, the standalone CLI, tests), so 0 sweeps were ever applied across three consumer repos while 628 learnings stayed resident. The sweep CLI (`scripts/sweep-expired-learnings.mjs:210`) deliberately emits nothing, and `orchestrator.evolve.completed` covers only the `--prune --apply` sibling — this is the ONLY record of a time-driven sweep. **Payload:** `scanned` (entries read from `learnings.jsonl`), `archived` (entries moved to `learnings-archive.jsonl`; a measured `0` is emitted, since the planner can legitimately RUN on a fail-open probe-error), `source` (always `session-end-3.6.4`, separating this producer from any future one), plus `session_id` / `semantic_session_id` via `sessionAttribution(repoRoot)` — the SAME root the record is pinned to via `emitEvent(..., {repoRoot})`, both keys OMITTED rather than fabricated when no `session.lock` is readable. Emission is best-effort and wrapped in its own catch: a telemetry failure never changes the sweep's return value, and — like the sweep itself — can never block a session close. |
|
|
132
|
+
| `orchestrator.rules.expiry_sweep_applied` | `scripts/sweep-expired-rules.mjs` (emit at the tail of `main`, after the writes; name const `RULE_EXPIRY_SWEEP_EVENT` in `scripts/lib/reconcile/rule-expiry-sweep.mjs`) | once per APPLIED generated-rule expiry sweep (#1377) — the `.claude/rules/*.md` counterpart of `orchestrator.learnings.sweep_applied` one corpus over: that one archives expired LEARNINGS, this one removes the expired ENTRIES the reconcile engine generated from them. Emitted on the `--apply` path ONLY and AFTER the rewrites/deletes, so a record's presence is proof that tracked rule files actually changed; a `--dry-run` (the DEFAULT) emits nothing, which is why the discriminator here is the event's presence and not a `dry_run` field. This event exists because the removal half did not: `rule-loader.mjs` stopped INJECTING an expired generated rule at read time and nothing ever removed one from disk, so an expired file stayed tracked and kept counting against `generated-byte-ceiling` while shipping to no wave. **Payload, all ALWAYS present including as `0`:** `rewritten` (files whose expired prose blocks were removed — their `## Provenance` pairs are KEPT as `markers only`, because `/reconcile` dedupes on those markers and dropping one re-proposes the learning), `deleted` (files whose every substantive entry expired; each one's pairs are stamped terminal via `markCandidateProcessed` BEFORE the unlink), `stamped` (candidate records stamped for those deletes — `0` whenever `deleted` is `0`), `write_errors` (per-file failures; a non-zero value also makes the CLI exit 2), `expired_entries` (entries the plan judged expired across all files), `files_scanned` (machine-generated rule files enumerated), `source` (always `sweep-expired-rules-cli`, separating this producer from any future session-end caller). A ZERO-WRITE APPLY IS EMITTED: `rewritten: 0` + `deleted: 0` is *nothing was expired*, while a non-zero `write_errors` beside them is *every write was refused* — outcomes a success-only emitter would collapse into one silence (`.claude/rules/host-resources.md` § HR-105). **Not in the payload, deliberately:** the per-file plans, including the `no-1to1-mapping` skips and the unresolvable `learning-id`s. Those carry learning subjects and rule slugs, and this record travels verbatim over the optional Clank webhook with no redaction — the full plan stays on stdout (`--json`). The emit is try/catch-wrapped because `emitEvent` THROWS `EventValidationError`, which must never turn a completed sweep into a failed one; `session_id` / `semantic_session_id` / `wave` / `schema_version` are stamped by `emitEvent()` |
|
|
130
133
|
| `orchestrator.session.shape_resolved` | `scripts/lib/session-shape.mjs` (`resolveAndRecordSessionShape`, emit in `_emitShapeResolved`; name const `SESSION_SHAPE_EVENT`), reachable as an entrypoint via `scripts/session-shape.mjs` | once per session-shape resolution, at the moment the coordinator turns the confirmed mode into an execution plan. Until this event existed the shape lived in PROSE at 27 sites contradicting each other in 8 answers (measured 2026-09-09), so "how many waves did this session actually run" was unanswerable from the ledger — which is why 6 consumer-repo `housekeeping` sessions ran the full 5-wave deep shape unnoticed. **Payload:** `session_type` (always), `total_waves`, `waves_config_honored` (false exactly when the ultradeep profile ignored the Session Config `waves` value — the fixed 7-wave shape of `skills/session-plan/SKILL.md` § Role-to-Wave Mapping; PRD AC-9's `waves < 7` rejection was dropped 2026-09-09), `discovery`, `agent_caps` (one clamped cap per wave, in wave order — `min(tier raw, agents-per-wave)`, `0` on a coordinator-direct wave), `coordinator_direct_waves` (the 1-based `n` of every coordinator-direct wave; `[]` is a MEASURED empty list, not an omission — a housekeeping shape must read `[1]` and a plain deep shape `[]`), `shape_version` (the `SESSION_SHAPE_VERSION` contract the record was produced under). **Optional, absent-is-not-zero:** `session_profile` (OMITTED, never `null`/`''`, when the session has no profile — a written null would read as "measured, no profile"; value set is the closed `VALID_SESSION_PROFILES`), `task_count`, plus `session_id`/`semantic_session_id` via `sessionAttribution(repoRoot)` — the SAME root the record is pinned to, omitted rather than fabricated without a readable `session.lock`. **A missing `repoRoot` SKIPS the emit with a stderr WARN** rather than falling through to `SO_PROJECT_DIR` (#941), and `--no-event` skips it entirely so a planning dry-run cannot record a session that never ran. `events.mjs` is imported lazily so a pure-resolver consumer does not gain `platform.mjs` and its module-load filesystem walk-ups |
|
|
131
134
|
| `orchestrator.issue_budget.reconciled` | `scripts/lib/issue-budget-reconcile.mjs` (`reconcileIssueBudget`, emit in `emitIssueBudgetReconciled`; name const `ISSUE_BUDGET_RECONCILED_EVENT`) | once per session close — the cross-check between what the session RECORDED as created (`record.issues_created.length`) and what the issue-budget ledger CHARGED. It exists because `readBudgetState` returns a ZEROED state for a MISSING counter file, so "the hook never ran for a single create" and "the session created nothing" are byte-identical in its return value; measured 2026-09-09 on a real session record with **26** recorded creations, **0** charged and no counter file under either accounting key. **Payload:** `verdict` (always — `match` \| `escaped` \| `no-ledger` \| `stale-record`; `no-ledger` is the absent-ledger case above, `stale-record` the inverse), `recorded`, `charged`, `exempt`, `overflow`, `escaped` (= `max(0, recorded − charged − exempt)`), and `ledgers` — one record per accounting key looked up (`key`: `semantic`\|`raw`, `path` — **repo-RELATIVE** `.orchestrator/runtime/issue-budget/<hash>.json`, never absolute: this payload also travels over the optional Clank webhook with no redaction, and an absolute ledger path names the operator's home directory and the private repo slug; the absolute form stays in the local WARN text only, `found`, `charged`, `exempt`); a fifth verdict `corrupt-ledger` marks a file that exists but has a non-integer `count`. **BOTH keys are read and SUMMED**, because the accounting key is semantic only when `current-session.json` verified the raw id (`resolveIssueBudgetSessionId`) — measured in one consumer repo: 25 of 36 counter files keyed semantic, 11 keyed raw, so reading one key reports a phantom escape for every session that used the other. `found` is measured with `existsSync` BEFORE the read and is the only thing that separates `no-ledger` from a real zero — read an absent `found` as "not measured", never as "no spend". Plus `session_id`/`semantic_session_id` via `sessionAttribution`. **A missing `repoRoot` SKIPS the emit with a stderr WARN** rather than falling through to `SO_PROJECT_DIR` (#941); `events.mjs` is imported lazily so no consumer of the reconcile module gains `platform.mjs`'s module-load filesystem walk-ups |
|
|
132
135
|
| `orchestrator.issue_budget.refunded` | `hooks/post-bash-issue-budget-refund.mjs` (`emitRefundDecision`; name const `ISSUE_BUDGET_REFUNDED_EVENT`) | once per refund DECISION — one record per `PostToolUseFailure` delivery whose command contains at least one `gh`/`glab issue create` statement, emitted on the no-op branches too so a census over N sessions has a denominator and not only a numerator. Before #1353 a refund wrote a stderr line only, which under exit 0 reaches the debug log alone: the refund path was unfalsifiable in the sense of `.claude/rules/host-resources.md` HR-105. **Payload:** `reason` (always, CLOSED enum — `refunded` \| `not-charged` \| `chain-not-attributable` \| `counter-at-zero` \| `no-signal`), `unit` (`count` \| `exempt` \| `null` — which counter was given back, read off the honoured charge records; `exempt` only when EVERY refund landed on the exempt counter, `null` whenever nothing was refunded), `statement_count` (issue-create statements `findIssueCreateStatements` found), plus `session_id`/`semantic_session_id` via `sessionAttribution`. Branch mapping: `no-signal` = G2b (a failure event carrying no failure FIELD), `chain-not-attributable` = G3b (the create is not the whole command, so the exit code judges neither), `refunded` = G5 with at least one honoured charge record, `not-charged` = G5 with none (parked at the cap, a re-delivered failure, or an identity-less call `refundBooking` answers `no-session` for). **`counter-at-zero` has no branch in the hook today**: a matched record whose counter is already 0 is absorbed by `refundBooking`'s never-below-zero guard and returns `refunded` like any other match, so the case is not observable without a new field on the shared core's verdict (`scripts/lib/issue-budget.mjs`) — the enum value is reserved, not dead. `mode: off` emits NOTHING (G4 returns before it, and there was no charge to give back). **No command text, issue title or path is in the payload** — it travels verbatim over the optional Clank webhook with no redaction, same rule as `orchestrator.issue_budget.reconciled`. `events.mjs` is imported lazily, awaited AND caught: a throwing emit must never change the hook's exit code or output |
|
package/docs/install.md
CHANGED
|
@@ -29,6 +29,22 @@ uninstall.
|
|
|
29
29
|
| **Cursor IDE** | `git clone https://github.com/Kanevry/session-orchestrator.git ~/Projects/session-orchestrator && cd ~/Projects/session-orchestrator && npm install && node scripts/cursor-install.mjs /path/to/your/project` |
|
|
30
30
|
| **Pi** | `pi install npm:session-orchestrator` ; dev fallback: `git clone https://github.com/Kanevry/session-orchestrator.git ~/Projects/session-orchestrator && cd ~/Projects/session-orchestrator && npm install && node scripts/pi-install.mjs /path/to/your/project --settings-only` |
|
|
31
31
|
|
|
32
|
+
### Headless Claude Code (`claude -p`): two commands need the namespaced form
|
|
33
|
+
|
|
34
|
+
`session` and `plan` are reserved terminal-only built-in names in non-interactive
|
|
35
|
+
sessions. Under `claude -p` the bare form answers `"/session isn't available in
|
|
36
|
+
this environment."` — that is the harness, not the plugin, and no frontmatter or
|
|
37
|
+
manifest field overrides it (claude 2.1.273, measured 2026-09-16). Use the
|
|
38
|
+
namespaced form:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
claude -p "/session-orchestrator:session deep"
|
|
42
|
+
claude -p "/session-orchestrator:plan feature"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Every other command keeps its bare form (`/go`, `/close`, `/test`, …), and
|
|
46
|
+
interactive sessions are unaffected.
|
|
47
|
+
|
|
32
48
|
### Claude Code: install the Node dependencies once
|
|
33
49
|
|
|
34
50
|
For Claude Code, also install the package's Node dependencies **once** and
|
package/docs/persona-panel.md
CHANGED
|
@@ -422,7 +422,7 @@ or commit them if you want an auditable history.
|
|
|
422
422
|
|
|
423
423
|
## See Also
|
|
424
424
|
|
|
425
|
-
- `
|
|
425
|
+
- `skills/persona-panel/SKILL.md` — command reference: argument syntax, flag validation, examples
|
|
426
426
|
- `skills/persona-panel/SKILL.md` — full skill spec: 6 phases, catalog format, dispatch
|
|
427
427
|
mechanics, consolidation logic, sidecar schema
|
|
428
428
|
- `skills/persona-panel/persona-format.md` — persona file format specification: frontmatter
|
package/docs/pi-setup.md
CHANGED
|
@@ -76,7 +76,7 @@ The package manifest in `package.json` exposes:
|
|
|
76
76
|
|
|
77
77
|
- `pi.extensions`: `./pi/extensions/session-orchestrator.ts`
|
|
78
78
|
- `pi.skills`: `./skills`
|
|
79
|
-
- `pi.prompts`: `./pi/prompts/*.md` generated from `commands/*.md`
|
|
79
|
+
- `pi.prompts`: `./pi/prompts/*.md` generated from `commands/*.md` plus every skill with explicit `user-invocable: true`
|
|
80
80
|
|
|
81
81
|
The extension uses `hooks/hooks-pi.json` and `scripts/lib/pi-hook-bridge.mjs` to translate Pi events into the existing hook stdin contract.
|
|
82
82
|
|