cohorte 1.2.6 → 1.3.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.
@@ -7,7 +7,7 @@
7
7
  <link rel="icon" type="image/png" sizes="16x16" href="./favicon-16.png" />
8
8
  <link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon-180.png" />
9
9
  <title>cohorte · dashboard</title>
10
- <script type="module" crossorigin src="./assets/index-YvkzH-yF.js"></script>
10
+ <script type="module" crossorigin src="./assets/index-BxgA_mz1.js"></script>
11
11
  <link rel="stylesheet" crossorigin href="./assets/index-Cj0SpgEY.css">
12
12
  </head>
13
13
  <body>
@@ -12,7 +12,7 @@ const { versions } = require('./versions');
12
12
  // Rendered surface agents live alongside these fixed (non-surface) agents; exclude them
13
13
  // from the orphan check so they're never mistaken for a stray surface agent.
14
14
  const FIXED_AGENTS = new Set([
15
- 'review', 'release',
15
+ 'review', 'release', 'smoke', 'profile-reader',
16
16
  'implementer.template',
17
17
  ]);
18
18
 
@@ -186,6 +186,39 @@ function checkIsolation(profile, projectRoot) {
186
186
  return mk('isolation', 'Isolation', 'ok', 'feature scripts rendered (worktree state not checked here)');
187
187
  }
188
188
 
