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.
- package/CHANGELOG.md +50 -0
- package/README.md +44 -9
- package/bin/cli.js +4 -1
- package/core/agents/implementer.template.md +27 -14
- package/core/agents/profile-reader.md +28 -0
- package/core/agents/review.md +7 -3
- package/core/agents/smoke.md +4 -2
- package/core/commands/audit.md +17 -9
- package/core/commands/doctor.md +21 -5
- package/core/commands/refactor.md +8 -3
- package/core/commands/review.md +46 -18
- package/core/commands/smoke.md +23 -9
- package/core/commands/update-pipeline.md +10 -4
- package/core/hooks/gate.py +95 -6
- package/core/templates/agent-handoff.md +2 -1
- package/core/templates/review-feedback.md +4 -1
- package/core/templates/steps/init-pipeline/02-interview-gaps.md +7 -0
- package/core/templates/steps/init-pipeline/04-write-render.md +16 -8
- package/core/workflows/audit.js +144 -0
- package/core/workflows/cycle.js +397 -0
- package/core/workflows/refactor.js +187 -0
- package/core/workflows/review.js +215 -0
- package/dashboard/dist/assets/{index-YvkzH-yF.js → index-BxgA_mz1.js} +10 -10
- package/dashboard/dist/index.html +1 -1
- package/dashboard/server/doctor.js +35 -1
- package/dashboard/server/index.js +4 -2
- package/install.ps1 +4 -1
- package/install.sh +5 -2
- package/package.json +1 -1
- package/profile/PIPELINE.template.md +17 -0
- package/profile/SCHEMA.md +113 -1
- package/scripts/preflight.sh +48 -0
- package/scripts/validate-core.mjs +39 -3
|
@@ -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-
|
|
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
|
|
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')
|
|
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
|
-
|
|
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.
|
|
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`).
|
|
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`);
|