session-orchestrator 3.20.0 → 3.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/030-wave-execution.mdc +10 -8
- package/CHANGELOG.md +414 -0
- package/README.md +16 -11
- package/agents/analyst.md +1 -1
- package/agents/architect-reviewer.md +1 -1
- package/agents/code-implementer.md +4 -2
- package/agents/db-specialist.md +1 -1
- package/agents/dialectic-deriver.md +1 -1
- package/agents/docs-writer.md +1 -1
- package/agents/memory-proposal-collector.md +1 -1
- package/agents/qa-strategist.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/session-reviewer.md +42 -1
- package/agents/skill-applied-judge.md +1 -1
- package/agents/test-writer.md +1 -1
- package/agents/ui-developer.md +1 -1
- package/agents/ux-evaluator.md +1 -1
- package/commands/release.md +60 -0
- package/docs/migration-v3.md +9 -6
- package/docs/persona-panel.md +3 -1
- package/docs/scope-collision-guard.md +167 -0
- package/hooks/enforce-scope.mjs +103 -3
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +11 -1
- package/hooks/on-session-end.mjs +52 -5
- package/hooks/on-stop.mjs +127 -12
- package/hooks/post-bash-write-verify.mjs +8 -32
- package/hooks/pre-task-scope-disjoint.mjs +1042 -0
- package/package.json +1 -1
- package/pi/prompts/release.md +12 -0
- package/scripts/autopilot.mjs +3 -1
- package/scripts/emit-session.mjs +42 -0
- package/scripts/export-hw-learnings.mjs +61 -2
- package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
- package/scripts/lib/backlog-scan.mjs +106 -15
- package/scripts/lib/build-live-signals.mjs +7 -3
- package/scripts/lib/ci-status-banner.mjs +207 -23
- package/scripts/lib/git-config-drift.mjs +471 -0
- package/scripts/lib/harness-audit/categories/category6.mjs +65 -12
- package/scripts/lib/io.mjs +193 -7
- package/scripts/lib/learnings/select.mjs +157 -3
- package/scripts/lib/memory-cleanup-stamp.mjs +132 -8
- package/scripts/lib/mirror-issues-banner.mjs +266 -0
- package/scripts/lib/named-vault-resolver.mjs +105 -16
- package/scripts/lib/peer-cards/schema.mjs +6 -2
- package/scripts/lib/reconcile/writer.mjs +69 -30
- package/scripts/lib/redact-spans.mjs +89 -0
- package/scripts/lib/scope-baseline.mjs +77 -17
- package/scripts/lib/scope-gate.mjs +658 -0
- package/scripts/lib/secret-masker.mjs +262 -0
- package/scripts/lib/session-lock.mjs +34 -10
- package/scripts/lib/session-registry.mjs +9 -1
- package/scripts/lib/state-md/mission-status.mjs +21 -12
- package/scripts/lib/tmux-layout/vcs-detector.mjs +108 -4
- package/scripts/lib/validate/check-agents.mjs +77 -5
- package/scripts/lib/validate/check-commands.mjs +2 -20
- package/scripts/lib/validate/check-doc-cli-commands.mjs +514 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +18 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +185 -17
- package/scripts/lib/validate/check-skills.mjs +191 -0
- package/scripts/lib/validate/check-test-git-config-target.mjs +665 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +22 -2
- package/scripts/lib/validate/check-untracked-test-deps.mjs +925 -0
- package/scripts/lib/validate/check-unwired-features.mjs +219 -11
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +965 -0
- package/scripts/lib/validate/frontmatter-block.mjs +61 -0
- package/scripts/lib/validate/tier-inference.mjs +46 -8
- package/scripts/lib/vault-mirror/namespace.mjs +146 -1
- package/scripts/lib/vault-mirror/process.mjs +264 -31
- package/scripts/lib/vault-mirror/render-sessions.mjs +115 -4
- package/scripts/lib/vault-status/board-writer.mjs +300 -56
- package/scripts/lib/vault-status/narrative-mirror.mjs +119 -5
- package/scripts/lib/vcs-repo-spec.mjs +500 -19
- package/scripts/print-learnings-index.mjs +30 -3
- package/scripts/release.mjs +616 -61
- package/scripts/site-numbers.mjs +1049 -0
- package/scripts/validate-plugin.mjs +61 -0
- package/scripts/validate-wave-scope.mjs +281 -12
- package/scripts/vault-mirror.mjs +26 -1
- package/skills/_shared/monitor-patterns.md +24 -4
- package/skills/_shared/state-ownership.md +17 -0
- package/skills/claude-md-drift-check/SKILL.md +9 -1
- package/skills/debug/SKILL.md +4 -1
- package/skills/discovery/issue-templates.md +4 -4
- package/skills/discovery/probes-code.md +2 -2
- package/skills/discovery/probes-feature.md +6 -6
- package/skills/discovery/probes-infra.md +2 -2
- package/skills/discovery/probes-session.md +5 -5
- package/skills/dispatcher/SKILL.md +10 -1
- package/skills/frontmatter-guard/SKILL.md +9 -1
- package/skills/gitlab-ops/SKILL.md +49 -39
- package/skills/gitlab-portfolio/SKILL.md +10 -1
- package/skills/memory-cleanup/SKILL.md +18 -5
- package/skills/npm-publish/SKILL.md +22 -50
- package/skills/persona-panel/SKILL.md +3 -1
- package/skills/plan/mode-new.md +23 -5
- package/skills/repo-audit/SKILL.md +10 -1
- package/skills/session-end/SKILL.md +44 -2
- package/skills/session-end/metrics-collection.md +1 -1
- package/skills/session-end/session-metrics-write.md +4 -10
- package/skills/session-plan/SKILL.md +2 -2
- package/skills/session-plan/wave-template.md +1 -1
- package/skills/session-start/SKILL.md +13 -1
- package/skills/spinout/SKILL.md +5 -1
- package/skills/sunset-review/SKILL.md +11 -1
- package/skills/tmux-layout/SKILL.md +7 -2
- package/skills/vault-mirror/SKILL.md +10 -1
- package/skills/vault-sync/SKILL.md +10 -1
- package/skills/vault-sync/validator.mjs +55 -6
- package/skills/wave-executor/wave-loop.md +42 -12
- package/scripts/lib/mission-status-schema.mjs +0 -114
|
@@ -19,19 +19,13 @@
|
|
|
19
19
|
|
|
20
20
|
1. Ensure `.orchestrator/metrics/` directory exists: `mkdir -p .orchestrator/metrics`
|
|
21
21
|
|
|
22
|
-
1-pre. **`memory_cleanup_at`
|
|
22
|
+
1-pre. **`memory_cleanup_at` is DERIVED, not remembered (#699 + 2026-08-17 follow-up)** — there is **no coordinator step here any more**. Do not set a `ranMemoryCleanupThisSession` boolean and do not call `stampMemoryCleanup()` by hand at session-end.
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
import { stampMemoryCleanup } from '../../scripts/lib/memory-cleanup-stamp.mjs';
|
|
24
|
+
`scripts/emit-session.mjs` derives the field itself: it calls `deriveMemoryCleanupSignal()` (`scripts/lib/memory-cleanup-stamp.mjs`), which reads the sibling `events.jsonl` for `orchestrator.memory.cleanup_completed` records whose `timestamp` falls inside this session's own `[started_at, completed_at]` window (and whose `semantic_session_id`, when present, matches). The emitting side is the LAST step of every `/memory-cleanup` run — see `skills/memory-cleanup/SKILL.md` § "Session-End Signal".
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
metricsEntry = stampMemoryCleanup(metricsEntry, {
|
|
29
|
-
ranCleanup: ranMemoryCleanupThisSession, // boolean
|
|
30
|
-
completedAt: metricsEntry.completed_at,
|
|
31
|
-
});
|
|
32
|
-
```
|
|
26
|
+
**Contract:** a no-op run (MEMORY.md already healthy, no files mutated) is still a cleanup, so it still emits and therefore still stamps. When `/memory-cleanup` did not run, no event exists, nothing is derived, and the field is simply absent — never `null`. An EXPLICIT `memory_cleanup_at` already present on the record WINS over derivation and is never overwritten; that path is for backfills and tests, not for normal operation.
|
|
33
27
|
|
|
34
|
-
**
|
|
28
|
+
**Why this stopped being a coordinator instruction.** It was one until 2026-08-17, and it measurably failed: a `/memory-cleanup` ran on 2026-08-14 with a documented yield, the prose step above was not executed, and all three session records of that day carried `memory_cleanup_at: null` — so the session-start banner reported "last cleanup 29 days ago" against the operator's own "3 days". `stampMemoryCleanup()` had **zero production callers** at that point; every reference to it was an instruction asking an LLM to remember. Same failure class as the STATE.md write-race Epic #583 replaced with a lock: Disziplin statt Mechanik.
|
|
35
29
|
|
|
36
30
|
> **#701.2 DOC NOTE — `completed_at >= started_at` guard:** This invariant is enforced mechanically by `scripts/emit-session.mjs`. The writer applies `clampTimestampsMonotonic()` (from `scripts/lib/session-schema/timestamps.mjs`) before `validateSession()`, clamping any inversion of `completed_at < started_at` to `started_at` and recording forensics in `_clamped: true` / `_original_completed_at`. Previously-inverted entries (e.g. `main-2026-06-21-session-4`) are already corrected. **No per-session coordinator action is needed** — the writer enforces the invariant at write time. Do not add defensive clamping logic here; the canonical guard lives in `emit-session.mjs`.
|
|
37
31
|
|
|
@@ -275,7 +275,7 @@ mission-status:
|
|
|
275
275
|
- `status`: always `brainstormed` at plan emission. Terminal values are updated at gate transitions by wave-executor: `brainstormed` → `validated` (user confirms via `/go`) → `in-dev` (agent dispatched) → `testing` (Quality wave) → `completed` (Quality gate green). session-end Phase 1.9 reads the current value to classify the item.
|
|
276
276
|
|
|
277
277
|
**Transition gates (summary):**
|
|
278
|
-
At plan time, all items start at `brainstormed`. When the user runs `/go` to approve the plan, wave-executor updates each item to `validated`. When an agent for a wave-plan item is dispatched, wave-executor updates that item to `in-dev`. When the Quality wave begins, items from prior waves move to `testing`. When the Quality gate passes, items finalize at `completed`. Rollback to `brainstormed` is permitted from any state.
|
|
278
|
+
At plan time, all items start at `brainstormed`. When the user runs `/go` to approve the plan, wave-executor updates each item to `validated`. When an agent for a wave-plan item is dispatched, wave-executor updates that item to `in-dev`. When the Quality wave begins, items from prior waves move to `testing`. When the Quality gate passes, items finalize at `completed`. Rollback to `brainstormed` is permitted from any state. This ordering is **coordinator convention, not a mechanical gate** — nothing validates a transition before it is written (see "Default and transitions" below).
|
|
279
279
|
|
|
280
280
|
**Omission rule:** When the plan has 0 wave-plan items (e.g., pure express-path coord-direct with no sub-agent tasks), do NOT emit the `### Wave-Plan Mission Status (machine-readable)` block.
|
|
281
281
|
|
|
@@ -298,7 +298,7 @@ Every wave-plan item carries a `status` field drawn from a 5-value enum. The fie
|
|
|
298
298
|
- **Default at plan creation:** `brainstormed` — all items start here.
|
|
299
299
|
- **Transitions are coordinator-level orchestration** (not inside individual agent prompts). See `skills/wave-executor/SKILL.md` "Mission-Status Updates (#340)" for when each transition fires.
|
|
300
300
|
- **Rollback:** any item may return to `brainstormed` from any state (e.g. if work is discarded or re-planned).
|
|
301
|
-
- **
|
|
301
|
+
- **No mechanical validation — by design.** The `status` values come from the 5-value enum in the table above, but nothing checks a transition before it is written. `setMissionStatus` (`scripts/lib/state-md/mission-status.mjs`) mirrors whatever string it is handed onto BOTH the body section and the frontmatter array, deliberately without an enum gate: gating it would reintroduce the exact body-says-X/frontmatter-says-Y divergence that sync exists to remove. An out-of-enum value therefore lands visibly on both surfaces instead of being silently rejected on one. Keeping the enum honest is the coordinator's job.
|
|
302
302
|
|
|
303
303
|
#### Status field in wave-plan items
|
|
304
304
|
|
|
@@ -18,7 +18,7 @@ For each wave, define agents with:
|
|
|
18
18
|
|
|
19
19
|
- `Isolation: worktree` means the wave-executor will pass `isolation: "worktree"` to the Agent tool, giving each agent its own git worktree copy
|
|
20
20
|
- `MaxTurns` is enforced via the agent prompt — wave-executor includes a turn limit instruction in each agent's prompt
|
|
21
|
-
- `status` is the mission-status enum value for this wave-plan item (#340). Always `brainstormed` in the initial plan. Wave-executor updates it at gate transitions (validated → in-dev → testing → completed).
|
|
21
|
+
- `status` is the mission-status enum value for this wave-plan item (#340). Always `brainstormed` in the initial plan. Wave-executor updates it at gate transitions (validated → in-dev → testing → completed). Rollback to `brainstormed` is allowed from any state. The five values are listed in `SKILL.md` § Mission-Status Enum; nothing validates them mechanically — `setMissionStatus` writes the string it is given to both STATE.md surfaces on purpose, so keeping the value in-enum is the coordinator's job.
|
|
22
22
|
|
|
23
23
|
> **Deconfliction rule:** Before finalizing agent specs for a wave, verify that no two agents in the same wave list overlapping `Files:` paths. If overlap is found, either merge the agents into one or move one task to a later wave. Two agents editing the same file in parallel causes merge conflicts that require manual resolution.
|
|
24
24
|
|
|
@@ -785,7 +785,19 @@ Group issues by:
|
|
|
785
785
|
|
|
786
786
|
Non-blocking. Cross-reference: `scripts/lib/ci-status-banner.mjs` (the sibling project-facing probe) and `.claude/rules/test-value.md` § TV-005 (why structural gates beat unit-test volume).
|
|
787
787
|
|
|
788
|
-
|
|
788
|
+
Additionally, invoke the mirror-issues probe (`scripts/lib/mirror-issues-banner.mjs`) via `await checkMirrorIssues({ repoRoot })`. This is the only probe that deliberately queries the platform the session did NOT auto-detect. `skills/gitlab-ops/SKILL.md` § VCS Auto-Detection selects exactly one platform via if/else, so in a repo whose `origin` is GitLab and whose `github` remote is a public mirror, no code path ever reads the mirror's issue tracker — issues filed there by external reporters are structurally invisible to every session. The VCS family is therefore hard-pinned to `'github'` inside the module rather than auto-detected. It takes no Session Config key: `resolveRepoSpec({ repoRoot, vcs: 'github' })` derives the `gh -R` spec from `git remote`, which makes the probe self-disabling — a repo with no GitHub mirror resolves to `undefined`, returns `null`, and spawns no subprocess.
|
|
789
|
+
|
|
790
|
+
The return contract has THREE states, not the usual two, and the third is the point: `null` means either "no mirror remote" or "queried successfully, zero open issues"; `{ severity, message, count, repoSpec, issues }` means N > 0; and `{ severity, message, repoSpec, degraded }` means the query did NOT succeed, where `degraded` is one of `cli-missing | timeout | parse-error | auth-error | query-failed`. Render `result.message` verbatim in either non-null case. A `degraded` result must be read as *"the mirror's state is unknown"* — never as clean. `scripts/lib/ci-status-banner.mjs` collapses all three of missing-CLI, unparseable output and absent-remote onto `null`, which in the banner contract reads as "all clear"; that collapse is why this gap survived unseen. Do not reproduce it.
|
|
791
|
+
|
|
792
|
+
Additionally, invoke the git-config-drift probe (`scripts/lib/git-config-drift.mjs`) via `checkGitConfigDrift({ repoRoot })` (synchronous — no await; `env` defaults to `process.env`). It reads `git config --local --list` with a FILTERED environment, so an ambient `GIT_DIR` cannot redirect the probe itself at a foreign repository and let it call this one clean. **Three states, not two:** `null` = read and clean; `{ severity: 'warn', message, findings }` = at least one unexpected entry (a local identity override, a local `commit.gpgsign`, a remote on a reserved fixture host, a `core.hooksPath` not pointing at `.husky/_`, or `GIT_DIR`/`GIT_WORK_TREE` set in the environment); `{ …, degraded }` = the config could NOT be read — **never render that as clean.** Render `result.message` alongside the other banners.
|
|
793
|
+
|
|
794
|
+
This is the only probe that inspects `.git/config`, and that is the whole point: `git status` cannot see that file. On 2026-08-19 a coordinator diagnostic exported `GIT_DIR` at this repository while the suite ran; test fixtures wrote a foreign remote and their own `user.email`/`user.name` into the local config, and the identity then authored two commits that reached both remotes. A recovery pass checked HEAD, the index and all 1614 tracked files, found everything clean, and missed it — because none of those surfaces show `.git/config`. It surfaced two hours later, from an agent measuring something else.
|
|
795
|
+
|
|
796
|
+
The complementary halves live elsewhere and are not duplicates of this probe: `tests/setup/scrub-git-env.mjs` (wired via `setupFiles` in `vitest.config.mjs`) removes the redirecting variables before any test runs, and `scripts/lib/validate/check-test-git-config-target.mjs` censuses untargeted state-mutating git calls in `tests/**`. The census is WARN-only by measurement — its first cut was 11 hits, all false positives — and it explicitly reports `gitDirInheritable`, the population it cannot close, because the incident's own call sites passed a correct `cwd` and were redirected anyway.
|
|
797
|
+
|
|
798
|
+
Non-blocking. Cross-reference: `scripts/lib/vcs-repo-spec.mjs` (`isQueryFailure` — the same absence-vs-query-failure split this probe's `degraded` state implements).
|
|
799
|
+
|
|
800
|
+
All banners are non-blocking — display in the Session Overview, do not halt the session. If `bootstrap-lock-freshness.mjs` is absent (pre-#186 plugin install) or `peer-cards/staleness-banner.mjs` is absent (pre-#503 plugin install) or `loop-readiness-banner.mjs` is absent (pre-#633 plugin install) or `instruction-budget-guard.mjs` is absent (pre-#687 plugin install) or `reconcile-nudge-banner.mjs` is absent (pre-#723 plugin install) or `sessions-staleness-banner.mjs` is absent (pre-#724 plugin install) or `sessions-integrity-banner.mjs` is absent (pre-#958 plugin install) or `owner-config-banner.mjs` is absent (pre-#820 plugin install) or `moc-staleness-banner.mjs` / `context-coverage-banner.mjs` are absent (pre-#831 plugin install) or `claude-md-budget-lint.mjs` is absent (pre-#878 plugin install) or `mirror-issues-banner.mjs` is absent (pre-#1022 plugin install), skip silently.
|
|
789
801
|
|
|
790
802
|
## Phase 4.5: Resource Health (v3.1.0)
|
|
791
803
|
|
package/skills/spinout/SKILL.md
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
name: spinout
|
|
3
3
|
user-invocable: true
|
|
4
4
|
model: sonnet
|
|
5
|
-
description:
|
|
5
|
+
description: >
|
|
6
|
+
Use when extracting a project into its own repo — a venture spinout (e.g. a product leaving its
|
|
7
|
+
incubator repo) or a sanitized content-snapshot fork. Guided 5-step runbook: target sphere + path,
|
|
8
|
+
confidentiality/sanitize check, copy + fresh git init, SNAPSHOT-FREEZE marker in the source repo,
|
|
9
|
+
remotes + registration. Trigger on 'spin out X', 'extract this into its own repo', 'fork X sanitized'.
|
|
6
10
|
---
|
|
7
11
|
|
|
8
12
|
# spinout — Guided Project-Extraction Runbook
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sunset-review
|
|
3
|
-
description:
|
|
3
|
+
description: >
|
|
4
|
+
Use this skill when the user wants to identify unused, near-zero-use, or stale skills/agents/commands in
|
|
5
|
+
the plugin surface so they can be demoted or retired. Combines agent-dispatch telemetry (start-events
|
|
6
|
+
only) with static reference scanning, classifies every surface item into Active / Investigate / Demote /
|
|
7
|
+
Retire, and emits a Markdown report plus JSON sidecar. NEVER auto-deletes — surfaces candidates for
|
|
8
|
+
human decision. Quarterly cadence. <example>Context: The plugin surface has grown and the maintainer
|
|
9
|
+
wants to prune dead weight. user: "/sunset-review" assistant: "Running the sunset walk — classifying
|
|
10
|
+
skills, agents, and commands by usage telemetry + static refs, grouped by Retire / Demote / Investigate
|
|
11
|
+
/ Active. No item is deleted automatically; I'll surface Retire/Demote candidates for your decision."
|
|
12
|
+
<commentary>The user wants a usage-driven prune candidate list; this skill runs the read-only walker,
|
|
13
|
+
presents grouped verdicts, and writes a sidecar — it never deletes.</commentary></example>
|
|
4
14
|
model: inherit
|
|
5
15
|
color: amber
|
|
6
16
|
---
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tmux-layout
|
|
3
|
-
description:
|
|
3
|
+
description: >
|
|
4
|
+
Use this skill when the operator wants a prepared tmux visualization layout for the session's
|
|
5
|
+
side-channels (STATE.md tail, CI-watch, events.jsonl tail). Renders a 4-pane default layout or debug
|
|
6
|
+
layout. Read-only side-channel observability — the coordinator chat stays in the operator's original
|
|
7
|
+
terminal. Trigger phrases: "tmux layout", "split panes for ci watch", "visualize session side-channels",
|
|
8
|
+
"show me state-md tail and ci".
|
|
4
9
|
model: inherit
|
|
5
10
|
color: cyan
|
|
6
11
|
tools: Read, Bash, Grep, Glob
|
|
@@ -47,7 +52,7 @@ The skill prints a one-line tmux command. Paste it into a SECOND terminal (do no
|
|
|
47
52
|
|---|---|---|
|
|
48
53
|
| 1 | **Shell** (operator scratch — NOT claude) | `bash` (interactive) |
|
|
49
54
|
| 2 | STATE.md tail | `tail -F <state-dir>/STATE.md` |
|
|
50
|
-
| 3 | CI watch (poll-loop wrapper) | `while true; do clear; glab ci status
|
|
55
|
+
| 3 | CI watch (poll-loop wrapper) | `while true; do clear; glab ci status -R <spec> --output json \| jq -r '.jobs[] \| ...'; sleep 15; done` |
|
|
51
56
|
| 4 | events.jsonl wave/gate filter | `tail -F .orchestrator/metrics/events.jsonl \| jq --unbuffered 'select(.event \| test("wave\|gate\|spiral"))'` |
|
|
52
57
|
| 5 | agent-status telemetry (#565, only with `--with-status-pane`) | `while true; do clear; jq . .orchestrator/runtime/agent-status-current.json 2>/dev/null \|\| echo ...; sleep 2; done` |
|
|
53
58
|
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: vault-mirror
|
|
3
|
-
description:
|
|
3
|
+
description: >
|
|
4
|
+
Use when you need to populate the Meta-Vault with machine-generated notes derived from
|
|
5
|
+
session-orchestrator JSONL records. Converts entries from `.orchestrator/metrics/sessions.jsonl` and
|
|
6
|
+
`.orchestrator/metrics/learnings.jsonl` into vault-conformant Markdown under `50-sessions/` and
|
|
7
|
+
`40-learnings/`. Called automatically at session-end Phase 3.7 and after evolve Phase 3.5 — only when
|
|
8
|
+
`vault-integration.enabled=true` and `vault-integration.mode != "off"`. Idempotent: re-runs safely;
|
|
9
|
+
skips hand-authored notes. Triggers: "mirror to vault", "sync session notes to vault", "write learning
|
|
10
|
+
notes to vault", "vault-mirror failed at session close". <example>Context: session-end is finalizing,
|
|
11
|
+
vault-integration.mode is "warn". user: "/close" assistant: "Running vault-mirror to write
|
|
12
|
+
50-sessions/session-2026-05-17.md from the closing session record — 1 created, 0 skipped."</example>
|
|
4
13
|
model: haiku
|
|
5
14
|
---
|
|
6
15
|
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: vault-sync
|
|
3
|
-
description:
|
|
3
|
+
description: >
|
|
4
|
+
Use when you need to validate the Meta-Vault's Markdown frontmatter and wiki-link integrity before
|
|
5
|
+
closing a session or after vault edits. Runs as a hard gate at session-end Phase 1 — blocks close if any
|
|
6
|
+
`.md` file fails the Zod frontmatter schema or has dangling `[[wiki-links]]`. Supports three modes:
|
|
7
|
+
`hard` (blocks on errors), `warn` (reports without blocking), `off` (skip). Reads `vault-sync.*` from
|
|
8
|
+
Session Config; respects per-vault exclude globs from `CLAUDE.md`. Triggers: "vault validation failed at
|
|
9
|
+
session close", "fix vault frontmatter errors", "check vault wiki-links", "why is session-end blocked by
|
|
10
|
+
vault-sync". <example>Context: session-end Phase 1 quality gate, vault-sync.enabled=true,
|
|
11
|
+
vault-sync.mode="hard". user: "/close" assistant: "vault-sync found 2 frontmatter errors in
|
|
12
|
+
vault/40-learnings/ml-notes.md — missing required `id` field. Fixing before close."</example>
|
|
4
13
|
model: haiku
|
|
5
14
|
---
|
|
6
15
|
|
|
@@ -254,6 +254,14 @@ for (let i = 0; i < args.length; i++) {
|
|
|
254
254
|
// ? — any single character except `/`
|
|
255
255
|
// literal path separators and characters otherwise
|
|
256
256
|
// Operates on POSIX-style forward-slash relative paths.
|
|
257
|
+
// `**` compiles to a SEGMENT-ANCHORED alternative, not a free `(?:.*?)` (#1013).
|
|
258
|
+
// The free form ended anywhere, including mid-segment, so every `**/` pattern
|
|
259
|
+
// silently grew a suffix-match: `**/README.md` matched `MYREADME.md`, and
|
|
260
|
+
// `**/archive/**` matched `90-archive/...`. This matcher answers "is this path
|
|
261
|
+
// EXCLUDED from validation", so an over-approximation is not a harmless
|
|
262
|
+
// widening — it is a false negative: the file is never checked and the
|
|
263
|
+
// validator still reports 0 errors. Anchoring narrows exclusion, i.e. it can
|
|
264
|
+
// only ever add files to the checked set.
|
|
257
265
|
function globToRegExp(glob) {
|
|
258
266
|
// Normalise input
|
|
259
267
|
const g = glob.replace(/\\/g, '/');
|
|
@@ -262,12 +270,18 @@ function globToRegExp(glob) {
|
|
|
262
270
|
const c = g[i];
|
|
263
271
|
if (c === '*') {
|
|
264
272
|
if (g[i + 1] === '*') {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
273
|
+
if (g[i + 2] === '/') {
|
|
274
|
+
// `**/` — zero or more WHOLE path segments. Zero repetitions is what
|
|
275
|
+
// makes `**/foo` match `foo` at the vault root, and it is why the
|
|
276
|
+
// trailing `/` is swallowed here rather than emitted literally.
|
|
277
|
+
re += '(?:[^/]+/)*';
|
|
278
|
+
i += 2;
|
|
279
|
+
} else {
|
|
280
|
+
// Trailing or standalone `**` (e.g. `a/**`) — everything below the
|
|
281
|
+
// preceding literal, segment boundaries included.
|
|
282
|
+
re += '.*';
|
|
283
|
+
i += 1;
|
|
284
|
+
}
|
|
271
285
|
} else {
|
|
272
286
|
re += '[^/]*';
|
|
273
287
|
}
|
|
@@ -658,6 +672,21 @@ const errors = [];
|
|
|
658
672
|
const warnings = [];
|
|
659
673
|
let filesChecked = 0;
|
|
660
674
|
let filesSkippedNoFrontmatter = 0;
|
|
675
|
+
// The paths behind the count (#1013). A bare count made "0 errors" ambiguous:
|
|
676
|
+
// it meant "everything CHECKED is valid", never "everything is valid", and the
|
|
677
|
+
// operator had no way to see WHICH files were never checked. The list is
|
|
678
|
+
// emitted only by the branches that actually ran the validation pass — so an
|
|
679
|
+
// ABSENT `files_skipped_no_frontmatter_paths` means "never inspected" (mode=off,
|
|
680
|
+
// no vault) while an EMPTY one means "inspected, nothing skipped". Same
|
|
681
|
+
// absence-preserving contract as `scripts/lib/reconcile-nudge-banner.mjs`
|
|
682
|
+
// (`skippedCandidates`), which distinguishes never-checked from checked-clean.
|
|
683
|
+
const filesSkippedNoFrontmatterPaths = [];
|
|
684
|
+
// Ceiling: 50 paths keeps the envelope bounded on a vault that has never been
|
|
685
|
+
// backfilled (real measurement 2026-08-15: 2 of 8574 files in the live vault).
|
|
686
|
+
// `paths.length < files_skipped_no_frontmatter` is itself the truncation
|
|
687
|
+
// signal, so no extra flag is needed. Revisit if a consuming vault routinely
|
|
688
|
+
// reports more than 50 — then the right fix is a backfill, not a bigger cap.
|
|
689
|
+
const SKIPPED_PATHS_CAP = 50;
|
|
661
690
|
let excludedCount = 0;
|
|
662
691
|
let archivedSkippedCount = 0;
|
|
663
692
|
|
|
@@ -686,6 +715,9 @@ for (const rec of records) {
|
|
|
686
715
|
|
|
687
716
|
if (!fm.hasFrontmatter) {
|
|
688
717
|
filesSkippedNoFrontmatter++;
|
|
718
|
+
if (filesSkippedNoFrontmatterPaths.length < SKIPPED_PATHS_CAP) {
|
|
719
|
+
filesSkippedNoFrontmatterPaths.push(rel);
|
|
720
|
+
}
|
|
689
721
|
continue;
|
|
690
722
|
}
|
|
691
723
|
|
|
@@ -737,6 +769,19 @@ for (const rec of records) {
|
|
|
737
769
|
|
|
738
770
|
const hasErrors = errors.length > 0;
|
|
739
771
|
|
|
772
|
+
// Silence is not success: a skipped file contributes to neither `errors` nor
|
|
773
|
+
// `files_checked`, so a bare "0 errors" line would let an unvalidated file pass
|
|
774
|
+
// unremarked at the session-end hard gate (#1013). Surface the count on stderr
|
|
775
|
+
// too — stdout stays pure JSON per the caller contract.
|
|
776
|
+
if (filesSkippedNoFrontmatter > 0) {
|
|
777
|
+
const shown = filesSkippedNoFrontmatterPaths.slice(0, 5).join(', ');
|
|
778
|
+
const more = filesSkippedNoFrontmatter - Math.min(5, filesSkippedNoFrontmatterPaths.length);
|
|
779
|
+
process.stderr.write(
|
|
780
|
+
`WARN: ${filesSkippedNoFrontmatter} file(s) NOT validated — no frontmatter: ` +
|
|
781
|
+
`${shown}${more > 0 ? ` (+${more} more)` : ''}\n`,
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
|
|
740
785
|
// ── mode=baseline ─────────────────────────────────────────────────────────
|
|
741
786
|
// Serialize current errors + warnings as a snapshot, then exit 0.
|
|
742
787
|
if (mode === 'baseline') {
|
|
@@ -774,6 +819,7 @@ if (mode === 'diff') {
|
|
|
774
819
|
excluded_count: excludedCount,
|
|
775
820
|
archived_skipped_count: archivedSkippedCount,
|
|
776
821
|
files_skipped_no_frontmatter: filesSkippedNoFrontmatter,
|
|
822
|
+
files_skipped_no_frontmatter_paths: filesSkippedNoFrontmatterPaths,
|
|
777
823
|
errors,
|
|
778
824
|
warnings,
|
|
779
825
|
});
|
|
@@ -794,6 +840,7 @@ if (mode === 'diff') {
|
|
|
794
840
|
excluded_count: excludedCount,
|
|
795
841
|
archived_skipped_count: archivedSkippedCount,
|
|
796
842
|
files_skipped_no_frontmatter: filesSkippedNoFrontmatter,
|
|
843
|
+
files_skipped_no_frontmatter_paths: filesSkippedNoFrontmatterPaths,
|
|
797
844
|
errors,
|
|
798
845
|
warnings,
|
|
799
846
|
});
|
|
@@ -816,6 +863,7 @@ if (mode === 'diff') {
|
|
|
816
863
|
excluded_count: excludedCount,
|
|
817
864
|
archived_skipped_count: archivedSkippedCount,
|
|
818
865
|
files_skipped_no_frontmatter: filesSkippedNoFrontmatter,
|
|
866
|
+
files_skipped_no_frontmatter_paths: filesSkippedNoFrontmatterPaths,
|
|
819
867
|
});
|
|
820
868
|
|
|
821
869
|
process.stderr.write(
|
|
@@ -839,6 +887,7 @@ emit({
|
|
|
839
887
|
excluded_count: excludedCount,
|
|
840
888
|
archived_skipped_count: archivedSkippedCount,
|
|
841
889
|
files_skipped_no_frontmatter: filesSkippedNoFrontmatter,
|
|
890
|
+
files_skipped_no_frontmatter_paths: filesSkippedNoFrontmatterPaths,
|
|
842
891
|
errors,
|
|
843
892
|
warnings,
|
|
844
893
|
});
|
|
@@ -433,7 +433,7 @@ This replaces the older prose slot "Relevant patterns from `<state-dir>/rules/`"
|
|
|
433
433
|
|
|
434
434
|
**Gate:** runs when `.orchestrator/metrics/learnings.jsonl` exists. When it does not — or when nothing clears the confidence floor, or the corpus is unreadable — the CLI prints nothing and exits 0. Same best-effort convention as every injector above (Grounding `:307`, Frontmatter-Guard `:386`, Path-Cousin-Guard `:208`): silent no-op on any failure, **never blocks dispatch**. Any non-zero exit means "inject nothing, continue".
|
|
435
435
|
|
|
436
|
-
**Zero new coordinator obligations.** The per-agent file scope this needs is the SAME `$AGENT_FILESCOPE_JSON`
|
|
436
|
+
**Zero new coordinator obligations.** The per-agent file scope this needs is the SAME `$AGENT_FILESCOPE_JSON` — `<state-dir>/filescopes/wave-<N>/<agent-id>.json` — that `## Scope Manifest` § 3.1 already requires you to write for every agent, and that the Scope-Union Assertion (#796) then consumes. Reuse that file — do not write a second one, and never a temp copy.
|
|
437
437
|
|
|
438
438
|
**Invocation:** once per agent, immediately after that agent's `$AGENT_FILESCOPE_JSON` is written, capture stdout as `$LEARNINGS_INDEX`:
|
|
439
439
|
|
|
@@ -455,6 +455,21 @@ When it is empty (no corpus, no qualifying entries, or any CLI failure), dispatc
|
|
|
455
455
|
|
|
456
456
|
**Instrumentation (why this one is measurable and its neighbours are not).** The rule injection above is a SHOULD and emits no signal either way, so "did the coordinator actually inject?" has been unanswerable after the fact — a gap the #1014 discovery wave had to leave open. This CLI emits `orchestrator.learnings.index.injected` to `.orchestrator/metrics/events.jsonl` (via `scripts/emit-event.mjs`, the canonical `emitEvent()` path — the same route `scripts/compute-grounding-injection.sh` uses for `orchestrator.grounding.injected`), carrying `count`, `scope_matched`, `global_count`, `candidates`, `truncated`, `bytes`, and `scope_source`. The before/after measurement is therefore a fact in the event log, not a matter of prose compliance. Emission is best-effort and suppressible with `--no-event`; a failed emit never blocks dispatch.
|
|
457
457
|
|
|
458
|
+
#### Pre-Dispatch: File-Scope Injection (#1020)
|
|
459
|
+
|
|
460
|
+
> **Read this first — this block is PER AGENT, unlike `#### Pre-Dispatch: Glob-Scoped Rule Injection (#336/#694)` above, which states "Per-wave scoping (not per-agent): the rule set is computed ONCE per wave".** Model it on **Pre-Dispatch Grounding Injection (#85)** — same cadence, same per-agent source. This injector legitimately has BOTH cadences (per-agent for the brief, per-wave for the § Scope Manifest union), which is exactly what makes the collapse tempting: reuse ONE agent's block for the whole batch and every agent reads the territory of every OTHER agent as its own. Deconfliction would then be **lifted rather than enforced**, and the double assignment § 3.2 exists to catch becomes invisible in the one channel where an agent could still notice it.
|
|
461
|
+
|
|
462
|
+
**Invocation:** for each agent, read `<state-dir>/filescopes/wave-<N>/<agent-id>.json` (= `$AGENT_FILESCOPE_JSON`) — the SAME file written in § Scope Manifest 3.1, not a re-derivation from the session plan and not a temp copy — and prepend its entries to that agent's prompt, one path per line:
|
|
463
|
+
|
|
464
|
+
FILE-SCOPE — exactly these:
|
|
465
|
+
```
|
|
466
|
+
<one path or glob per line, verbatim from that agent's scope file>
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
Marker line plus fenced block, in that order: `hooks/pre-task-scope-disjoint.mjs` extracts the scope from the prompt by finding the marker and taking the FIRST fenced block after it, so this shape is what makes an agent's declared territory machine-readable at dispatch time. An unparseable or absent block resolves to ALLOW there, so a malformed injection degrades to today's behaviour rather than blocking dispatch. When the scope file is missing or empty (Discovery waves), inject nothing and dispatch unchanged.
|
|
470
|
+
|
|
471
|
+
> **Registration note.** That hook was armed in `hooks/hooks.json` on 2026-08-14, after a green Full Gate. Its `PreToolUse` matcher is **`Agent`** — measured over 12 archived transcripts of this repo, `Agent` accounts for 147 of 147 dispatch `tool_use` blocks. A `Task` matcher would hit the unrelated todo family (`TaskCreate`/`TaskUpdate`/`TaskGet`/…) and never once fire on a dispatch: armed and inert, the failure mode that reads as done. It is deliberately absent from `hooks-codex.json` / `hooks-cursor.json` / `hooks-pi.json` — those platforms have no `Agent` dispatch tool, so the asymmetry is registered in `DOCUMENTED_ASYMMETRIES` rather than papered over with a matcher that can never fire.
|
|
472
|
+
|
|
458
473
|
#### Structured Reasoning (STATE:/PLAN:) — opt-in via `reasoning-output: true` (#79)
|
|
459
474
|
|
|
460
475
|
When `$CONFIG.reasoning-output` is `true`, append the following block to every agent prompt. The pattern is adapted from the BitGN PAC Agent's Soft-SGR: short structured transparency lines before tool invocations, without forcing structured output. Leave the block OUT when the flag is `false` (default) — this preserves exact legacy prompt behavior.
|
|
@@ -1099,19 +1114,34 @@ Before each wave dispatch:
|
|
|
1099
1114
|
```
|
|
1100
1115
|
The `gates` field (optional) mirrors `enforcement-gates` from Session Config (#77). When present, hooks check each gate individually via `gate_enabled()`. Missing gate entries default to enabled, preserving default behavior.
|
|
1101
1116
|
2. Validate by piping through `node "$PLUGIN_ROOT/scripts/validate-wave-scope.mjs"` (where `$PLUGIN_ROOT` is `$CLAUDE_PLUGIN_ROOT`, `$CODEX_PLUGIN_ROOT`, or `$CURSOR_RULES_DIR` per platform — see `skills/_shared/config-reading.md`). If validation fails (exit 1), fix the JSON based on stderr errors and retry.
|
|
1102
|
-
3.
|
|
1103
|
-
To compute `allowedPaths`: read each agent's specification from the session plan. Each agent lists its "Files:" scope (e.g., `skills/session-end/SKILL.md`, `scripts/*.sh`). Collect all file paths and glob patterns from all agents in this wave into a single flat array. Deduplicate entries. If an agent's scope uses globs (e.g., `scripts/*.sh`), include the glob pattern as-is — the enforcement hook resolves globs at check time.
|
|
1117
|
+
3. **`allowedPaths` is COMPUTED from per-agent scope files — never hand-transcribed (#1020).** Transcribing the union by hand produced 5 scope divergences in ONE session. Three steps, in this order; none of them is a judgement call. Globs stay verbatim (`scripts/*.sh`) — the enforcement hook resolves them at check time.
|
|
1104
1118
|
|
|
1105
|
-
**
|
|
1119
|
+
**3.1 — one file per agent.** Write each agent's "Files:" scope from the session plan, verbatim, as a JSON array of strings to `<state-dir>/filescopes/wave-<N>/<agent-id>.json`. That path IS `$AGENT_FILESCOPE_JSON` — the same file `--assert-subset` (#796 below), Grounding Injection (#85), the Learnings-Index (#1014) and the File-Scope Injection (#1020) already consume. Do not write a second copy anywhere, and **never to a `$TMPDIR` temp path**: the injector and `hooks/pre-task-scope-disjoint.mjs` need an addressable, wave-keyed location that a temp file cannot be. Reading `$AGENT_FILESCOPE_JSON` as "some temp file" is the one failure that costs no error — the injector finds nothing, no `FILE-SCOPE` block reaches the prompt, `extractScopeFromPrompt` returns `[]`, and the dispatch is ALLOWed exactly as it was before #1020, signal-free. The coordinator's OWN planned direct edits go into `<state-dir>/filescopes/wave-<N>/coordinator.json` in the identical form and take part in both steps below: 2 of those 5 divergences were coordinator-direct edits, for which no agent scope file exists by construction, and the commit guard caught them only at the commit boundary.
|
|
1106
1120
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1121
|
+
> **`<state-dir>/filescopes/` is control state, like `wave-scope.json` itself — never a wave territory.** Step 3.1 necessarily runs BEFORE the union of 3.3 exists, so writing these files reports `bash-write-verify: N file(s) changed by a Bash call OUTSIDE the wave's allowedPaths` naming `filescopes/wave-<N>/*.json`. Expected once per wave rollover at this step; it is information, not a scope violation. Never widen `allowedPaths` to silence it — that would grant agents write access to the deconfliction record itself.
|
|
1122
|
+
|
|
1123
|
+
**3.2 — assert disjointness BEFORE computing the union.** Build the sidecar — an ARRAY of `{id, files}` records (never an object map: a duplicated agent id must stay visible), one record per file written in 3.1, `coordinator.json` included — and run:
|
|
1109
1124
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1125
|
+
```bash
|
|
1126
|
+
node "$PLUGIN_ROOT/scripts/validate-wave-scope.mjs" \
|
|
1127
|
+
--assert-disjoint "$WAVE_SCOPES_SIDECAR" < <state-dir>/wave-scope.json
|
|
1113
1128
|
```
|
|
1114
1129
|
|
|
1130
|
+
Exit 1 (one stderr message per collision) means two agents were handed the same file: fix the session plan, rewrite the affected 3.1 files, re-assert. Never widen the union to make it pass. This runs **before** 3.3 because a union computed over colliding scopes launders the defect into the very artefact meant to prevent it — `allowedPaths` then grants the file and every later gate sees a legal write.
|
|
1131
|
+
|
|
1132
|
+
**3.3 — compute the union.** `--union` is a QUERY MODE that still requires a schema-valid manifest on stdin, so write the skeleton first with `"allowedPaths": []`, then:
|
|
1133
|
+
|
|
1134
|
+
```bash
|
|
1135
|
+
node "$PLUGIN_ROOT/scripts/validate-wave-scope.mjs" \
|
|
1136
|
+
--union "$WAVE_SCOPES_SIDECAR" < <state-dir>/wave-scope.json
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
It prints the computed `allowedPaths` array as JSON on stdout **instead of** the manifest echo — one JSON document per run, the flag decides which. Insert that array as `allowedPaths`, then write the final `wave-scope.json`. It already applies the Test-Sibling Expansion below (`expandTestSiblings(unionFileScopes(scopes), { role })`, role read from the manifest), so do not also run the helper by hand.
|
|
1140
|
+
|
|
1141
|
+
**The `--assert-subset` assertion (#796, below) stays unchanged and keeps running.** It checks a DIFFERENT property — each agent's scope ⊆ the union — and a double assignment is structurally invisible to it: a file claimed twice is a subset twice over. `--assert-disjoint` is an addition, never a replacement.
|
|
1142
|
+
|
|
1143
|
+
**Test-Sibling Expansion (#970):** an `allowedPaths` entry that names a production file but NOT its test sibling makes the wave's own regression test unwritable — the scope guard then mechanically enforces exactly the inconsistency the quality gate exists to catch. Cross-repo evidence, three occurrences in ONE session: a migrations glob without the SQL-test directory (the regression test could not be written); a lone `.actions.ts` file (the wave's cross-tenant security test stayed red); a dead-export deletion whose importing test lay outside every scope (the suite ended red). Do NOT hand-derive the sibling paths — step 3.3's `--union` runs `expandTestSiblings(…, { role })` for you, so the hook, the validator and this prose state one rule.
|
|
1144
|
+
|
|
1115
1145
|
The helper is pure (same input → same output, no filesystem writes) and is also surfaced by `scripts/validate-wave-scope.mjs`. **The role decides, inside the helper** — `scripts/lib/scope-gate.mjs` `TEST_SIBLING_EXPANSION_ROLES` is THE list (currently `Impl-Core`, `Impl-Polish` — exactly where the incident occurred), and #5/#6 below describe that gate rather than restating it. Pass the role string; do not pre-filter by role in prose, and do not hand-roll the equivalent `{ enabled: … }`. Matching is trimmed + case-insensitive, so `impl-core` behaves as `Impl-Core`.
|
|
1116
1146
|
|
|
1117
1147
|
> **Fail-closed:** an ABSENT or unrecognised `role` does **not** expand. Omitting it fails loudly (an agent's write to its own test is blocked, recoverable by one re-union); the opposite default would silently hand a Quality phase-1 simplification agent write access to the suite. `{ enabled: false }` is the unconditional opt-out and `{ enabled: true }` the explicit opt-in — both override the role.
|
|
@@ -1123,11 +1153,11 @@ Before each wave dispatch:
|
|
|
1123
1153
|
**The sibling rule is repo-configurable, not a hardcoded layout.** THIS repo has zero `__tests__/` directories and no co-located tests; consumer-repo shapes (`<file>.test.*` beside the source, `<dir>/__tests__/**`, `supabase/migrations/** → supabase/tests/**`) are configured per repo and do not apply here.
|
|
1124
1154
|
|
|
1125
1155
|
Three ordering constraints, all load-bearing:
|
|
1126
|
-
-
|
|
1156
|
+
- The deconfliction check (3.2) runs on the DECLARED per-agent scopes, **before** the union expands anything. Named ceiling: two agents whose production files share a basename receive the same emitted sibling glob, which a declared-scope check cannot see — revisit if a wave is ever scoped by basename family instead of by directory.
|
|
1127
1157
|
- Expand **before** `wave-scope.json` is written, in ONE pass. `hooks/post-bash-write-verify.mjs` fingerprints `allowedPaths` via `scopeSignature()` and fires a control notice on change, so a later mutation reads as tampering.
|
|
1128
1158
|
- Skip **absolute** entries entirely — expanding a Gate-5b out-of-repo grant would sprout a synthetic `tests/**` sibling outside the repo.
|
|
1129
1159
|
|
|
1130
|
-
**Pre-Dispatch Scope-Union Assertion (#796):** `wave-scope.json` is GLOBAL per wave — `hooks/enforce-scope.mjs` Gate 7 checks EVERY agent against the same `allowedPaths` union, so a union that (re)written for only ONE agent silently denies its siblings' legitimate writes. Before each `Agent()` batch, mechanically assert — for EVERY agent in the batch — that its fileScope ⊆ `wave-scope.allowedPaths`.
|
|
1160
|
+
**Pre-Dispatch Scope-Union Assertion (#796):** `wave-scope.json` is GLOBAL per wave — `hooks/enforce-scope.mjs` Gate 7 checks EVERY agent against the same `allowedPaths` union, so a union that (re)written for only ONE agent silently denies its siblings' legitimate writes. Before each `Agent()` batch, mechanically assert — for EVERY agent in the batch — that its fileScope ⊆ `wave-scope.allowedPaths`. `$AGENT_FILESCOPE_JSON` is that agent's § 3.1 file — `<state-dir>/filescopes/wave-<N>/<agent-id>.json`, already written above and shared with every other consumer. Do not re-write it to a temp path here (§ 3.1 says why that degrades silently); just run:
|
|
1131
1161
|
|
|
1132
1162
|
```bash
|
|
1133
1163
|
node "$PLUGIN_ROOT/scripts/validate-wave-scope.mjs" \
|
|
@@ -1158,4 +1188,4 @@ Before each wave dispatch:
|
|
|
1158
1188
|
5. Delete `<state-dir>/wave-scope.json`
|
|
1159
1189
|
6. Write Phase 2 wave-scope.json with test file allowedPaths (`**/*.test.*`, `**/*.spec.*`, `**/__tests__/**`)
|
|
1160
1190
|
7. Dispatch test/review agents
|
|
1161
|
-
7. After the final wave completes, delete `<state-dir>/wave-scope.json` (cleanup)
|
|
1191
|
+
7. After the final wave completes, delete `<state-dir>/wave-scope.json` (cleanup). Delete `<state-dir>/filescopes/` in the same step — the per-agent scope files (§ 3.1) are wave-local working state, and a stale `wave-<N>/` directory left behind is a scope claim nobody re-verified.
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mission-Status Enum + Transition Validator (issue #340).
|
|
3
|
-
*
|
|
4
|
-
* Pure ESM, no I/O, no external dependencies. All functions are deterministic.
|
|
5
|
-
*
|
|
6
|
-
* Five-state lifecycle for wave-plan items:
|
|
7
|
-
*
|
|
8
|
-
* brainstormed → validated → in-dev → testing → completed
|
|
9
|
-
*
|
|
10
|
-
* Rollbacks to `brainstormed` are permitted from any state (user can reset scope).
|
|
11
|
-
* Idempotent self-transitions are always allowed.
|
|
12
|
-
* All other transitions are rejected.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Canonical ordered list of valid mission-status values.
|
|
17
|
-
*
|
|
18
|
-
* @type {readonly string[]}
|
|
19
|
-
*/
|
|
20
|
-
export const MISSION_STATUS_VALUES = Object.freeze([
|
|
21
|
-
'brainstormed',
|
|
22
|
-
'validated',
|
|
23
|
-
'in-dev',
|
|
24
|
-
'testing',
|
|
25
|
-
'completed',
|
|
26
|
-
]);
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Allowed forward transitions (excluding self-transitions and rollback to
|
|
30
|
-
* 'brainstormed', which are handled separately in isValidMissionStatusTransition).
|
|
31
|
-
*
|
|
32
|
-
* @type {ReadonlyMap<string, string>}
|
|
33
|
-
*/
|
|
34
|
-
const FORWARD_TRANSITIONS = new Map([
|
|
35
|
-
['brainstormed', 'validated'],
|
|
36
|
-
['validated', 'in-dev'],
|
|
37
|
-
['in-dev', 'testing'],
|
|
38
|
-
['testing', 'completed'],
|
|
39
|
-
]);
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Returns true when `value` is one of the five canonical status strings.
|
|
43
|
-
*
|
|
44
|
-
* @param {unknown} value
|
|
45
|
-
* @returns {boolean}
|
|
46
|
-
*/
|
|
47
|
-
export function isValidMissionStatus(value) {
|
|
48
|
-
return typeof value === 'string' && MISSION_STATUS_VALUES.includes(value);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Returns true when the transition from `from` → `to` is permitted.
|
|
53
|
-
*
|
|
54
|
-
* Allowed transitions:
|
|
55
|
-
* - Forward: brainstormed→validated, validated→in-dev, in-dev→testing, testing→completed
|
|
56
|
-
* - Idempotent: any state → itself
|
|
57
|
-
* - Rollback: any state → brainstormed
|
|
58
|
-
*
|
|
59
|
-
* All other transitions return false.
|
|
60
|
-
*
|
|
61
|
-
* @param {string} from - current status value
|
|
62
|
-
* @param {string} to - target status value
|
|
63
|
-
* @returns {boolean}
|
|
64
|
-
*/
|
|
65
|
-
export function isValidMissionStatusTransition(from, to) {
|
|
66
|
-
if (!isValidMissionStatus(from) || !isValidMissionStatus(to)) return false;
|
|
67
|
-
// Idempotent self-transition
|
|
68
|
-
if (from === to) return true;
|
|
69
|
-
// Rollback to brainstormed from any state
|
|
70
|
-
if (to === 'brainstormed') return true;
|
|
71
|
-
// Forward transition
|
|
72
|
-
return FORWARD_TRANSITIONS.get(from) === to;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Validates a mission-status entry object.
|
|
77
|
-
*
|
|
78
|
-
* Expected shape:
|
|
79
|
-
* { id: string, task: string, wave: number, status: <MISSION_STATUS_VALUES> }
|
|
80
|
-
*
|
|
81
|
-
* @param {unknown} obj
|
|
82
|
-
* @returns {{ ok: boolean, errors: string[] }}
|
|
83
|
-
*/
|
|
84
|
-
export function validateMissionStatusEntry(obj) {
|
|
85
|
-
const errors = [];
|
|
86
|
-
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
87
|
-
return { ok: false, errors: ['entry must be a non-null, non-array object'] };
|
|
88
|
-
}
|
|
89
|
-
const entry = /** @type {Record<string, unknown>} */ (obj);
|
|
90
|
-
|
|
91
|
-
// id — non-empty string
|
|
92
|
-
if (typeof entry.id !== 'string' || entry.id.trim() === '') {
|
|
93
|
-
errors.push('id must be a non-empty string');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// task — non-empty string
|
|
97
|
-
if (typeof entry.task !== 'string' || entry.task.trim() === '') {
|
|
98
|
-
errors.push('task must be a non-empty string');
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// wave — integer >= 1
|
|
102
|
-
if (!Number.isInteger(entry.wave) || /** @type {number} */ (entry.wave) < 1) {
|
|
103
|
-
errors.push('wave must be a positive integer');
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// status — one of MISSION_STATUS_VALUES
|
|
107
|
-
if (!isValidMissionStatus(entry.status)) {
|
|
108
|
-
errors.push(
|
|
109
|
-
`status must be one of [${MISSION_STATUS_VALUES.join(', ')}], got: ${JSON.stringify(entry.status)}`,
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return { ok: errors.length === 0, errors };
|
|
114
|
-
}
|