189
+ // Workflow variants (review/audit/refactor as deterministic multi-agent runs) are opt-in;
190
+ // the conversational commands stay the default path, so nothing here is ever 'bad'.
191
+ // Whether the session has workflows ENABLED needs a live Claude session — /doctor
192
+ // in-session checks that; here we only check what's on disk.
193
+ function checkWorkflows(projectRoot, globalDir, installMode) {
194
+ if (installMode === 'none') return mk('workflows', 'Workflows', 'skip', 'no core installed');
195
+ const dir = installMode === 'bundled'
196
+ ? path.join(projectRoot, '.claude', 'workflows')
197
+ : path.join(globalDir, 'workflows');
198
+ const agentsDir = installMode === 'bundled'
199
+ ? path.join(projectRoot, '.claude', 'agents')
200
+ : path.join(globalDir, 'agents');
201
+ const scripts = ['review.js', 'audit.js', 'refactor.js', 'cycle.js'];
202
+ const missing = scripts.filter(s => !exists(path.join(dir, s)));
203
+ if (missing.length === scripts.length) {
204
+ return mk('workflows', 'Workflows', 'warn',
205
+ 'no workflow scripts installed — conversational commands only (the default path)',
206
+ 'npx cohorte update (ships core/workflows/)');
207
+ }
208
+ if (missing.length) {
209
+ return mk('workflows', 'Workflows', 'warn', `missing script(s): ${missing.join(', ')}`,
210
+ 'npx cohorte update (half-copied core)');
211
+ }
212
+ if (!exists(path.join(agentsDir, 'profile-reader.md'))) {
213
+ return mk('workflows', 'Workflows', 'warn',
214
+ 'scripts present but the profile-reader agent (their phase 0) is missing',
215
+ 'npx cohorte update (re-copies the fixed agents)');
216
+ }
217
+ return mk('workflows', 'Workflows', 'ok',
218
+ 'scripts + profile-reader installed — opt-in per run; needs Claude Code ≥ 2.1.154 with ' +
219
+ 'workflows enabled (run /doctor in-session to check the live half)');
220
+ }
221
+
189
222
  function scanSpecs(projectRoot) {
190
223
  const dir = path.join(projectRoot, 'specs');
191
224
  const specs = [];
@@ -240,6 +273,7 @@ async function state({ projectRoot, globalDir, cliVersion }) {
240
273
  checkRetrieval(profile),
241
274
  checkDesign(profile, projectRoot),
242
275
  checkIsolation(profile, projectRoot),
276
+ checkWorkflows(projectRoot, globalDir, v.installMode),
243
277
  checkSpecs(specs),
244
278
  ];
245
279
 
@@ -113,11 +113,13 @@ function runAction(req, res, body, { pkgRoot }) {
113
113
  // Run a pipeline slash-command through Claude Code headless (`claude -p`) in the project dir,
114
114
  // streaming its output. The command is whitelisted (no arbitrary injection into claude -p) and
115
115
  // runs autonomously (--dangerously-skip-permissions), so it never hangs waiting on a prompt.
116
+ // Headless caveat the UI warns about: the run starts without any confirmation prompt and there
117
+ // is no resume — if the claude process dies mid-run, the run is simply gone.
116
118
  function runClaude(req, res, body) {
117
119
  const project = body.project ? path.resolve(body.project) : null;
118
120
  const command = String(body.command || '');
119
- if (!/^\/(init-pipeline|update-pipeline)$/.test(command)) {
120
- return sendJson(res, 400, { error: 'unsupported command (only /init-pipeline or /update-pipeline)' });
121
+ if (!/^\/(init-pipeline|update-pipeline|audit)$/.test(command)) {
122
+ return sendJson(res, 400, { error: 'unsupported command (only /init-pipeline, /update-pipeline or /audit)' });
121
123
  }
122
124
  if (!project || !fs.existsSync(project)) {
123
125
  return sendJson(res, 400, { error: 'project path not found' });
package/install.ps1 CHANGED
@@ -136,6 +136,7 @@ try {
136
136
  Copy-Tree (Join-Path $src 'core\commands') (Join-Path $dest 'commands')
137
137
  Copy-Tree (Join-Path $src 'core\hooks') (Join-Path $dest 'hooks')
138
138
  Copy-Tree (Join-Path $src 'core\templates') (Join-Path $dest 'templates')
139
+ Copy-Tree (Join-Path $src 'core\workflows') (Join-Path $dest 'workflows')
139
140
  # 0.1.19 renamed questionnaire-domain-brief.md -> research-brief.md; drop the stale copy.
140
141
  Remove-Item -LiteralPath (Join-Path $dest 'templates\questionnaire-domain-brief.md') -Force -ErrorAction SilentlyContinue
141
142
  New-Item -ItemType Directory -Force -Path (Join-Path $dest 'pipeline\scripts') | Out-Null
@@ -145,6 +146,7 @@ try {
145
146
  Copy-Item (Join-Path $src 'scripts\*.template') (Join-Path $dest 'pipeline\scripts') -Force
146
147
  Copy-Item (Join-Path $src 'scripts\kanban-move.sh') (Join-Path $dest 'pipeline\scripts') -Force
147
148
  Copy-Item (Join-Path $src 'scripts\telemetry-send.sh') (Join-Path $dest 'pipeline\scripts') -Force
149
+ Copy-Item (Join-Path $src 'scripts\preflight.sh') (Join-Path $dest 'pipeline\scripts') -Force
148
150
  Copy-Item (Join-Path $src 'core\agents\implementer.template.md') (Join-Path $dest 'pipeline') -Force
149
151
  if (Test-Path (Join-Path $src 'CHANGELOG.md')) { Copy-Item (Join-Path $src 'CHANGELOG.md') (Join-Path $dest 'pipeline') -Force }
150
152
  [System.IO.File]::WriteAllText((Join-Path $dest 'pipeline\VERSION'), "$ver`n", [System.Text.UTF8Encoding]::new($false))
@@ -182,7 +184,8 @@ try {
182
184
  New-Item -ItemType Directory -Force -Path (Join-Path $dest 'agents') | Out-Null
183
185
  Copy-Item (Join-Path $src 'core\agents\review.md'),
184
186
  (Join-Path $src 'core\agents\release.md'),
185
- (Join-Path $src 'core\agents\smoke.md') (Join-Path $dest 'agents') -Force
187
+ (Join-Path $src 'core\agents\smoke.md'),
188
+ (Join-Path $src 'core\agents\profile-reader.md') (Join-Path $dest 'agents') -Force
186
189
  # 0.1.19 split the bi-mode questionnaire-researcher into research-agent + questionnaire-architect;
187
190
  # copy-over never deletes, so scrub the retired agent lest a dead subagent_type linger.
188
191
  Remove-Item -LiteralPath (Join-Path $dest 'agents\questionnaire-researcher.md') -Force -ErrorAction SilentlyContinue
package/install.sh CHANGED
@@ -78,6 +78,7 @@ copy_core() {
78
78
  cp -R "$src/core/commands" "$dest/"
79
79
  cp -R "$src/core/hooks" "$dest/"
80
80
  cp -R "$src/core/templates" "$dest/"
81
+ cp -R "$src/core/workflows" "$dest/"
81
82
  # 0.1.19 renamed questionnaire-domain-brief.md → research-brief.md; drop the stale copy.
82
83
  rm -f "$dest/templates/questionnaire-domain-brief.md"
83
84
  mkdir -p "$dest/pipeline/scripts"
@@ -87,7 +88,9 @@ copy_core() {
87
88
  cp "$src"/scripts/*.template "$dest/pipeline/scripts/"
88
89
  cp "$src/scripts/kanban-move.sh" "$dest/pipeline/scripts/"
89
90
  cp "$src/scripts/telemetry-send.sh" "$dest/pipeline/scripts/"
90
- chmod +x "$dest/pipeline/scripts/kanban-move.sh" "$dest/pipeline/scripts/telemetry-send.sh" 2>/dev/null || true
91
+ cp "$src/scripts/preflight.sh" "$dest/pipeline/scripts/"
92
+ chmod +x "$dest/pipeline/scripts/kanban-move.sh" "$dest/pipeline/scripts/telemetry-send.sh" \
93
+ "$dest/pipeline/scripts/preflight.sh" 2>/dev/null || true
91
94
  cp "$src/core/agents/implementer.template.md" "$dest/pipeline/"
92
95
  [ -f "$src/CHANGELOG.md" ] && cp "$src/CHANGELOG.md" "$dest/pipeline/"
93
96
  printf '%s\n' "$ver" > "$dest/pipeline/VERSION"
@@ -128,7 +131,7 @@ PY
128
131
  copy_fixed_agents() {
129
132
  mkdir -p "$dest/agents"
130
133
  cp "$src/core/agents/review.md" "$src/core/agents/release.md" \
131
- "$src/core/agents/smoke.md" \
134
+ "$src/core/agents/smoke.md" "$src/core/agents/profile-reader.md" \
132
135
  "$dest/agents/"
133
136
  # 0.1.19 split the bi-mode questionnaire-researcher into research-agent + questionnaire-architect;
134
137
  # copy-over never deletes, so scrub the retired agent lest a dead subagent_type linger.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cohorte",
3
- "version": "1.2.6",
3
+ "version": "1.3.0",
4
4
  "description": "Portable, stack-agnostic multi-agent development pipeline for Claude Code — install the core, run /init-pipeline, and it adapts to your project's stack.",
5
5
  "bin": {
6
6
  "cohorte": "bin/cli.js"
@@ -59,7 +59,13 @@ surfaces:
59
59
  # vs the Opus lead); haiku = purely mechanical scaffolding;
60
60
  # inherit = only for surfaces with real design decisions
61
61
  test_cmd: pnpm --filter api test
62
+ # Bridled variants — what agents actually RUN (dot reporter / failures-only /
63
+ # --quiet), so a green run costs lines, not pages. "" ⇒ callers fall back to
64
+ # `<cmd> 2>&1 | tail -40`. /init-pipeline asks for these; never store a bare
65
+ # `pnpm test` as the thing agents execute.
66
+ test_quiet_cmd: pnpm --filter api test --reporter=dot
62
67
  lint_cmd: pnpm --filter api lint
68
+ lint_quiet_cmd: pnpm --filter api lint --quiet
63
69
  format_cmd: pnpm --filter api format
64
70
  typecheck_cmd: pnpm --filter api exec tsc --noEmit
65
71
  build_cmd: ""
@@ -74,7 +80,9 @@ surfaces:
74
80
  # lead's tier, often Opus) ONLY if this surface must make
75
81
  # novel design decisions
76
82
  test_cmd: pnpm --filter web test
83
+ test_quiet_cmd: pnpm --filter web test --reporter=dot
77
84
  lint_cmd: pnpm --filter web lint
85
+ lint_quiet_cmd: pnpm --filter web lint --quiet
78
86
  format_cmd: pnpm --filter web format
79
87
  typecheck_cmd: pnpm check-types
80
88
  build_cmd: pnpm --filter web build
@@ -94,9 +102,11 @@ commands:
94
102
  install: pnpm install
95
103
  dev: pnpm dev
96
104
  lint: pnpm lint
105
+ lint_quiet: pnpm lint --quiet # bridled variant (see surfaces[].*_quiet_cmd)
97
106
  format: pnpm format
98
107
  typecheck: pnpm check-types
99
108
  test: pnpm test
109
+ test_quiet: pnpm test --reporter=dot # bridled variant — what /review·/smoke preflight runs
100
110
  # migration commands — omit / leave "" if the project has no DB migrations
101
111
  migrate: "cd apps/api && node ace migration:run"
102
112
  make_migration: "cd apps/api && node ace make:migration"
@@ -154,6 +164,13 @@ gate:
154
164
  - "git rebase"
155
165
  - "git reset"
156
166
  - "docker compose"
167
+ # Phase gate: review/smoke dispatches require a fresh `.claude/preflight.ok` stamp,
168
+ # written by pipeline/scripts/preflight.sh when typecheck+lint+tests are green —
169
+ # gate.py "ask"s the dispatch when the stamp is missing, stale, or HEAD moved.
170
+ preflight:
171
+ enabled: true
172
+ agents: [review, smoke] # subagent_types the stamp gates
173
+ max_age_minutes: 30
157
174
 
158
175
  ```
159
176
 
package/profile/SCHEMA.md CHANGED
@@ -27,12 +27,14 @@ generic pipeline uses it, so a stateless agent can read/regenerate the profile c
27
27
  | `surfaces[].tools` | list | init | Frontmatter `tools:` for the rendered agent. |
28
28
  | `surfaces[].model` | enum | init (`<SURFACE_MODEL>`) | Frontmatter `model:` tier — `sonnet`/`haiku`/`inherit`. Default `sonnet` (implementers mostly apply a frozen contract — far cheaper than the Opus lead the dispatcher runs on, and Sonnet handles it well); `haiku` for purely mechanical surfaces (scaffolding); `inherit` only for surfaces with real design decisions worth the lead's model. |
29
29
  | `surfaces[].*_cmd` | string | implementer | test/lint/format/typecheck/build commands. |
30
+ | `surfaces[].test_quiet_cmd` `.lint_quiet_cmd` | string | implementer, preflight, workflows | Bridled variants agents actually run (dot reporter / `--quiet` / failures-only). `""` ⇒ `<cmd> 2>&1 \| tail -40`. See §Output discipline. |
30
31
  | `surfaces[].uses_design` | bool | build, frontend | Whether this surface consumes designs. |
31
32
  | `contract.enabled` | bool | build | `false` ⇒ skip contract authoring (§2 of /build). |
32
33
  | `contract.mechanism` | enum | build, lead | `shared-types-zod`/`openapi`/`protobuf`/`json-schema`/`none`. |
33
34
  | `contract.path` `.ext` `.index` | string | build | Where `<feature_id>` contract is authored + barrel. |
34
35
  | `contract.authored_by` | const `lead` | build | Implementers import it read-only, never edit. |
35
36
  | `commands.*` | string | all | Repo-wide install/dev/lint/format/typecheck/test + migrate. |
37
+ | `commands.test_quiet` `.lint_quiet` | string | review, smoke, audit, workflows | Repo-wide bridled variants — what the `/review`·`/smoke` pre-flight runs. Same fallback as the per-surface ones. |
36
38
  | `rbac.enabled` | bool | brainstorm, review | Toggle RBAC personas + authz audit. |
37
39
  | `rbac.hierarchy` | list | review | Highest→lowest role list. |
38
40
  | `design.enabled` | bool | build, frontend, align-ds | `false` ⇒ design steps are no-ops. |
@@ -50,6 +52,9 @@ generic pipeline uses it, so a stateless agent can read/regenerate the profile c
50
52
  | `gate.ask[]` | list | hooks/gate.py, settings | Command substrings that require confirm, on any branch. |
51
53
  | `gate.ask_on_default_branch[]` | list | hooks/gate.py | Confirm ONLY on `default_branch`; free on feature branches. |
52
54
  | `gate.default_branch` | string | hooks/gate.py | Protected branch (default `main`); gate resolves via git. |
55
+ | `gate.preflight.enabled` | bool | hooks/gate.py, review, smoke | Phase gate: review/smoke dispatches need a fresh preflight stamp. See §Preflight. |
56
+ | `gate.preflight.agents[]` | list | hooks/gate.py | `subagent_type`s the stamp gates (default `[review, smoke]`). |
57
+ | `gate.preflight.max_age_minutes` | number | hooks/gate.py | Stamp freshness window (default 30). |
53
58
 
54
59
  ## Prose sections
55
60
 
@@ -186,6 +191,49 @@ at each phase boundary is always safe — each command's closing line recommends
186
191
  commands enforce: never paste a diff into a dispatch (agents compute their own, scoped); never echo a
187
192
  staged report or design brief into chat; redirect bulky command output to a file and grep it.
188
193
 
194
+ ## Output discipline — quiet commands
195
+
196
+ A test runner's default output is written for a human watching a terminal: one line per test, banners,
197
+ timing tables. An agent pays input price for every one of those lines, on every turn they survive in its
198
+ context. The profile therefore stores **two forms of each noisy command**:
199
+
200
+ - `test_cmd` / `lint_cmd` — the full form, for a human running it by hand.
201
+ - `test_quiet_cmd` / `lint_quiet_cmd` (per surface) and `commands.test_quiet` / `commands.lint_quiet`
202
+ (repo-wide) — the **bridled** form agents actually execute: dot/failures-only reporter
203
+ (`--reporter=dot`, `--quiet`, `-q`, `--silent`, framework equivalent) so a green run costs lines,
204
+ not pages, and a red run prints only the failures.
205
+
206
+ Rules for every consumer (implementers, preflight, `/audit` gates, workflow agents):
207
+
208
+ 1. Run the quiet variant when set.
209
+ 2. Quiet variant empty/absent (older profile) ⇒ run `<full cmd> 2>&1 | tail -40` — never the bare
210
+ command into your context.
211
+ 3. Need the full log? Redirect it to a file and grep it; never print it.
212
+
213
+ `/init-pipeline` **asks** for these variants (detected defaults offered first) instead of silently
214
+ storing a bare `pnpm test` as the thing agents execute; `/update-pipeline` tops up older profiles.
215
+
216
+ ## Preflight — the deterministic phase gate
217
+
218
+ `/review` and `/smoke` start by running `pipeline/scripts/preflight.sh` — a plain shell script (no
219
+ agent) that executes the profile's mechanical checks in order (typecheck → lint → tests, quiet
220
+ variants) with all output redirected to `specs/reports/<id>.preflight.txt`:
221
+
222
+ - **Any check red** ⇒ the script prints the last 40 lines raw and exits 1. The command **aborts
223
+ there: zero agents are spawned.** A reviewer dispatched onto code that doesn't compile burns its
224
+ whole run rediscovering what `tsc` already printed for free — the failure goes straight to the
225
+ human (or `/fix`) instead.
226
+ - **All green** ⇒ the script stamps `.claude/preflight.ok` (`<epoch> <HEAD sha>`).
227
+
228
+ `hooks/gate.py` enforces the stamp as a **phase gate** (the `preflight` block of `gate-config.json`,
229
+ generated from `gate.preflight`): a Task dispatch of a listed `subagent_type` (default
230
+ `review`/`smoke`) with a missing/stale stamp — older than `max_age_minutes`, or HEAD moved — gets an
231
+ "ask", so a lead can't accidentally skip the gate but a human can consciously override it. The gate
232
+ hook fires for **every** agent in the session, including subagents spawned by the Workflow runtime
233
+ (they run in `acceptEdits` whatever the session mode — Write/Edit auto-approved — but Bash and Task
234
+ still pass through hooks). In `bypassPermissions` (headless runs) every gate "ask" is escalated to a
235
+ hard deny, because nobody is there to answer a prompt.
236
+
189
237
  ## Rendering / reconciling a surface agent (shared procedure)
190
238
 
191
239
  Both `/init-pipeline` (initial render) and `/build` (auto-reconcile when a spec needs a new agent) use
@@ -203,7 +251,13 @@ this exact procedure so a surface is always defined the same way. To add surface
203
251
  (resolve bundled `.claude/` vs global `~/.claude/`), substituting `<SURFACE_AGENT>`, `<SURFACE_LABEL>`,
204
252
  `<SURFACE_PATH>`, `<SURFACE_TOOLS>`, `<SURFACE_MODEL>`, `<PROJECT_NAME>`, and the surface-specific
205
253
  blocks (`<SURFACE_EXTRA_NEVER>`, `<SURFACE_DESIGN_INPUT>`, `<SURFACE_TDD_STEP1>` — leave the design
206
- ones empty unless `uses_design`). For a `uses_design` surface, fill them **link-based** (never with a
254
+ ones empty unless `uses_design`). Fill `<SURFACE_CONVENTIONS>` with the surface's convention slice
255
+ **baked at render time**: `PIPELINE.md` §Conventions `### Shared` + this surface's
256
+ `### Surface: <key>` stanza + its §Testing lines, verbatim. At runtime the agent then reads only
257
+ the profile's machine block (the fenced `yaml pipeline-profile`) — never the prose sections. The
258
+ bake stays honest because §Conventions edits go through `/update-pipeline`, whose reconcile
259
+ re-renders every agent (step 2 below); hand-edit the prose without re-rendering and the baked
260
+ slice goes stale — that's the trade for not re-reading the prose on every dispatch. For a `uses_design` surface, fill them **link-based** (never with a
207
261
  stored `design_project` id — that goes stale on a DS rebuild):
208
262
  - `<SURFACE_DESIGN_INPUT>` — a 4th input bullet: _"The **feature design** — the pages this feature
209
263
  touches, listed in your dispatch's design slot as full links
@@ -257,6 +311,64 @@ files automatically. It works because every generated artifact is a **determinis
257
311
  Re-running `/init-pipeline` remains possible (it reconciles too) but is only *needed* when the stack
258
312
  itself changes in ways `/build` §1.5 can't auto-grow (e.g. package manager or contract mechanism swap).
259
313
 
314
+ ## Workflows — deterministic multi-agent runs (opt-in)
315
+
316
+ Three phases have a **workflow variant** — a deterministic orchestration script the Claude Code
317
+ Workflow runtime executes instead of the lead reasoning out the fan-out turn by turn:
318
+ `<core>/workflows/review.js`, `audit.js`, `refactor.js` (installed to `.claude/workflows/` bundled or
319
+ `~/.claude/workflows/` global). The conversational commands (`/review`, `/audit`, `/refactor`)
320
+ **remain the default path and the fallback** — a workflow runs only when the human explicitly asks
321
+ for it ("run the review workflow"), and requires Claude Code ≥ **2.1.154** with workflows enabled.
322
+ `/doctor` reports which path a session will take. The interactive commands (`/init-pipeline`,
323
+ `/brainstorm`, `/spec`) and the dispatch-only ones (`/build`, `/ship`) have **no** workflow variant on
324
+ purpose: they're interviews or already a single parallel dispatch — a script adds nothing.
325
+
326
+ Shared design, all three scripts:
327
+
328
+ - **Phase 0 is always `profile-reader`** — workflow scripts have no filesystem or shell access, so a
329
+ dedicated agent (`core/agents/profile-reader.md`, haiku, read-only) reads `PIPELINE.md` and returns
330
+ the `yaml pipeline-profile` block as JSON. Every later phase is parameterized from that object.
331
+ - **Mechanical phases run on haiku** (profile read, preflight, diff staging, report merging/writing);
332
+ judgment phases dispatch the same pinned agents the commands use (`review` at sonnet, the surface
333
+ implementers at their `surfaces[].model` tier) — the per-surface `model:` routing carries over.
334
+ - **Only the verdict comes back.** Bulk (diffs, reports, backlogs) is staged to the same disk
335
+ buffers the commands use (`specs/reports/`, `specs/refactor-backlog.md`); the workflow's return is
336
+ counts + verdict + paths.
337
+ - **`review.js`** — preflight gate (aborts red, zero agents), one `git diff --stat` staged per
338
+ touched surface, one reviewer per surface in parallel, then an **adversarial cross-check** phase
339
+ that tries to refute each CRITICAL/security finding before it can trigger a fix loop.
340
+ - **`audit.js`** — one auditor per domain (each surface + `shared`), concurrency capped by the
341
+ runtime (~16), merged into the prioritized `specs/refactor-backlog.md`.
342
+ - **`refactor.js`** — big domains only (it skips domains with a handful of open items — the
343
+ conversational `/refactor` is cheaper there): `shared` first and alone, then the other domains'
344
+ implementers in parallel, each verified per-domain.
345
+ - **`cycle.js`** — the **full dev cycle** on a frozen spec: contract → parallel build → rounds of
346
+ [preflight → smoke ∥ review(+cross-check) → fix on the surfaces with findings], looping until
347
+ **zero open findings + a PASS smoke** (`maxRounds`, default 5, and the token budget are runaway
348
+ protection, not targets). Since a workflow can't ask anything mid-run, the decisions move to the
349
+ edges: a **readiness gate** aborts up front if the spec isn't frozen (other gaps ride along as
350
+ deferred questions), and everything genuinely human comes back at the END in the result's
351
+ `questions` array — empty when `/brainstorm` + `/spec` did their job. Even a finding that implies
352
+ a **contract change stays inside the loop**: a lead-equivalent agent re-authors spec §5 + the
353
+ contract file (exactly what conversational `/fix` §1 does — implementers still never touch it),
354
+ the consuming surfaces re-dispatch, and the loop continues; the re-authorings are reported in the
355
+ result's `contractChanges` for the human to review in the diff. A clean exit ticks the DoD and
356
+ stamps the freshness gate so `/ship <id>` is a straight shot; a stopped run appends its open
357
+ findings to the spec's `## Remediation` so a rerun of the cycle — or a conversational `/fix` —
358
+ continues seamlessly. `/ship` itself stays outside on purpose — outward-facing and irreversible,
359
+ it keeps its human confirmation.
360
+ **Corollary — harden the spec:** the more `/brainstorm` + `/spec` pre-answer (edge cases, error
361
+ envelopes, role matrix, design links), the further the cycle runs and the emptier `questions`
362
+ comes back; a vague spec just converts into deferred questions.
363
+ - **No input mid-run.** A workflow runs to completion without questions; anything interactive
364
+ (contract changes, human decisions) belongs to the conversational path — or, for `cycle.js`, to
365
+ the `questions` array of its result. The gate hook still fires on workflow subagents (see
366
+ §Preflight) — in unattended runs its asks become denies.
367
+ - **Permissions:** `/init-pipeline` and `/update-pipeline` extend the generated `settings.json`
368
+ `allow` list with what workflow agents need (the quiet commands, the shipped
369
+ `pipeline/scripts/*.sh`, read-only git incl. `git rev-parse`, and the retrieval provider's MCP
370
+ tools) so a run never stalls mid-workflow on a permission prompt nobody is watching.
371
+
260
372
  ## Kanban — mirroring the pipeline onto an Obsidian board
261
373
 
262
374
  An **optional, user-scoped** mirror of the dev flow: each pipeline stage moves a card across an
@@ -0,0 +1,48 @@
1
+ #!/bin/sh
2
+ #
3
+ # preflight.sh — deterministic phase gate for /review and /smoke.
4
+ #
5
+ # Runs the profile's mechanical checks (typecheck, lint, tests — whatever the caller
6
+ # passes) BEFORE any agent is spawned. A red gate means the caller aborts and relays
7
+ # the raw failure — dispatching reviewers onto code that doesn't even compile burns
8
+ # their whole run on noise a compiler already printed for free.
9
+ #
10
+ # preflight.sh <report-file> "<cmd>" ["<cmd>"...]
11
+ #
12
+ # - Each command runs through `sh -c`, all output appended to <report-file> — the bulk
13
+ # never enters the calling agent's context.
14
+ # - First failure: prints the last 40 lines of the report raw to stderr and exits 1.
15
+ # The caller must stop there — no agents.
16
+ # - All green: writes `<project>/.claude/preflight.ok` ("<epoch> <HEAD sha>") — the
17
+ # stamp `hooks/gate.py` checks (gate-config.json `preflight` block) before letting
18
+ # review/smoke agents dispatch.
19
+
20
+ set -u
21
+
22
+ report="${1:?usage: preflight.sh <report-file> \"<cmd>\" [\"<cmd>\"...]}"
23
+ shift
24
+ [ "$#" -gt 0 ] || { echo "preflight: no commands given" >&2; exit 2; }
25
+
26
+ mkdir -p "$(dirname "$report")" 2>/dev/null || true
27
+ : > "$report"
28
+
29
+ n=0
30
+ for cmd in "$@"; do
31
+ [ -n "$cmd" ] || continue
32
+ n=$((n + 1))
33
+ printf '\n$ %s\n' "$cmd" >> "$report"
34
+ if ! sh -c "$cmd" >> "$report" 2>&1; then
35
+ echo "PREFLIGHT FAIL — $cmd" >&2
36
+ echo "--- last 40 lines of $report ---" >&2
37
+ tail -40 "$report" >&2
38
+ exit 1
39
+ fi
40
+ done
41
+
42
+ # Stamp for the gate.py phase gate: epoch + HEAD sha of the checkout we verified.
43
+ proj="${CLAUDE_PROJECT_DIR:-.}"
44
+ sha=$(git rev-parse HEAD 2>/dev/null || echo none)
45
+ mkdir -p "$proj/.claude" 2>/dev/null || true
46
+ printf '%s %s\n' "$(date +%s)" "$sha" > "$proj/.claude/preflight.ok" 2>/dev/null || true
47
+
48
+ echo "PREFLIGHT PASS ($n checks green) — full log: $report"
@@ -43,7 +43,8 @@ for (const f of readdirSync(join(root, "core/commands"))) {
43
43
  // Every non-template agent needs name/tools/model, and must be shipped by
44
44
  // both installers (a new agent that install.sh doesn't copy never reaches
45
45
  // a global install — the exact bug that motivated this check).
46
- const AGENT_MODEL = { review: "sonnet", release: "haiku", smoke: "sonnet" };
46
+ const AGENT_MODEL = { review: "sonnet", release: "haiku", smoke: "sonnet",
47
+ "profile-reader": "haiku" };
47
48
  const installSh = read("install.sh");
48
49
  const installPs1 = read("install.ps1");
49
50
 
@@ -54,7 +55,7 @@ for (const f of readdirSync(join(root, "core/agents"))) {
54
55
  if (!fm) { fail(path, "missing or malformed YAML frontmatter"); continue; }
55
56
  if (f === "implementer.template.md") {
56
57
  for (const ph of ["<SURFACE_AGENT>", "<SURFACE_LABEL>", "<SURFACE_PATH>",
57
- "<SURFACE_TOOLS>", "<SURFACE_MODEL>", "<PROJECT_NAME>",
58
+ "<SURFACE_TOOLS>", "<SURFACE_MODEL>", "<PROJECT_NAME>", "<SURFACE_CONVENTIONS>",
58
59
  "<SURFACE_EXTRA_NEVER>", "<SURFACE_DESIGN_INPUT>", "<SURFACE_TDD_STEP1>"])
59
60
  if (!text.includes(ph)) fail(path, `render placeholder ${ph} disappeared`);
60
61
  continue;
@@ -89,7 +90,7 @@ for (const path of allDocs) {
89
90
  }
90
91
  for (const m of text.matchAll(/subagent_type:\s*(?:`|)([a-z-]+)(?:`|)/g)) {
91
92
  const t = m[1];
92
- if (["review", "release", "smoke"].includes(t)) continue;
93
+ if (["review", "release", "smoke", "profile-reader"].includes(t)) continue;
93
94
  if (t.startsWith("<")) continue; // <surface.agent> placeholder
94
95
  if (!existsSync(join(root, "core/agents", `${t}.md`)))
95
96
  fail(path, `dispatches subagent_type ${t} with no core/agents/${t}.md`);
@@ -146,6 +147,41 @@ for (const f of shipped.filter((f) => f.endsWith(".sh") && !shipped.includes(`${
146
147
  if (!src.includes(`scripts/${f}`) && !src.includes(`scripts\\${f}`))
147
148
  fail(name, `never copies scripts/${f} into pipeline/scripts/ (silent no-op at runtime)`);
148
149
 
150
+ // ── workflow scripts ────────────────────────────────────────────────────────
151
+ // core/workflows/*.js run inside the Claude Code Workflow runtime: an async
152
+ // function body with agent()/pipeline()/… injected, plus one `export const
153
+ // meta` line. Validate the syntax the same way the runtime parses it (plain
154
+ // `node --check` would reject the top-level return/await), and the invariants:
155
+ // a meta literal, phase 0 through profile-reader, and no Date.now()-family
156
+ // calls (they would break workflow resume).
157
+ const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
158
+ const workflowsDir = join(root, "core/workflows");
159
+ if (!existsSync(workflowsDir) || readdirSync(workflowsDir).length === 0)
160
+ fail("core/workflows", "workflow scripts missing/empty");
161
+ else for (const f of readdirSync(workflowsDir)) {
162
+ if (!f.endsWith(".js")) continue;
163
+ const path = `core/workflows/${f}`;
164
+ const text = read(path);
165
+ if (!/^export const meta = \{/m.test(text))
166
+ fail(path, "missing the `export const meta = {…}` literal");
167
+ if (!text.includes("agentType: 'profile-reader'"))
168
+ fail(path, "phase 0 must read the profile via the profile-reader agent");
169
+ if (/\bDate\.now\(\)|\bMath\.random\(\)|new Date\(\)/.test(text))
170
+ fail(path, "Date.now()/Math.random()/new Date() are unavailable in workflow scripts");
171
+ try {
172
+ new AsyncFunction("agent", "parallel", "pipeline", "phase", "log", "args",
173
+ "budget", "workflow", text.replace(/^export const meta/m, "const meta"));
174
+ } catch (e) {
175
+ fail(path, `does not parse as a workflow body: ${e.message}`);
176
+ }
177
+ }
178
+ // Both shell installers must copy the workflows dir (bin/cli.js copies by rule,
179
+ // covered by the ci.yml dry-run).
180
+ if (!installSh.includes("core/workflows"))
181
+ fail("install.sh", "does not copy core/workflows (copy_core)");
182
+ if (!installPs1.includes("core\\workflows"))
183
+ fail("install.ps1", "does not copy core\\workflows (Copy-Core)");
184
+
149
185
  // ── report ──────────────────────────────────────────────────────────────────
150
186
  if (errors.length) {
151
187
  console.error(`validate-core: ${errors.length} error(s)\n`);