session-orchestrator 4.1.0 → 4.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/session-plan/SKILL.md +1 -1
- 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/session-plan/SKILL.md +1 -1
- package/.cursor/skills/session-plan/SKILL.md +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +34 -0
- package/README.md +8 -8
- package/agents/ux-evaluator.md +1 -1
- package/commands/close.md +3 -3
- package/commands/go.md +2 -0
- package/commands/memory-cleanup.md +4 -3
- package/commands/persona-panel.md +1 -1
- package/commands/session.md +3 -2
- package/docs/README.md +4 -4
- package/docs/USER-GUIDE.md +115 -48
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +55 -1
- package/docs/ci-setup.md +1 -1
- package/docs/codex-setup.md +1 -0
- package/docs/components.md +2 -2
- package/docs/cursor-setup.md +1 -0
- package/docs/events-schema.md +4 -1
- package/docs/instruction-delivery.md +1 -1
- package/docs/memory-proposal-flow.md +3 -3
- package/docs/migration-v4.md +2 -2
- package/docs/owner-config-schema.md +74 -90
- package/docs/persona-panel.md +4 -4
- package/docs/pi-setup.md +1 -0
- package/docs/rule-authoring.md +13 -6
- package/docs/scope-collision-guard.md +2 -0
- package/docs/session-config-reference.md +55 -22
- package/docs/session-config-template.md +9 -5
- package/docs/vault-docs-architecture.md +4 -2
- package/hooks/_lib/hook-import-set.json +28 -3
- package/hooks/_lib/vcs-create-matcher.mjs +214 -16
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/pre-bash-issue-budget.mjs +123 -26
- package/hooks/subagent-telemetry.mjs +106 -20
- package/package.json +4 -4
- package/scripts/baseline-archetypes.mjs +28 -0
- package/scripts/lib/auto-dialectic.mjs +0 -68
- package/scripts/lib/baseline-archetypes.mjs +439 -0
- package/scripts/lib/build-live-signals.mjs +5 -6
- package/scripts/lib/config/issue-budget.mjs +68 -8
- package/scripts/lib/config/private-config-dir.mjs +3 -2
- package/scripts/lib/config/remote-hosts.mjs +2 -2
- package/scripts/lib/config-schema.mjs +79 -0
- package/scripts/lib/file-lock.mjs +47 -5
- package/scripts/lib/issue-budget-reconcile.mjs +392 -0
- package/scripts/lib/issue-budget.mjs +76 -3
- package/scripts/lib/learnings/evolve-telemetry.mjs +1 -2
- package/scripts/lib/maintenance-due-banner.mjs +440 -0
- package/scripts/lib/owner-config.example.yaml +29 -46
- package/scripts/lib/owner-yaml.mjs +14 -13
- package/scripts/lib/quality-gate.mjs +13 -6
- package/scripts/lib/resource-probe/evaluate.mjs +19 -21
- package/scripts/lib/rules-sync.mjs +34 -4
- package/scripts/lib/session-close-backfill.mjs +182 -40
- package/scripts/lib/session-end/phase-skip.mjs +85 -86
- package/scripts/lib/session-end/tail-runner.mjs +178 -0
- package/scripts/lib/session-schema/constants.mjs +6 -0
- package/scripts/lib/session-schema/validator.mjs +20 -0
- package/scripts/lib/session-shape.mjs +558 -0
- package/scripts/lib/session-start-probes.mjs +10 -3
- package/scripts/lib/session-token-rollup.mjs +95 -10
- package/scripts/lib/state-md/frontmatter-mutators.mjs +22 -34
- package/scripts/lib/state-md.mjs +1 -0
- package/scripts/lib/subagents-schema.mjs +77 -9
- package/scripts/lib/telemetry/pricing.mjs +197 -0
- package/scripts/lib/telemetry/sync.mjs +50 -1
- package/scripts/lib/validate/check-skill-script-paths.mjs +33 -10
- package/scripts/lib/validate/check-unwired-features.mjs +8 -7
- package/scripts/lib/vault-mirror/process.mjs +2 -1
- package/scripts/lib/vault-status/narrative-mirror.mjs +4 -4
- package/scripts/lib/wave-resource-gate.mjs +23 -27
- package/scripts/lib/wave-sizing.mjs +10 -3
- package/scripts/materialize-wave-scope.mjs +68 -14
- package/scripts/print-applicable-rules.mjs +7 -6
- package/scripts/print-learnings-index.mjs +3 -2
- package/scripts/session-shape.mjs +266 -0
- package/skills/_shared/config-reading.md +15 -9
- package/skills/_shared/private-capability-context.md +89 -0
- package/skills/bootstrap/SKILL.md +61 -13
- package/skills/bootstrap/_shared-template.md +99 -14
- package/skills/bootstrap/deep-template.md +36 -26
- package/skills/bootstrap/fast-template.md +44 -8
- package/skills/bootstrap/intensity-heuristic.md +10 -4
- package/skills/bootstrap/private-contract.md +119 -0
- package/skills/bootstrap/public-fallback.md +30 -18
- package/skills/bootstrap/standard-template.md +39 -24
- package/skills/discovery/probes-ui.md +1 -1
- package/skills/docs-orchestrator/audience-mapping.md +1 -1
- package/skills/evolve/SKILL.md +2 -2
- package/skills/gitlab-ops/SKILL.md +3 -3
- package/skills/grill/SKILL.md +1 -1
- package/skills/memory-cleanup/SKILL.md +2 -2
- package/skills/plan/mode-new.md +9 -0
- package/skills/reconcile/SKILL.md +1 -1
- package/skills/session-end/SKILL.md +3 -2
- package/skills/session-end/phase-3-2-docs-verification.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +23 -65
- package/skills/session-end/phase-3-7a-recommendations.md +2 -2
- package/skills/session-end/references/phase-3-documentation-updates.md +8 -6
- package/skills/session-end/references/phase-5-issue-cleanup.md +26 -0
- package/skills/session-end/session-metrics-write.md +31 -12
- package/skills/session-plan/SKILL.md +56 -48
- package/skills/session-plan/wave-template.md +8 -15
- package/skills/session-start/SKILL.md +18 -2
- package/skills/session-start/phase-2-5-docs-planning.md +1 -1
- package/skills/session-start/phase-8-5-express-path.md +12 -9
- package/skills/session-start/references/phase-1-5-session-continuity.md +2 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +21 -5
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +3 -1
- package/skills/test-runner/rubric-v1.md +2 -2
- package/skills/wave-executor/SKILL.md +42 -12
- package/skills/wave-executor/circuit-breaker.md +3 -1
- package/skills/wave-executor/references/wave-loop-dispatch.md +4 -2
- package/skills/wave-executor/references/wave-loop-review.md +1 -1
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +6 -2
- package/templates/nextjs-minimal/package.json +1 -1
- package/templates/node-minimal/package.json +1 -1
- package/scripts/lib/multi-provider-build/providers.mjs +0 -64
- package/scripts/lib/multi-provider-build/templating.mjs +0 -130
- package/scripts/lib/owner-config/coerce.mjs +0 -29
- package/scripts/lib/owner-config/constants.mjs +0 -21
- package/scripts/lib/owner-config/defaults.mjs +0 -50
- package/scripts/lib/owner-config/error.mjs +0 -19
- package/scripts/lib/owner-config/index.mjs +0 -13
- package/scripts/lib/owner-config/merge.mjs +0 -52
- package/scripts/lib/owner-config/validate.mjs +0 -259
- package/scripts/lib/owner-config-loader.mjs +0 -170
- package/scripts/lib/owner-config.mjs +0 -28
- package/scripts/lib/soul-resolve.mjs +0 -130
- package/scripts/lib/vault-mirror/render.mjs +0 -8
|
@@ -69,11 +69,27 @@
|
|
|
69
69
|
|
|
70
70
|
Additionally, invoke the instruction-budget probe (`scripts/lib/instruction-budget-guard.mjs`) via `checkInstructionBudget({ repoRoot })`. The helper returns `null` (silent no-op) when the always-on directive count is at or under the configured ceiling, or on any read failure. When a non-null result is returned (`{ severity: 'warn', message }`), render `result.message` alongside the other banners. Non-blocking. Cross-reference: "Instruction Budget Audit" (#687; archived in the private Meta-Vault).
|
|
71
71
|
|
|
72
|
-
Additionally, invoke the
|
|
73
|
-
- **Nudge fires**: `"⚠ reconcile-nudge: <N> active learnings, <E> rule-eligible, last reconcile run: <never|YYYY-MM-DD> — run /reconcile to convert learnings into rules."` plus, when `reconcile.enabled: false`, an additional line: `"(reconcile.enabled: false — banner is advisory only; /reconcile still runs on-demand.)"`
|
|
74
|
-
- **No nudge**: silent (no banner).
|
|
72
|
+
Additionally, invoke the maintenance-due probe (`scripts/lib/maintenance-due-banner.mjs`) via `await checkMaintenanceDue({ repoRoot, config: $CONFIG })`. This probe REPLACED the former `reconcile-nudge` entry in `scripts/lib/session-start-probes.mjs` (it reuses `computeReconcileNudge` wholesale as one of its six signals) AND the two retired session-end nudges — Phase 3.6.5 auto-dream and Phase 3.6.7 auto-dialectic — which asked the operator at close for work only a housekeeping session can do. Return contract: `null` (silent no-op) or `{ severity: 'warn', message }`; never throws. `computeMaintenanceDue` exposes the same computation without the banner suppression.
|
|
75
73
|
|
|
76
|
-
|
|
74
|
+
The six signals (`MAINTENANCE_TOTAL_SIGNALS = 6`), each side-effect-free:
|
|
75
|
+
|
|
76
|
+
| id | due when | source |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `evolve` | no `orchestrator.evolve.completed` on record AND ≥ `MAINTENANCE_MIN_LEARNINGS` (20) active learnings | `events.jsonl` + `computeReconcileNudge` |
|
|
79
|
+
| `sweep` | the dry-run expiry sweep would archive ≥ 1 entry | `scripts/lib/learnings/expiry-sweep.mjs` |
|
|
80
|
+
| `reconcile` | `computeReconcileNudge().nudge === true` | `scripts/lib/reconcile-nudge-banner.mjs` (reused whole) |
|
|
81
|
+
| `dialectic` | `shouldDispatchAutoDialectic().trigger === true` | `scripts/lib/auto-dialectic.mjs` — the side-effect-free decision function; a variant that advanced the last-run stamp would consume the signal it reports (the former recording wrapper was removed in #1288) |
|
|
82
|
+
| `memory-cleanup` | `shouldDispatchAutoDream().trigger === true` | `scripts/lib/auto-dream.mjs` |
|
|
83
|
+
| `pending-sidecar` | an unapplied pending dream/dialectic proposal younger than `SIDECAR_MAX_AGE_DAYS` (14) | `.orchestrator/*-pending*.md` |
|
|
84
|
+
|
|
85
|
+
Introduces NO new Session Config key — every threshold is an existing key or an existing module constant.
|
|
86
|
+
|
|
87
|
+
- **Nothing due and everything readable**: silent (no banner).
|
|
88
|
+
- **A housekeeping session completed within `HOUSEKEEPING_COOLDOWN_DAYS` (7) days**: silent, even when signals are due — the operator already did the thing the banner would ask for. This cooldown is what keeps the instrument rare (HR-101, `.claude/rules/host-resources.md`); the housekeeping session's own record IS the last-maintenance stamp.
|
|
89
|
+
- **Otherwise**: render `result.message`, shaped as `⚠ maintenance due: <N> of 6 (<id>: <detail> · <id>: <detail>) — run /session housekeeping.` Every due signal carries the number or date that made it due (HR-106) — no aggregate stands alone.
|
|
90
|
+
- **Unreadable inputs** append an ` · undeterminable: <id>, <id>` suffix before the `— run /session housekeeping.` tail. A signal whose inputs cannot be read is three-state: never folded into "clean" (the `never` vs `undeterminable` discipline inherited from `reconcile-nudge-banner.mjs`).
|
|
91
|
+
|
|
92
|
+
Non-blocking. Cross-reference: `scripts/lib/reconcile-nudge-banner.mjs` (`computeReconcileNudge`), `scripts/lib/reconcile/engine.mjs` (`runReconcile`), `skills/reconcile/SKILL.md`, `skills/session-start/SKILL.md` Phase 7 (the housekeeping maintenance loop the banner routes to), and issues #723 / #1200.
|
|
77
93
|
|
|
78
94
|
Additionally, invoke the sessions-staleness probe (`scripts/lib/sessions-staleness-banner.mjs`) via `checkSessionsStaleness({ repoRoot })` (synchronous — no await). This detects the "close-through" gap: sessions that end without ever writing a `.orchestrator/metrics/sessions.jsonl` ledger record. It returns `null` (silent no-op) when `.orchestrator/metrics/sessions.jsonl` or `.orchestrator/metrics/events.jsonl` are absent or all-malformed, when no foreign (pre-session) event exists, or when the gap between the last ledger entry and the newest foreign event is at or under the warn threshold. When a non-null result is returned (`{ severity, message }`), render `result.message` alongside the other banners:
|
|
79
95
|
- **warn** (gap > 8h): `"⚠ sessions-staleness: last sessions.jsonl entry <ISO> is <N>h behind pre-session events.jsonl activity <ISO> — possible close-through gap (sessions ended without a ledger record; run node scripts/backfill-abandoned-sessions.mjs --dry-run)."`
|
|
@@ -156,5 +172,5 @@
|
|
|
156
172
|
|
|
157
173
|
Non-blocking. Cross-reference: `scripts/lib/vcs-repo-spec.mjs` (`isQueryFailure` — the same absence-vs-query-failure split this probe's `degraded` state implements).
|
|
158
174
|
|
|
159
|
-
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`
|
|
175
|
+
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 `maintenance-due-banner.mjs` / `reconcile-nudge-banner.mjs` are 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.
|
|
160
176
|
|
|
@@ -99,5 +99,7 @@ Setting `telemetry:\n enabled: true` in the host-local `~/.config/session-orche
|
|
|
99
99
|
|
|
100
100
|
The decision persists host-locally in `~/.config/session-orchestrator/telemetry.json`; once `consent` is non-`null` (granted or denied), `resolveConsent().prompt` stays `false` and this phase never fires again on that host — no repeat prompting across repos or sessions.
|
|
101
101
|
|
|
102
|
-
Cross-reference: GitLab #845 (Epic #841); `docs/
|
|
102
|
+
Cross-reference: GitLab #845 (Epic #841); `docs/telemetry.md`; consent API in `scripts/lib/telemetry/consent.mjs` (`resolveConsent`, `grantConsent`, `denyConsent`, `isHeadless`, `readTelemetryState`).
|
|
103
|
+
|
|
104
|
+
Historical design: archived PRD `docs/prd/2026-07-20-anonymous-usage-telemetry.md` §3 FA1/FA5. <!-- path-check: historical -->
|
|
103
105
|
|
|
@@ -316,14 +316,14 @@ indicates a translucent/blurred background that uses:
|
|
|
316
316
|
- `.glassEffect()` — correct Liquid Glass API
|
|
317
317
|
- `.glassEffect(.regular)` / `.glassEffect(.prominent)` — parametrised Liquid Glass
|
|
318
318
|
- Solid color backgrounds (no blur) — these are not Liquid Glass candidates
|
|
319
|
-
- Explicitly documented exceptions in `docs/apple-hig-exceptions.md`
|
|
319
|
+
- Explicitly documented exceptions in `docs/apple-hig-exceptions.md` <!-- path-check: example -->
|
|
320
320
|
|
|
321
321
|
**Severity mapping:**
|
|
322
322
|
|
|
323
323
|
| Condition | Severity |
|
|
324
324
|
|---|---|
|
|
325
325
|
| Non-compliant surface, no documented exception | MEDIUM |
|
|
326
|
-
| Non-compliant surface, documented exception in `docs/apple-hig-exceptions.md` | LOW |
|
|
326
|
+
| Non-compliant surface, documented exception in `docs/apple-hig-exceptions.md` | LOW <!-- path-check: example --> |
|
|
327
327
|
|
|
328
328
|
**Evidence required:**
|
|
329
329
|
- Peekaboo screenshot of the non-compliant surface (referenced in `evidence_path`).
|
|
@@ -87,6 +87,8 @@ Before starting the first wave (Discovery role):
|
|
|
87
87
|
- `persistence` (default: true), `enforcement` (default: warn), `isolation` (default: auto)
|
|
88
88
|
- `agents-per-wave` (default: 6), `max-turns` (default: auto), `pencil` (default: null)
|
|
89
89
|
|
|
90
|
+
**Neither `agents-per-wave` nor `max-turns` carries its own default here.** The per-wave `agentCap` and `maxTurns` come from the RESOLVED SHAPE (`node scripts/session-shape.mjs --repo-root "$PWD" --session-type <session-type> [--profile <session-profile>] [--known-scope true|false]`, module `scripts/lib/session-shape.mjs`) — that is the one place a session mode becomes an execution shape. Session Config's `agents-per-wave` (with its per-type override, e.g. `6 (deep: 18)`) CLAMPS the shape's `agentCap`; `max-turns: auto` is expanded per type inside the shape, not in this file.
|
|
91
|
+
|
|
90
92
|
**Execution Config shortcut:** If the session-plan output contains an `### Execution Config` section, its execution-level fields (waves, agents-per-wave, isolation, enforcement, max-turns) take precedence over `$CONFIG`. Session-level fields (persistence, pencil) always come from `$CONFIG`. If the Execution Config section is missing, use `$CONFIG` alone.
|
|
91
93
|
6. **Initialize session metrics** (if `persistence` enabled): Prepare a metrics tracking object for this session:
|
|
92
94
|
- `session_id`: `<branch>-<YYYY-MM-DD>-<HHmm>` (HHmm from `started_at` — ensures uniqueness across multiple sessions per day)
|
|
@@ -152,6 +154,19 @@ Wave 0 — Initializing
|
|
|
152
154
|
|
|
153
155
|
Create the `<state-dir>` directory if needed (`mkdir -p <state-dir>`) before writing. This file is the persistent state record — other skills and resumed sessions read it.
|
|
154
156
|
|
|
157
|
+
**Then VALIDATE `total-waves` against the resolved shape — do not skip this.** A plan whose wave count the shape does not produce must never be dispatched silently:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
node scripts/session-shape.mjs --repo-root "$PWD" \
|
|
161
|
+
--session-type <session-type> [--profile <session-profile>] [--known-scope true|false] \
|
|
162
|
+
--no-event | jq .totalWaves
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`--no-event` is used HERE because the plan-time run already recorded `orchestrator.session.shape_resolved` — this is a re-read, not a second resolution. Compare the printed number with the plan's wave count (the value just written to `total-waves`):
|
|
166
|
+
|
|
167
|
+
- **Equal** → continue to Wave 1.
|
|
168
|
+
- **Mismatch** → STOP. Surface it via `AskUserQuestion` per `.claude/rules/ask-via-tool.md`, with the shape's number and the plan's number both in the option descriptions: **re-plan to the shape (Recommended)** — rebuild the wave plan at the shape's wave count, the only outcome that keeps STATE.md, the ledger and the dispatch loop describing the same session — versus **proceed with a logged Deviation**, which requires appending the divergence to STATE.md `## Deviations` (`appendDeviationOnDisk()` from `scripts/lib/state-md.mjs`) before the first dispatch.
|
|
169
|
+
|
|
155
170
|
#### Pre-Wave 1b Extension: Docs Tasks Persistence (A3 / #230)
|
|
156
171
|
|
|
157
172
|
After writing the base STATE.md frontmatter above, conditionally persist the docs tasks block emitted by session-plan:
|
|
@@ -227,7 +242,7 @@ Claude Code's `Agent` tool with `isolation: "worktree"` changes `process.cwd()`
|
|
|
227
242
|
|
|
228
243
|
**Rules for the coordinator (this is YOU during wave execution):**
|
|
229
244
|
|
|
230
|
-
1. **After every Agent() dispatch** (before reading its output), call `restoreCoordinatorCwd()` from `scripts/lib/
|
|
245
|
+
1. **After every Agent() dispatch** (before reading its output), call `restoreCoordinatorCwd()` from `scripts/lib/workspace.mjs`. `wave-loop.md § 2` makes this explicit.
|
|
231
246
|
2. **Prefer absolute file paths** for Read/Edit/Write tool calls. A drifted CWD turns relative paths into silent cross-tree writes.
|
|
232
247
|
3. **Before any Bash git command**, either `cd` inside a subshell (`cd /path && cmd`) or rely on `git -C /path <cmd>`. Do not assume CWD.
|
|
233
248
|
4. **Verify at checkpoints** — when in doubt, run `git rev-parse --show-toplevel` to confirm which tree is currently active.
|
|
@@ -302,29 +317,44 @@ Cross-reference: PRD F2.1 / issue #501 / `docs/memory-proposal-flow.md` (coordin
|
|
|
302
317
|
|
|
303
318
|
## Session Type Behavior
|
|
304
319
|
|
|
305
|
-
### Housekeeping Sessions
|
|
320
|
+
### Housekeeping Sessions — the Maintenance Loop
|
|
321
|
+
|
|
322
|
+
A housekeeping session is **ONE coordinator-direct wave**, not a shrunken multi-wave run: `node scripts/session-shape.mjs --repo-root "$PWD" --session-type housekeeping --no-event` resolves to `totalWaves: 1` with that wave's `coordinatorDirect: true` and `writes: true`. "Coordinator-direct" means **no wave-executor dispatch loop** — it does not mean zero subagents (`/evolve dialectic` dispatches the read-only `dialectic-deriver`).
|
|
323
|
+
|
|
324
|
+
**Ordered default scope — the maintenance loop.** Run it in this order, before the session's selected issues:
|
|
325
|
+
|
|
326
|
+
| # | Run | Gate | Artefact that proves it ran |
|
|
327
|
+
|---|---|---|---|
|
|
328
|
+
| 1 | `claude-md-drift-check` | unconditional | checker JSON (`errors`/`warnings` counts) |
|
|
329
|
+
| 2 | expired-learnings sweep | unconditional | `orchestrator.learnings.sweep_applied` |
|
|
330
|
+
| 3 | `/evolve analyze` | AUQ-gated (the operator approves the proposed learnings) | `orchestrator.evolve.completed` |
|
|
331
|
+
| 4 | `/reconcile` | AUQ-gated (rule proposals are never applied unasked) | `orchestrator.reconcile.completed` with `dry_run: false` |
|
|
332
|
+
| 5 | `/evolve dialectic` | AUQ-gated (the derived thesis is presented, not committed) | `orchestrator.dialectic.completed` |
|
|
333
|
+
| 6 | `/memory-cleanup` | AUQ-gated (deletions are operator-approved) | `orchestrator.memory.cleanup_completed` |
|
|
334
|
+
|
|
335
|
+
The session-start probe `maintenance-due` (`scripts/lib/maintenance-due-banner.mjs`) says which of these are DUE for this repo; a run that is not due may be skipped, and the skip is reported. An AUQ-gated run the operator declines is reported as declined — never as done. **Absence of the artefact event is the only evidence that counts**: a run claimed in prose without its event is not a run (`.claude/rules/verification-before-completion.md`).
|
|
306
336
|
|
|
307
|
-
|
|
337
|
+
Then the mechanics:
|
|
308
338
|
|
|
309
339
|
1. Initialize STATE.md as normal (`session-type: housekeeping`, `total-waves: 1`)
|
|
310
|
-
2. Do NOT create `wave-scope.json` —
|
|
311
|
-
3.
|
|
340
|
+
2. Do NOT create `wave-scope.json` — there is no agent fan-out to constrain; the coordinator's own edits stay governed by its `coordinator.json` record
|
|
341
|
+
3. Execute the maintenance loop above, then the session's selected issues, serially as coordinator actions
|
|
312
342
|
4. Run Baseline quality checks after all tasks complete (not between tasks)
|
|
313
343
|
5. Skip session-reviewer dispatch — housekeeping changes are low-risk
|
|
314
344
|
6. Do NOT update STATE.md to `status: completed` — that write is reserved for session-end per state-ownership contract (`skills/_shared/state-ownership.md`). Leave `status: active`.
|
|
315
345
|
7. Proceed directly to session-end (`/close`)
|
|
316
346
|
|
|
317
|
-
|
|
347
|
+
Beyond the loop: git cleanup, SSOT refresh, CI fixes, branch merges, documentation.
|
|
318
348
|
End with a single commit summarizing all housekeeping work.
|
|
319
349
|
|
|
320
350
|
### Feature Sessions
|
|
321
|
-
-
|
|
322
|
-
-
|
|
351
|
+
- **3 waves** (Impl-Core → Impl-Polish+Quality → Finalization) with **no Discovery wave** — read them from the shape, not from this file: `node scripts/session-shape.mjs --repo-root "$PWD" --session-type feature --no-event`
|
|
352
|
+
- Per-wave agent caps come from the shape's `agentCap` (the shape caps a feature wave at 4), clamped by Session Config `agents-per-wave`
|
|
323
353
|
- Balance between implementation speed and quality
|
|
324
354
|
|
|
325
355
|
### Deep Sessions
|
|
326
|
-
-
|
|
327
|
-
-
|
|
356
|
+
- **5 waves** from the shape (`--session-type deep`); the **Discovery wave is conditional** — pass `--known-scope true` when the scope is already established and the shape drops Discovery, leaving 4 waves
|
|
357
|
+
- Per-wave agent caps come from the shape's `agentCap`, clamped by Session Config `agents-per-wave` with its per-type override (this repo: `agents-per-wave: 6 (deep: 18)`)
|
|
328
358
|
- Extra emphasis on Discovery role and Quality role
|
|
329
359
|
- May include security audits, performance profiling, architecture refactoring
|
|
330
360
|
|
|
@@ -332,9 +362,9 @@ End with a single commit summarizing all housekeeping work.
|
|
|
332
362
|
|
|
333
363
|
Not a fourth session type — a PROFILE over `session-type: deep`, resolved from the `/session ultradeep` argument alias (`commands/session.md`). Everything below applies only when STATE.md frontmatter carries `session-profile: ultradeep`; every other behaviour in this skill is unchanged, because downstream still reads `deep`. Full spec — wave table, mandatory artefacts, cost model: `docs/prd/2026-09-06-ultradeep-session-profile.md`.
|
|
334
364
|
|
|
335
|
-
- **
|
|
365
|
+
- **The wave count and the wave roles come from the shape**, not from this file: `node scripts/session-shape.mjs --repo-root "$PWD" --session-type deep --profile ultradeep --no-event` returns `totalWaves: 7` (Research+Code-Discovery → Synthesis-Gate → Impl-Core → Impl-Polish → Review-Panel → Quality → Release/Finalization) and reports `wavesConfigHonored: false` with the ignored Session Config `waves` value — the profile OWNS its wave count. Role narrative: `skills/session-plan/SKILL.md` § Role-to-Wave Mapping.
|
|
336
366
|
- **Wave 2 is coordinator-direct and dispatches ZERO agents.** Make NO `Agent()` call in this wave. The coordinator consolidates wave 1 into `docs/audits/<YYYY-MM-DD>-<slug>.md`, updates STATE.md, and asks ONE **blocking** `AskUserQuestion` (confirm scope / narrow / abort) per `.claude/rules/ask-via-tool.md`. Wave 3 does not start until that question is answered — this is the one gate the profile exists for, so a silent "no tasks, skip it" is a defect, not an optimisation (`skills/session-plan/SKILL.md` § Empty roles, coordinator-direct exception).
|
|
337
|
-
- **`max-turns` per ROLE,
|
|
367
|
+
- **`max-turns` is per ROLE, and the numbers live in the shape:** take each wave's value from `waves[].maxTurns` in the shape output above (the Research, implementing and Release/Finalization figures are produced there, not restated here). Set it on the dispatch; a wave whose `maxTurns` is `null` is coordinator-direct and dispatches nothing.
|
|
338
368
|
- **Web tools are role-bound.** Research agents in wave 1 receive `WebSearch` and `WebFetch`. **No write-capable agent may receive them** — not in wave 1's Code-Discovery half, and not in any later wave. The grant follows the READ-ONLY property, so the pairing "has Write/Edit" + "has WebSearch/WebFetch" must never occur in a single dispatch. Research findings carry URL + retrieval date, the web analogue of the PSA-006 evidence rule (`.claude/rules/parallel-sessions.md`).
|
|
339
369
|
- **Budgets are not implemented.** The PRD's `ultradeep.max-*` block (§ 7) is deferred until three runs have been measured (HR-105: no threshold without a firing rate). Nothing reads such a key today — do not invent one, and do not gate a wave on it.
|
|
340
370
|
|
|
@@ -4,7 +4,7 @@ Sub-reference for the wave-executor skill. Defines safety mechanisms for agent e
|
|
|
4
4
|
|
|
5
5
|
## Circuit Breaker
|
|
6
6
|
|
|
7
|
-
1. **MaxTurns enforcement**: Read `
|
|
7
|
+
1. **MaxTurns enforcement**: Read the wave's `maxTurns` from the resolved shape (`scripts/session-shape.mjs` / `scripts/lib/session-shape.mjs`; a Session Config `max-turns: auto` is expanded per session type THERE, not here). Include this instruction in EVERY agent prompt:
|
|
8
8
|
```
|
|
9
9
|
TURN LIMIT: You have a maximum of [N] turns. If you cannot complete within [N] turns, report PARTIAL with what you accomplished and what remains.
|
|
10
10
|
```
|
|
@@ -93,6 +93,8 @@ The function never throws — it always returns a result object. Treat `skipped:
|
|
|
93
93
|
| 3–4 | feature / deep | `worktree` |
|
|
94
94
|
| ≥ 5 | any | `worktree` |
|
|
95
95
|
|
|
96
|
+
`sessionType` for `resolveIsolation` comes from the resolved shape (the same `session-type` the shape was resolved for), not from a value re-derived at dispatch time.
|
|
97
|
+
|
|
96
98
|
Rationale: the verified learning `coordinator-over-worktree-on-shared-files` (confidence 0.75) shows that small waves on partitioned scopes merge cleaner when run in-place. Two consecutive deep-session regressions (2026-04-20 07:30, 09:00) were worktree base-ref staleness on ≤2-agent waves editing the same SKILL.md. Graduated default makes worktree the tool for parallelism, not the default tax on every wave.
|
|
97
99
|
|
|
98
100
|
2. **Enforcement auto-promote (#194)**: Call `resolveEnforcement({ isolation, configEnforcement })` from the same module. When isolation resolves to `none` and the user has not explicitly set `configEnforcement: 'off'`, enforcement auto-promotes from `warn` → `strict`. Worktrees provide filesystem-level isolation; in-place dispatch relies on the scope hook as the only barrier — it must be hard, not informational. Write the resolved value into `wave-scope.json` `enforcement`.
|
|
@@ -113,7 +113,7 @@ When `worker-pool.enabled: true` in Session Config, dispatch via `runWavePool()`
|
|
|
113
113
|
|
|
114
114
|
Use the **Agent tool** to dispatch this wave's agents. **`run_in_background: true` is the PLATFORM DEFAULT for wave dispatch, not a deviation of ours** — since Claude Code **2.1.232**, non-teammate agent spawns default to background in interactive sessions. Our own measurement 2026-08-22 (v2.1.239) is why that default is the right one for a wave: under blocking dispatch the coordinator was 143 s incapable of acting between an agent's mid-run escalation and its own next turn — escalation latency equals the batch's remaining runtime. Background dispatch returns turns to the coordinator between agent completions; a running agent received a queued message mid-run and answered ~9 min before its final report.
|
|
115
115
|
|
|
116
|
-
Dispatch in **SMALL BATCHES of 3–4 Agent() calls per message** (cumulative up to the wave's `agents-per-wave` cap). Large single-message fan-outs (>4 Agent() calls in one message) remain **FORBIDDEN** — fleet evidence (conf 1.0, 5 sessions) shows they drop Agent() calls SILENTLY (the coordinator receives fewer results than it dispatched, with no error), whereas serial / small-batch dispatch held 13/13 and 8/8. That evidence PREDATES backgrounding and is untouched by it; what backgrounding changes is the *cost* of batching, which is now near zero — a background batch returns its launch acks immediately, so batches no longer serialize the wave. Dispatch a batch, let its acks return, then dispatch the next, until every planned agent is started; then run **Started-Set Verification** below.
|
|
116
|
+
Dispatch in **SMALL BATCHES of 3–4 Agent() calls per message** (cumulative up to the wave's `agents-per-wave` cap). Large single-message fan-outs (>4 Agent() calls in one message) remain **FORBIDDEN** — fleet evidence (conf 1.0, 5 sessions) shows they drop Agent() calls SILENTLY (the coordinator receives fewer results than it dispatched, with no error), whereas serial / small-batch dispatch held 13/13 and 8/8. That evidence PREDATES backgrounding and is untouched by it; what backgrounding changes is the *cost* of batching, which is now near zero — a background batch returns its launch acks immediately, so batches no longer serialize the wave. Dispatch a batch, let its acks return, then dispatch the next, until every planned agent is started; then run **Started-Set Verification** below. The archived, private spec `docs/specs/2026-07-02-fleet-mining-followup-grill.md` (C4) records the batching-policy rationale. The `worker-pool.enabled: true` path (above) is the mechanised opt-in alternative to manual batching. <!-- path-check: historical -->
|
|
117
117
|
|
|
118
118
|
**What is NOT the reason for 3–4 (platform caps, as of Claude Code 2.1.232).** The batch size defends against the SILENT DROP measured above. It is not, and never was, a workaround for a platform concurrency limit — and the upstream numbers no longer permit reading it as one: **2.1.217** set the concurrent-subagent cap to **20** (`CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS`), **2.1.219** set subagent nesting depth to **3**, **2.1.224** REMOVED the former 200-spawn-per-session cap, and **2.1.232** made background the spawn default (above). So neither 3–4, nor `agents-per-wave: 6`, nor `deep: 18` sits anywhere near a platform ceiling; the only ceiling a wave can actually reach is the concurrency cap of 20, and only through the worker-pool `maxParallel` path. The rule stays at 3–4 because the drop evidence is about how many `Agent()` calls survive ONE assistant message — a question no version bump has addressed. **Revisit-Trigger:** a measurement showing a >4-call single message delivering 100% of its calls. A raised cap is not that measurement. Version provenance: `docs/audits/2026-09-06-360-audit/w1/d9-claude-code-fable.md` (FA-6, 2026-09-06).
|
|
119
119
|
|
|
@@ -123,6 +123,8 @@ After resolving `isolation`, compute the wave's enforcement via `resolveEnforcem
|
|
|
123
123
|
|
|
124
124
|
Before dispatching, verify the wave's agent count does not exceed `$CONFIG.agents-per-wave` — if it does, warn the user and request plan revision.
|
|
125
125
|
|
|
126
|
+
`sessionType` for `resolveIsolation` is the session type the shape was resolved for, and the wave's `coordinatorDirect` and `writes` flags are READ FROM the shape's wave entry (`scripts/session-shape.mjs` → `waves[]`) rather than inferred from the role name. The dispatch-side marker is unchanged: the coordinator still keys the table below on `coordinator-direct: true` in the wave-plan item — the shape says which waves are expected to carry it, the plan item is what the coordinator acts on.
|
|
127
|
+
|
|
126
128
|
**Coordinator-direct waves (`coordinator-direct: true`) dispatch NOTHING — and that is not a silent drop.** Keyed on the marker, never on a profile name (`skills/session-plan/SKILL.md` keys its matching empty-role exception the same way), so any future coordinator-direct wave inherits this:
|
|
127
129
|
|
|
128
130
|
| Step | Behaviour when the wave-plan item carries `coordinator-direct: true` |
|
|
@@ -480,7 +482,7 @@ After `wave-scope.json` is written for this wave and before assembling the `Agen
|
|
|
480
482
|
|
|
481
483
|
**Gate:** runs when `.claude/rules/` exists. When it does not, the CLI prints nothing and exits 0 — zero behaviour change. This step never blocks dispatch: any non-zero exit or empty output means "inject nothing, continue" (same best-effort framing as Pre-Dispatch Grounding Injection above).
|
|
482
484
|
|
|
483
|
-
**Per-wave scoping (not per-agent):** the rule set is computed ONCE per wave from the wave's `allowedPaths` union (the same `wave-scope.json` source used elsewhere), not per agent. The CLI resolves `scopePaths` from `allowedPaths`, `mode` from the `session-type:` frontmatter in
|
|
485
|
+
**Per-wave scoping (not per-agent):** the rule set is computed ONCE per wave from the wave's `allowedPaths` union (the same `wave-scope.json` source used elsewhere), not per agent. The CLI resolves `scopePaths` from `allowedPaths`, `mode` from the `session-type:` frontmatter in the active harness's `STATE.md`, and `hostClass` from `.orchestrator/host.json` — all overridable, all degrading to "no gating" when unreadable. Default state and scope paths resolve independently: state-directory override first, then active-harness and legacy fallback.
|
|
484
486
|
|
|
485
487
|
**Invocation:** once per wave, run from the repo root and capture stdout as `$RULES_BLOCK`:
|
|
486
488
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
**Step 2.0 — Restore coordinator CWD (#219):** BEFORE reading any agent output or running any quality check, restore the coordinator's working directory. Claude Code's `Agent` tool with `isolation: "worktree"` `chdir()`s into each worktree internally and does NOT restore it on agent return. Subsequent Edit/Write/Bash calls would silently route to whichever worktree's tree CWD last drifted into.
|
|
10
10
|
|
|
11
11
|
```js
|
|
12
|
-
import { restoreCoordinatorCwd } from '$PLUGIN_ROOT/scripts/lib/
|
|
12
|
+
import { restoreCoordinatorCwd } from '$PLUGIN_ROOT/scripts/lib/workspace.mjs';
|
|
13
13
|
|
|
14
14
|
const cwd = await restoreCoordinatorCwd();
|
|
15
15
|
if (cwd.restored) {
|
|
@@ -61,13 +61,13 @@ Before each wave dispatch:
|
|
|
61
61
|
2. Validate by piping through `node "$PLUGIN_ROOT/scripts/validate-wave-scope.mjs"` (`$PLUGIN_ROOT` — see "Shell variables used in this section" above). If validation fails (exit 1), fix the JSON based on stderr errors and retry.
|
|
62
62
|
3. **`allowedPaths` is COMPUTED from one canonical declaration array — never hand-transcribed (#1020/#1083).** Transcribing either declaration shape or the union by hand produced scope divergences. Globs stay verbatim (`scripts/*.sh`) — the enforcement hook resolves them at check time.
|
|
63
63
|
|
|
64
|
-
**3.1 — materialize both declaration shapes once.** Build one JSON array from the session plan, one `{id, files}` record for every agent plus exactly one `coordinator` record for the coordinator's planned direct edits. `files` arrays, their entries and their order are the plan's verbatim declarations.
|
|
64
|
+
**3.1 — materialize both declaration shapes once.** Build one JSON array from the session plan, one `{id, files}` record for every agent plus exactly one `coordinator` record for the coordinator's planned direct edits. `files` arrays, their entries and their order are the plan's verbatim declarations. Run from the **project root**, materialize it ONCE and capture the aggregate-sidecar path:
|
|
65
65
|
|
|
66
66
|
```bash
|
|
67
67
|
WAVE_SCOPE_RECORDS='[{"id":"W3-I1","files":["scripts/example.mjs"]},{"id":"coordinator","files":["skills/wave-executor/wave-loop.md"]}]'
|
|
68
68
|
WAVE_SCOPES_SIDECAR="$(
|
|
69
69
|
printf '%s' "$WAVE_SCOPE_RECORDS" | node "$PLUGIN_ROOT/scripts/materialize-wave-scope.mjs" \
|
|
70
|
-
--state-dir "$STATE_DIR" --wave "$WAVE"
|
|
70
|
+
--state-dir "$STATE_DIR" --wave "$WAVE" --warn-missing
|
|
71
71
|
)"
|
|
72
72
|
[ -n "$WAVE_SCOPES_SIDECAR" ] || { echo "materialize-wave-scope produced no sidecar path" >&2; exit 1; }
|
|
73
73
|
```
|
|
@@ -81,6 +81,10 @@ Before each wave dispatch:
|
|
|
81
81
|
|
|
82
82
|
`materialize-wave-scope.mjs` validates the COMPLETE input before writing; it writes `<state-dir>/filescopes/wave-<N>/<agent-id>.json` as each bare `files` array first, then writes `<state-dir>/filescopes/wave-<N>.scopes.json` as the unchanged aggregate record array last. Its human stdout is only that final sidecar path, so the command substitution above is the canonical `$WAVE_SCOPES_SIDECAR`. On error, do not continue with a partial declaration set; correct the plan and run the one command again.
|
|
83
83
|
|
|
84
|
+
**Check missing-path warnings before dispatch (#1235).** The recipe opts into `--warn-missing`: each absent concrete path is named with its agent on stderr. Relative paths resolve from the current project working directory, never from `$STATE_DIR`. A warning does not fail materialization or alter stdout, declaration bytes, ordering, or session binding. Grants containing `*` or ending in `/` retain their existing glob/prefix meaning and are skipped; `?` and braces remain literal path characters.
|
|
85
|
+
|
|
86
|
+
For each file the plan intentionally creates, append **`--new-file PATH`**, repeating the option for multiple files. Each exception must exactly match a path already declared in a `files` array and pass the same path validation. Invalid or undeclared exceptions fail before any scope file is written, even without `--warn-missing`. This explicit declaration suppresses only that path's warning; the materializer never guesses a filename or rewrites a grant. Without `--warn-missing`, existing callers keep their quiet missing-path behavior.
|
|
87
|
+
|
|
84
88
|
The per-agent path IS `$AGENT_FILESCOPE_JSON` — the same file `--assert-subset` (#796 below), Grounding Injection (#85), the Learnings-Index (#1014) and File-Scope Injection (#1020) consume. Never write a `$TMPDIR` copy: it degrades to a signal-free allow when an injector cannot find the addressable wave-keyed file. The coordinator's record is materialized as `coordinator.json` and included in the aggregate, so its direct edits are covered by the two checks below.
|
|
85
89
|
|
|
86
90
|
> **`<state-dir>/filescopes/` is control state, like `wave-scope.json` itself — never a wave territory.** Step 3.1 necessarily runs before the union 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.
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* scripts/lib/multi-provider-build/providers.mjs
|
|
3
|
-
*
|
|
4
|
-
* Provider configuration for the single-source → many-provider build PoC.
|
|
5
|
-
*
|
|
6
|
-
* Scope: the THREE harnesses session-orchestrator actually targets
|
|
7
|
-
* (Claude Code primary, Codex CLI, Cursor IDE). Inspired by pbakaus/impeccable's
|
|
8
|
-
* 12-provider build (Apache-2.0) — we deliberately keep the templating mechanic
|
|
9
|
-
* and drop the 12-provider breadth (maintenance ≫ value for providers with ~0
|
|
10
|
-
* users; see the linked backlog issue's overengineering verdict).
|
|
11
|
-
*
|
|
12
|
-
* Each provider entry:
|
|
13
|
-
* {
|
|
14
|
-
* key, // canonical id used in build output paths
|
|
15
|
-
* tags, // <tag> block markers this provider keeps (rest are stripped)
|
|
16
|
-
* configDir, // on-disk harness dir
|
|
17
|
-
* placeholders: { model, configFile, commandPrefix, askInstruction }
|
|
18
|
-
* }
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
/** @typedef {{key:string, tags:string[], configDir:string, placeholders:{model:string, configFile:string, commandPrefix:string, askInstruction:string}}} ProviderConfig */
|
|
22
|
-
|
|
23
|
-
/** @type {Record<string, ProviderConfig>} */
|
|
24
|
-
export const PROVIDERS = {
|
|
25
|
-
'claude-code': {
|
|
26
|
-
key: 'claude-code',
|
|
27
|
-
tags: ['claude-code', 'claude'],
|
|
28
|
-
configDir: '.claude',
|
|
29
|
-
placeholders: {
|
|
30
|
-
model: 'Claude',
|
|
31
|
-
configFile: 'CLAUDE.md',
|
|
32
|
-
commandPrefix: '/',
|
|
33
|
-
askInstruction: 'STOP and call the AskUserQuestion tool to clarify.',
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
codex: {
|
|
37
|
-
key: 'codex',
|
|
38
|
-
tags: ['codex'],
|
|
39
|
-
configDir: '.codex',
|
|
40
|
-
placeholders: {
|
|
41
|
-
model: 'GPT',
|
|
42
|
-
configFile: 'AGENTS.md',
|
|
43
|
-
commandPrefix: '$',
|
|
44
|
-
askInstruction: "STOP and use Codex's structured user-input tool to clarify.",
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
cursor: {
|
|
48
|
-
key: 'cursor',
|
|
49
|
-
tags: ['cursor'],
|
|
50
|
-
configDir: '.cursor',
|
|
51
|
-
placeholders: {
|
|
52
|
-
model: 'the model',
|
|
53
|
-
configFile: '.cursorrules',
|
|
54
|
-
commandPrefix: '/',
|
|
55
|
-
askInstruction: 'ask the user directly to clarify before proceeding.',
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/** The full set of recognized provider block tags across ALL providers. */
|
|
61
|
-
export const ALL_BLOCK_TAGS = new Set(Object.values(PROVIDERS).flatMap((p) => p.tags));
|
|
62
|
-
|
|
63
|
-
/** Provider keys in a stable order. */
|
|
64
|
-
export const PROVIDER_KEYS = Object.keys(PROVIDERS);
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* scripts/lib/multi-provider-build/templating.mjs
|
|
3
|
-
*
|
|
4
|
-
* Single-source → many-provider templating (PoC). One source document with
|
|
5
|
-
* provider-conditional blocks + placeholders renders to a provider-specific
|
|
6
|
-
* artifact. This replaces the "maintain N near-identical copies by hand" model
|
|
7
|
-
* our codex-install.mjs / cursor-install.mjs currently imply.
|
|
8
|
-
*
|
|
9
|
-
* Two primitives (clean-room reimplementation of impeccable's mechanic):
|
|
10
|
-
* 1. compileProviderBlocks(content, activeTags)
|
|
11
|
-
* <codex> … </codex> blocks: keep body iff the tag is active, else drop.
|
|
12
|
-
* Unknown tags are left untouched (so real HTML/JSX isn't mangled).
|
|
13
|
-
* 2. replacePlaceholders(content, providerKey, opts)
|
|
14
|
-
* {{model}} {{config_file}} {{command_prefix}} {{ask_instruction}}
|
|
15
|
-
* {{available_commands}} substitution + optional /cmd → $cmd rewrite.
|
|
16
|
-
*
|
|
17
|
-
* renderForProvider() composes both in the correct order (blocks first, then
|
|
18
|
-
* placeholders — matching impeccable's pipeline).
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { PROVIDERS, ALL_BLOCK_TAGS } from './providers.mjs';
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Keep `<tag>…</tag>` blocks whose tag is in `activeTags`; strip the rest.
|
|
25
|
-
* Only tags in `knownTags` are treated as conditional blocks — any other
|
|
26
|
-
* `<foo>…</foo>` (e.g. real markup) is left exactly as-is.
|
|
27
|
-
*
|
|
28
|
-
* Tags must sit on their own line, e.g.:
|
|
29
|
-
* <codex>
|
|
30
|
-
* Codex-only text.
|
|
31
|
-
* </codex>
|
|
32
|
-
*
|
|
33
|
-
* @param {string} content
|
|
34
|
-
* @param {Iterable<string>} activeTags
|
|
35
|
-
* @param {Iterable<string>} [knownTags] — defaults to ALL_BLOCK_TAGS
|
|
36
|
-
* @returns {string}
|
|
37
|
-
*/
|
|
38
|
-
export function compileProviderBlocks(content, activeTags, knownTags = ALL_BLOCK_TAGS) {
|
|
39
|
-
const active = new Set(activeTags);
|
|
40
|
-
const known = new Set(knownTags);
|
|
41
|
-
let compiledAny = false;
|
|
42
|
-
|
|
43
|
-
// Match a block: leading newline (or start), <tag> on its own line, body,
|
|
44
|
-
// </tag> on its own line. Non-greedy body; the backreference enforces matching.
|
|
45
|
-
const blockRe = /(^|\r?\n)[ \t]*<([a-z][a-z0-9-]*)>[ \t]*\r?\n([\s\S]*?)\r?\n[ \t]*<\/\2>[ \t]*(?=\r?\n|$)/g;
|
|
46
|
-
|
|
47
|
-
const out = content.replace(blockRe, (match, prefix, tag, body) => {
|
|
48
|
-
if (!known.has(tag)) return match; // not a provider block — leave untouched
|
|
49
|
-
compiledAny = true;
|
|
50
|
-
return active.has(tag) ? `${prefix}${body}` : prefix;
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// Collapse the 3+ blank-line runs that stripping can leave behind.
|
|
54
|
-
return compiledAny ? out.replace(/(\r?\n){3,}/g, '\n\n') : out;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Substitute {{placeholders}} for a provider and (optionally) rewrite command
|
|
59
|
-
* invocations from `/name` to the provider's prefix.
|
|
60
|
-
*
|
|
61
|
-
* @param {string} content
|
|
62
|
-
* @param {string} providerKey
|
|
63
|
-
* @param {{ commandNames?: string[] }} [opts]
|
|
64
|
-
* @returns {string}
|
|
65
|
-
*/
|
|
66
|
-
export function replacePlaceholders(content, providerKey, opts = {}) {
|
|
67
|
-
const provider = PROVIDERS[providerKey];
|
|
68
|
-
if (!provider) throw new Error(`Unknown provider: ${providerKey}`);
|
|
69
|
-
const { model, configFile, commandPrefix, askInstruction } = provider.placeholders;
|
|
70
|
-
const commandNames = opts.commandNames ?? [];
|
|
71
|
-
|
|
72
|
-
const availableCommands = commandNames.map((n) => `${commandPrefix}${n}`).join(', ');
|
|
73
|
-
|
|
74
|
-
let result = content
|
|
75
|
-
.replace(/\{\{model\}\}/g, model)
|
|
76
|
-
.replace(/\{\{config_file\}\}/g, configFile)
|
|
77
|
-
.replace(/\{\{ask_instruction\}\}/g, askInstruction)
|
|
78
|
-
.replace(/\{\{available_commands\}\}/g, availableCommands)
|
|
79
|
-
.replace(/\{\{command_prefix\}\}/g, commandPrefix);
|
|
80
|
-
|
|
81
|
-
// Rewrite `/cmd` invocations to the provider prefix (e.g. Codex uses `$`).
|
|
82
|
-
// Longest names first so `/session-end` is rewritten before `/session`.
|
|
83
|
-
if (commandPrefix !== '/' && commandNames.length > 0) {
|
|
84
|
-
const sorted = [...commandNames].sort((a, b) => b.length - a.length);
|
|
85
|
-
for (const name of sorted) {
|
|
86
|
-
// `/name` where name is followed by a non-identifier char or end.
|
|
87
|
-
const re = new RegExp(`\\/(${escapeRegex(name)})(?=[^a-zA-Z0-9_-]|$)`, 'g');
|
|
88
|
-
// Use a replacement FUNCTION, not a string — a `$` commandPrefix would
|
|
89
|
-
// otherwise collide with String.replace's `$$`/`$1` escape syntax.
|
|
90
|
-
result = result.replace(re, (_match, g1) => `${commandPrefix}${g1}`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return result;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Render a source document for a single provider: compile blocks, then
|
|
99
|
-
* substitute placeholders.
|
|
100
|
-
*
|
|
101
|
-
* @param {string} source
|
|
102
|
-
* @param {string} providerKey
|
|
103
|
-
* @param {{ commandNames?: string[] }} [opts]
|
|
104
|
-
* @returns {string}
|
|
105
|
-
*/
|
|
106
|
-
export function renderForProvider(source, providerKey, opts = {}) {
|
|
107
|
-
const provider = PROVIDERS[providerKey];
|
|
108
|
-
if (!provider) throw new Error(`Unknown provider: ${providerKey}`);
|
|
109
|
-
const compiled = compileProviderBlocks(source, provider.tags);
|
|
110
|
-
return replacePlaceholders(compiled, providerKey, opts);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Render a source for every configured provider.
|
|
115
|
-
* @param {string} source
|
|
116
|
-
* @param {{ commandNames?: string[] }} [opts]
|
|
117
|
-
* @returns {Record<string,string>} keyed by provider key
|
|
118
|
-
*/
|
|
119
|
-
export function renderAll(source, opts = {}) {
|
|
120
|
-
const out = {};
|
|
121
|
-
for (const key of Object.keys(PROVIDERS)) {
|
|
122
|
-
out[key] = renderForProvider(source, key, opts);
|
|
123
|
-
}
|
|
124
|
-
return out;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** @param {string} s */
|
|
128
|
-
function escapeRegex(s) {
|
|
129
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
130
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* owner-config/coerce.mjs — Strict-mode wrapper around validate().
|
|
3
|
-
*
|
|
4
|
-
* Imports: validate + OwnerConfigError. Throws on validation failure, returns
|
|
5
|
-
* the normalized value on success. Convenient for CLI entrypoints and tests
|
|
6
|
-
* that want to assert on success without inspecting `ok`.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { validate } from './validate.mjs';
|
|
10
|
-
import { OwnerConfigError } from './error.mjs';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Strict-mode wrapper around validate(). Returns the normalized value when
|
|
14
|
-
* the input passes the gate, otherwise throws OwnerConfigError with the
|
|
15
|
-
* full error list attached as `.errors`.
|
|
16
|
-
*
|
|
17
|
-
* @param {unknown} raw
|
|
18
|
-
* @returns {object}
|
|
19
|
-
*/
|
|
20
|
-
export function coerce(raw) {
|
|
21
|
-
const result = validate(raw);
|
|
22
|
-
if (!result.ok) {
|
|
23
|
-
throw new OwnerConfigError(
|
|
24
|
-
`owner config validation failed (${result.errors.length} error${result.errors.length === 1 ? '' : 's'})`,
|
|
25
|
-
result.errors
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
return result.value;
|
|
29
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* owner-config/constants.mjs — Schema constants for the owner persona config.
|
|
3
|
-
*
|
|
4
|
-
* Leaf module. No imports. All exported arrays are frozen so consumers cannot
|
|
5
|
-
* accidentally mutate the canonical enum sets.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/** Current owner-config schema version. New writes are stamped with this. */
|
|
9
|
-
export const CURRENT_OWNER_SCHEMA_VERSION = 1;
|
|
10
|
-
|
|
11
|
-
/** Valid values for tone.style. */
|
|
12
|
-
export const VALID_TONE_STYLES = Object.freeze(['direct', 'neutral', 'friendly']);
|
|
13
|
-
|
|
14
|
-
/** Valid values for efficiency.output-level. */
|
|
15
|
-
export const VALID_OUTPUT_LEVELS = Object.freeze(['lite', 'full', 'ultra']);
|
|
16
|
-
|
|
17
|
-
/** Valid values for efficiency.preamble. */
|
|
18
|
-
export const VALID_PREAMBLE_LEVELS = Object.freeze(['minimal', 'verbose']);
|
|
19
|
-
|
|
20
|
-
/** Valid values for efficiency.comments-in-code. */
|
|
21
|
-
export const VALID_COMMENTS_LEVELS = Object.freeze(['minimal', 'full']);
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* owner-config/defaults.mjs — Canonical default-filled owner config factory.
|
|
3
|
-
*
|
|
4
|
-
* Imports constants only. Leaf in the dependency sense (no circular deps).
|
|
5
|
-
* Returns the fully-default-filled config with `owner.name` and
|
|
6
|
-
* `owner.language` intentionally left blank — those are required from the user;
|
|
7
|
-
* the bootstrap interview (D2) fills them in.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { CURRENT_OWNER_SCHEMA_VERSION } from './constants.mjs';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Returns the canonical default-filled owner config. `owner.name` and
|
|
14
|
-
* `owner.language` are intentionally empty — those are user-required fields
|
|
15
|
-
* the bootstrap interview (D2) fills in. The `defaults()` shape is what
|
|
16
|
-
* `merge()` uses as its base when callers pass partial overrides.
|
|
17
|
-
*
|
|
18
|
-
* @returns {object}
|
|
19
|
-
*/
|
|
20
|
-
export function defaults() {
|
|
21
|
-
return {
|
|
22
|
-
'schema-version': CURRENT_OWNER_SCHEMA_VERSION,
|
|
23
|
-
owner: {
|
|
24
|
-
name: '',
|
|
25
|
-
'email-hash': null,
|
|
26
|
-
language: '',
|
|
27
|
-
},
|
|
28
|
-
tone: {
|
|
29
|
-
style: 'neutral',
|
|
30
|
-
tonality: null,
|
|
31
|
-
},
|
|
32
|
-
efficiency: {
|
|
33
|
-
'output-level': 'full',
|
|
34
|
-
preamble: 'minimal',
|
|
35
|
-
'comments-in-code': 'minimal',
|
|
36
|
-
},
|
|
37
|
-
'hardware-sharing': {
|
|
38
|
-
enabled: false,
|
|
39
|
-
'hash-salt': null,
|
|
40
|
-
},
|
|
41
|
-
defaults: {
|
|
42
|
-
'preferred-test-command': null,
|
|
43
|
-
'preferred-editor': null,
|
|
44
|
-
},
|
|
45
|
-
metadata: {
|
|
46
|
-
created_at: null,
|
|
47
|
-
updated_at: null,
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* owner-config/error.mjs — OwnerConfigError class.
|
|
3
|
-
*
|
|
4
|
-
* Leaf module. No imports. Thrown by coerce() when validation fails. The
|
|
5
|
-
* `.errors` array carries the full list of human-readable failure messages
|
|
6
|
-
* collected by validate().
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export class OwnerConfigError extends Error {
|
|
10
|
-
/**
|
|
11
|
-
* @param {string} message
|
|
12
|
-
* @param {string[]} [errors]
|
|
13
|
-
*/
|
|
14
|
-
constructor(message, errors = []) {
|
|
15
|
-
super(message);
|
|
16
|
-
this.name = 'OwnerConfigError';
|
|
17
|
-
this.errors = errors;
|
|
18
|
-
}
|
|
19
|
-
}
|