cohorte 1.2.5 → 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 +58 -0
- package/README.md +44 -9
- package/bin/cli.js +13 -3
- 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
package/core/hooks/gate.py
CHANGED
|
@@ -16,6 +16,23 @@ A pattern matches a command segment when the segment *contains* the pattern
|
|
|
16
16
|
(after normalizing whitespace). `deny` wins over `ask`. If the config is missing
|
|
17
17
|
or unreadable, the hook stays silent (exit 0) and lets settings.json decide.
|
|
18
18
|
|
|
19
|
+
The hook fires for EVERY agent in the session — the lead, /build's implementers,
|
|
20
|
+
and subagents spawned by the Workflow runtime alike. Workflow subagents run in
|
|
21
|
+
acceptEdits regardless of the session's permission mode (their Write/Edit calls
|
|
22
|
+
are auto-approved), but acceptEdits does NOT auto-approve Bash or Task, so this
|
|
23
|
+
gate still sees and can block them. In bypassPermissions (headless `claude -p`,
|
|
24
|
+
dashboard actions) there is no human to answer a prompt, so every `ask` match is
|
|
25
|
+
escalated to a hard deny with the reason attached — a clear refusal beats a
|
|
26
|
+
prompt that can never be answered.
|
|
27
|
+
|
|
28
|
+
Two extra duties beyond Bash patterns:
|
|
29
|
+
|
|
30
|
+
- Phase gate (`preflight` block in gate-config.json): a Task dispatch of a
|
|
31
|
+
listed subagent_type (default review/smoke) requires a fresh
|
|
32
|
+
`.claude/preflight.ok` stamp, written by pipeline/scripts/preflight.sh when
|
|
33
|
+
typecheck+lint+tests are green. Stale/missing stamp => "ask" — dispatching
|
|
34
|
+
reviewers onto code that doesn't compile burns their whole run.
|
|
35
|
+
|
|
19
36
|
Protocol: reads the PreToolUse payload on stdin; emits a JSON permissionDecision
|
|
20
37
|
of "deny" or "ask" on a match; otherwise exits 0 silently.
|
|
21
38
|
"""
|
|
@@ -25,6 +42,7 @@ import os
|
|
|
25
42
|
import re
|
|
26
43
|
import subprocess
|
|
27
44
|
import sys
|
|
45
|
+
import time
|
|
28
46
|
|
|
29
47
|
SPLIT = re.compile(r"&&|\|\||[;|\n]")
|
|
30
48
|
WS = re.compile(r"\s+")
|
|
@@ -33,18 +51,24 @@ WS = re.compile(r"\s+")
|
|
|
33
51
|
def load_config() -> dict:
|
|
34
52
|
root = os.environ.get("CLAUDE_PROJECT_DIR", ".")
|
|
35
53
|
path = os.path.join(root, ".claude", "gate-config.json")
|
|
36
|
-
empty = {"deny": [], "ask": [], "ask_on_default_branch": [], "default_branch": "main"
|
|
54
|
+
empty = {"deny": [], "ask": [], "ask_on_default_branch": [], "default_branch": "main",
|
|
55
|
+
"preflight": {}}
|
|
37
56
|
try:
|
|
38
57
|
with open(path, "r", encoding="utf-8") as fh:
|
|
39
58
|
cfg = json.load(fh)
|
|
40
59
|
except Exception:
|
|
41
60
|
return empty
|
|
61
|
+
preflight = cfg.get("preflight")
|
|
62
|
+
if not isinstance(preflight, dict):
|
|
63
|
+
preflight = {}
|
|
42
64
|
return {
|
|
43
65
|
"deny": list(cfg.get("deny", [])),
|
|
44
66
|
"ask": list(cfg.get("ask", [])),
|
|
45
67
|
# Patterns gated ONLY on the default branch — allowed freely on feature branches.
|
|
46
68
|
"ask_on_default_branch": list(cfg.get("ask_on_default_branch", [])),
|
|
47
69
|
"default_branch": cfg.get("default_branch", "main") or "main",
|
|
70
|
+
# Phase gate: {"enabled": true, "agents": ["review","smoke"], "max_age_minutes": 30}
|
|
71
|
+
"preflight": preflight,
|
|
48
72
|
}
|
|
49
73
|
|
|
50
74
|
|
|
@@ -67,22 +91,83 @@ def current_branch():
|
|
|
67
91
|
return None
|
|
68
92
|
|
|
69
93
|
|
|
94
|
+
def current_head():
|
|
95
|
+
"""HEAD sha of CLAUDE_PROJECT_DIR, or None."""
|
|
96
|
+
root = os.environ.get("CLAUDE_PROJECT_DIR", ".")
|
|
97
|
+
try:
|
|
98
|
+
out = subprocess.run(
|
|
99
|
+
["git", "rev-parse", "HEAD"],
|
|
100
|
+
cwd=root, capture_output=True, text=True, timeout=3,
|
|
101
|
+
)
|
|
102
|
+
if out.returncode == 0:
|
|
103
|
+
return out.stdout.strip() or None
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def check_preflight(payload: dict, cfg: dict) -> int:
|
|
110
|
+
"""Phase gate on Task dispatches: review/smoke agents need a green preflight stamp."""
|
|
111
|
+
pf = cfg.get("preflight") or {}
|
|
112
|
+
if not pf.get("enabled"):
|
|
113
|
+
return 0
|
|
114
|
+
agents = pf.get("agents") or ["review", "smoke"]
|
|
115
|
+
subagent = (payload.get("tool_input") or {}).get("subagent_type", "") or ""
|
|
116
|
+
if subagent not in agents:
|
|
117
|
+
return 0
|
|
118
|
+
|
|
119
|
+
root = os.environ.get("CLAUDE_PROJECT_DIR", ".")
|
|
120
|
+
stamp = os.path.join(root, ".claude", "preflight.ok")
|
|
121
|
+
why = None
|
|
122
|
+
try:
|
|
123
|
+
with open(stamp, "r", encoding="utf-8") as fh:
|
|
124
|
+
epoch_s, _, sha = fh.read().strip().partition(" ")
|
|
125
|
+
age_min = (time.time() - float(epoch_s)) / 60
|
|
126
|
+
max_age = float(pf.get("max_age_minutes", 30) or 30)
|
|
127
|
+
if age_min > max_age:
|
|
128
|
+
why = f"the preflight stamp is {age_min:.0f} min old (max {max_age:.0f})"
|
|
129
|
+
else:
|
|
130
|
+
head = current_head()
|
|
131
|
+
if head and sha not in ("", "none") and head != sha:
|
|
132
|
+
why = "HEAD moved since the preflight ran"
|
|
133
|
+
except Exception:
|
|
134
|
+
why = "no preflight stamp found"
|
|
135
|
+
if why is None:
|
|
136
|
+
return 0
|
|
137
|
+
return decide(
|
|
138
|
+
"ask",
|
|
139
|
+
f"Phase gate: dispatching `{subagent}` but {why}. Run the deterministic pre-flight first "
|
|
140
|
+
f"(pipeline/scripts/preflight.sh — typecheck + lint + tests) so agents never review red code; "
|
|
141
|
+
f"or confirm to dispatch anyway (PIPELINE.md gate.preflight).",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
70
145
|
def main() -> int:
|
|
71
146
|
try:
|
|
72
147
|
payload = json.load(sys.stdin)
|
|
73
148
|
except Exception:
|
|
74
149
|
return 0 # malformed input → don't block
|
|
75
150
|
|
|
76
|
-
|
|
151
|
+
tool = payload.get("tool_name")
|
|
152
|
+
cfg = load_config()
|
|
153
|
+
|
|
154
|
+
if tool == "Task":
|
|
155
|
+
return check_preflight(payload, cfg)
|
|
156
|
+
if tool != "Bash":
|
|
77
157
|
return 0
|
|
78
158
|
|
|
79
159
|
command = (payload.get("tool_input") or {}).get("command", "") or ""
|
|
80
|
-
cfg = load_config()
|
|
81
160
|
deny, ask, branch_gated = cfg["deny"], cfg["ask"], cfg["ask_on_default_branch"]
|
|
82
161
|
default = cfg["default_branch"]
|
|
83
162
|
if not deny and not ask and not branch_gated:
|
|
84
163
|
return 0
|
|
85
164
|
|
|
165
|
+
# No human can answer a prompt in bypassPermissions (headless runs) — an "ask"
|
|
166
|
+
# there either hangs or silently auto-resolves, so escalate it to a clear deny.
|
|
167
|
+
unattended = payload.get("permission_mode") == "bypassPermissions"
|
|
168
|
+
ask_decision = "deny" if unattended else "ask"
|
|
169
|
+
ask_suffix = " (denied outright: unattended run, nobody to confirm)" if unattended else ""
|
|
170
|
+
|
|
86
171
|
# Branch-conditional patterns (e.g. git/docker) are gated only on the default branch;
|
|
87
172
|
# on a feature branch they run freely. Unknown branch (no repo / detached / no git) ⇒
|
|
88
173
|
# be conservative and gate. Resolve the branch once, lazily.
|
|
@@ -100,12 +185,16 @@ def main() -> int:
|
|
|
100
185
|
return decide("deny", f"`{pat}` is forbidden by the project's PIPELINE.md gate.")
|
|
101
186
|
for pat in ask:
|
|
102
187
|
if norm(pat) in seg:
|
|
103
|
-
return decide(
|
|
188
|
+
return decide(ask_decision,
|
|
189
|
+
f"`{pat}` is a gated command — confirm first (PIPELINE.md gate)."
|
|
190
|
+
f"{ask_suffix}")
|
|
104
191
|
if on_default:
|
|
105
192
|
for pat in branch_gated:
|
|
106
193
|
if norm(pat) in seg:
|
|
107
|
-
return decide(
|
|
108
|
-
|
|
194
|
+
return decide(ask_decision,
|
|
195
|
+
f"`{pat}` is gated on the default branch `{default}` — confirm "
|
|
196
|
+
f"(PIPELINE.md gate). It runs freely on feature branches."
|
|
197
|
+
f"{ask_suffix}")
|
|
109
198
|
|
|
110
199
|
return 0
|
|
111
200
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# HANDOFF — <surface> · `<feature_id>`
|
|
2
2
|
|
|
3
3
|
<!-- Keep it tight: the lead only acts on mismatches, test failures, remediation ticks, and TODOs.
|
|
4
|
-
Never list files one by one — the lead has `git diff --stat`.
|
|
4
|
+
Never list files one by one — the lead has `git diff --stat`. Never paste code excerpts —
|
|
5
|
+
a file:line reference is enough, the code is on disk. One line per item. -->
|
|
5
6
|
|
|
6
7
|
## Summary
|
|
7
8
|
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- Emitted-report shape. Everything in an HTML comment is guidance — do NOT emit it.
|
|
4
4
|
Verdict rules live in the review agent's instructions (SHIP = no CRITICAL/security;
|
|
5
|
-
REVISE = ≥1 CRITICAL; BLOCK = security vulnerability).
|
|
5
|
+
REVISE = ≥1 CRITICAL; BLOCK = security vulnerability).
|
|
6
|
+
CAPPED: max 20 findings, ONE line each (severity · file:line · type · concrete fix),
|
|
7
|
+
ZERO code excerpts — file:line is enough, the source is on disk. Overflow ⇒ keep all
|
|
8
|
+
CRITICAL/HIGH/security, fill by severity, close with one `+<n> more …` line. -->
|
|
6
9
|
|
|
7
10
|
feature_id: <feature_id>
|
|
8
11
|
Feature branch: <feature_branch_prefix><feature_id>
|
|
@@ -15,6 +15,13 @@ Ask ONLY what you couldn't confidently detect. Batch related questions. Cover:
|
|
|
15
15
|
per SCHEMA.md §Specialization. If the human accepts, apply the rules: **shared code (routing, global
|
|
16
16
|
state, DS kit/tokens) becomes its own single-owner surface**, and cross-slice shapes go through the
|
|
17
17
|
contract. Default to NOT splitting when boundaries are tangled or slices are tiny — coarse is fine.
|
|
18
|
+
- **Quiet commands** (never store a bare `pnpm test` as what agents execute) — for each noisy command
|
|
19
|
+
(tests, lint; per surface AND repo-wide), propose the detected **bridled variant** as the
|
|
20
|
+
Recommended option: dot/failures-only reporter (`--reporter=dot` vitest/playwright, `--silent`
|
|
21
|
+
jest, `-q` pytest, `--quiet` eslint/ruff — whatever the detected runner supports). These land in
|
|
22
|
+
`test_quiet_cmd`/`lint_quiet_cmd` + `commands.test_quiet`/`lint_quiet` and are what agents and the
|
|
23
|
+
`/review`·`/smoke` pre-flight actually run (SCHEMA.md §Output discipline). If the human declines or
|
|
24
|
+
the runner has no such flag, leave `""` — consumers then fall back to `<cmd> 2>&1 | tail -40`.
|
|
18
25
|
- **Contract** — mechanism (`shared-types-zod` / `openapi` / `protobuf` / `json-schema` / `none`) and
|
|
19
26
|
where feature contracts are authored. If `none`, surfaces sync by the spec prose alone.
|
|
20
27
|
- **UI language** — language of all user-facing copy.
|
|
@@ -9,18 +9,26 @@
|
|
|
9
9
|
3. **Render one agent per surface** — for each surface, follow SCHEMA.md §"Rendering / reconciling a
|
|
10
10
|
surface agent" (steps 2–3): render `.claude/agents/<agent>.md` from the installer's
|
|
11
11
|
`pipeline/implementer.template.md`, substituting `<SURFACE_AGENT>`, `<SURFACE_LABEL>`, `<SURFACE_PATH>`,
|
|
12
|
-
`<SURFACE_TOOLS>`, `<SURFACE_MODEL>`, `<PROJECT_NAME>`,
|
|
12
|
+
`<SURFACE_TOOLS>`, `<SURFACE_MODEL>`, `<PROJECT_NAME>`, `<SURFACE_CONVENTIONS>` (the surface's
|
|
13
|
+
baked convention slice — §Shared + its `### Surface:` stanza + its §Testing lines from the
|
|
14
|
+
PIPELINE.md you just wrote), and the surface-specific blocks
|
|
13
15
|
(`<SURFACE_EXTRA_NEVER>`, `<SURFACE_DESIGN_INPUT>`, `<SURFACE_TDD_STEP1>` — fill design-related ones
|
|
14
16
|
only when `uses_design`).
|
|
15
|
-
Leave `review.md` + `release.md` as-is (generic).
|
|
16
|
-
4. **Generate `.claude/gate-config.json`** from the `gate` block — copy all
|
|
17
|
-
`{"deny": [...], "ask": [...], "ask_on_default_branch": [...], "default_branch": "<vcs.default_branch>"
|
|
17
|
+
Leave `review.md` + `release.md` + `profile-reader.md` as-is (generic).
|
|
18
|
+
4. **Generate `.claude/gate-config.json`** from the `gate` block — copy all five keys verbatim:
|
|
19
|
+
`{"deny": [...], "ask": [...], "ask_on_default_branch": [...], "default_branch": "<vcs.default_branch>",
|
|
20
|
+
"preflight": {"enabled": <gate.preflight.enabled>, "agents": [...], "max_age_minutes": <n>}}`
|
|
21
|
+
(profile has no `preflight` block ⇒ omit the key — the hook then skips the phase gate).
|
|
18
22
|
5. **Write `.claude/settings.json`** permissions (`ask`/`deny` lists mirroring the gate, **plus an
|
|
19
23
|
`allow` list of the project's read-only / verification commands** so agents don't stall on
|
|
20
|
-
permission prompts: the detected
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
`
|
|
24
|
+
permission prompts — including mid-workflow, where nobody is watching a prompt: the detected
|
|
25
|
+
per-surface `test_cmd`/`lint_cmd`/`typecheck_cmd`/`build_cmd` **and their `*_quiet_cmd`
|
|
26
|
+
variants** and repo-wide `commands.*` equivalents as `Bash(<cmd>:*)` rules, plus read-only git —
|
|
27
|
+
`Bash(git status:*)`, `Bash(git diff:*)`, `Bash(git log:*)`, `Bash(git rev-parse:*)` — plus the
|
|
28
|
+
shipped pipeline scripts for BOTH cores (`Bash(.claude/pipeline/scripts/:*)` and
|
|
29
|
+
`Bash(~/.claude/pipeline/scripts/:*)` — preflight, kanban-move, telemetry-send), and the
|
|
30
|
+
retrieval provider's MCP tools when wired (e.g. `mcp__serena`). Never allowlist anything matching
|
|
31
|
+
a `gate.ask`/`gate.deny` pattern. Mention the human can widen it later with
|
|
24
32
|
`/fewer-permission-prompts`) + the hooks, **conditioned on the install mode:**
|
|
25
33
|
- **bundled:** register the PreToolUse `Bash` hook `.claude/hooks/gate.py` and the PostToolUse
|
|
26
34
|
formatter (detected formatter).
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// cohorte — /audit as a deterministic workflow (opt-in; the conversational
|
|
2
|
+
// /audit command remains the default path and the fallback).
|
|
3
|
+
//
|
|
4
|
+
// Invoke with args = {target: "<path or domain>"} (optional — default whole repo).
|
|
5
|
+
//
|
|
6
|
+
// Shape (SCHEMA.md §Workflows): profile via profile-reader (phase 0), the
|
|
7
|
+
// mechanical gates staged to disk by one haiku agent, then ONE auditor per
|
|
8
|
+
// domain (each surface + `shared`) running concurrently — the runtime caps
|
|
9
|
+
// concurrency at ~16, extra domains queue — and a merge phase that writes the
|
|
10
|
+
// prioritized specs/refactor-backlog.md. Only the summary comes back.
|
|
11
|
+
|
|
12
|
+
export const meta = {
|
|
13
|
+
name: 'cohorte-audit',
|
|
14
|
+
description: 'Audit the codebase against PIPELINE.md: mechanical gates, one auditor per domain in parallel, prioritized refactor backlog',
|
|
15
|
+
whenToUse: 'Only when the human explicitly asks for the audit workflow. args = {target: "<path or domain>"} — omit for the whole repo.',
|
|
16
|
+
phases: [
|
|
17
|
+
{ title: 'Profile', detail: 'PIPELINE.md → JSON via profile-reader', model: 'haiku' },
|
|
18
|
+
{ title: 'Gates', detail: 'format/lint/typecheck/tests → specs/reports/audit-gates.txt', model: 'haiku' },
|
|
19
|
+
{ title: 'Audit', detail: 'one review-in-audit-mode agent per domain (concurrent, runtime-capped ~16)' },
|
|
20
|
+
{ title: 'Backlog', detail: 'merge + write specs/refactor-backlog.md', model: 'haiku' },
|
|
21
|
+
],
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const target = (typeof args === 'string' ? args.trim() : args && args.target) || ''
|
|
25
|
+
|
|
26
|
+
const PROFILE = { type: 'object', additionalProperties: true }
|
|
27
|
+
|
|
28
|
+
const GATES = {
|
|
29
|
+
type: 'object', required: ['failures'], additionalProperties: false,
|
|
30
|
+
properties: {
|
|
31
|
+
failures: {
|
|
32
|
+
type: 'array', maxItems: 40,
|
|
33
|
+
items: {
|
|
34
|
+
type: 'object', required: ['file', 'line', 'kind', 'summary'], additionalProperties: false,
|
|
35
|
+
properties: {
|
|
36
|
+
file: { type: 'string' }, line: { type: 'integer' },
|
|
37
|
+
kind: { enum: ['lint', 'format', 'type', 'test'] },
|
|
38
|
+
summary: { type: 'string', description: 'one line, no output excerpts' },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
overflow: { type: 'integer', description: 'failures beyond the 40-item cap' },
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const BACKLOG = {
|
|
47
|
+
type: 'object', required: ['items'], additionalProperties: false,
|
|
48
|
+
properties: {
|
|
49
|
+
items: {
|
|
50
|
+
type: 'array', maxItems: 30,
|
|
51
|
+
items: {
|
|
52
|
+
type: 'object', required: ['severity', 'file', 'line', 'kind', 'fix'], additionalProperties: false,
|
|
53
|
+
properties: {
|
|
54
|
+
severity: { enum: ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] },
|
|
55
|
+
file: { type: 'string' }, line: { type: 'integer' },
|
|
56
|
+
kind: { enum: ['rule', 'tdd', 'lint', 'format', 'type', 'security'] },
|
|
57
|
+
fix: { type: 'string', description: 'one concrete change, one line, no code excerpts' },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
overflow: { type: 'integer' },
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Phase 0 — profile ────────────────────────────────────────────────────────
|
|
66
|
+
phase('Profile')
|
|
67
|
+
const profile = await agent(
|
|
68
|
+
'Return this project\'s PIPELINE.md `yaml pipeline-profile` block as JSON, per your instructions.',
|
|
69
|
+
{ agentType: 'profile-reader', label: 'profile', schema: PROFILE, effort: 'low' },
|
|
70
|
+
)
|
|
71
|
+
if (!profile || profile.error) {
|
|
72
|
+
return { error: `profile unreadable: ${(profile && profile.error) || 'profile-reader returned nothing'}` }
|
|
73
|
+
}
|
|
74
|
+
const cmds = profile.commands || {}
|
|
75
|
+
const surfaces = Array.isArray(profile.surfaces) ? profile.surfaces : []
|
|
76
|
+
const quiet = (q, full) => (q && !String(q).startsWith('<') ? q : full ? `${full} 2>&1 | tail -40` : '')
|
|
77
|
+
const scope = target || 'the whole repo'
|
|
78
|
+
|
|
79
|
+
// ── Phase 1 — mechanical gates, staged to disk ───────────────────────────────
|
|
80
|
+
phase('Gates')
|
|
81
|
+
const gateCmds = [
|
|
82
|
+
cmds.format ? `${cmds.format} --check` : '',
|
|
83
|
+
quiet(cmds.lint_quiet, cmds.lint),
|
|
84
|
+
cmds.typecheck,
|
|
85
|
+
quiet(cmds.test_quiet, cmds.test),
|
|
86
|
+
].filter(c => c && !String(c).startsWith('<'))
|
|
87
|
+
const gates = await agent(
|
|
88
|
+
`Run the cohorte audit's mechanical gates, scoped to ${scope}. Commands (adapt the format one to the ` +
|
|
89
|
+
`project's check mode if --check is wrong): ${gateCmds.map(c => JSON.stringify(c)).join(' · ')}.\n` +
|
|
90
|
+
'Redirect EVERY command\'s output into specs/reports/audit-gates.txt (append, `cmd >> file 2>&1`) — ' +
|
|
91
|
+
'never print it — and keep going after failures (this is an inventory, not a gate to pass). Then grep ' +
|
|
92
|
+
'the file and return each failure as file/line/kind/one-line summary, capped at 40 (set overflow for the rest).',
|
|
93
|
+
{ model: 'haiku', label: 'gates', schema: GATES, effort: 'low' },
|
|
94
|
+
)
|
|
95
|
+
const mech = (gates && gates.failures) || []
|
|
96
|
+
log(`Mechanical failures: ${mech.length}${gates && gates.overflow ? ` (+${gates.overflow} overflow)` : ''}`)
|
|
97
|
+
|
|
98
|
+
// ── Phase 2 — one auditor per domain, concurrent ─────────────────────────────
|
|
99
|
+
// Domains = every surface + `shared` (contract package + anything outside the
|
|
100
|
+
// surface trees). The runtime caps concurrent agents (~16); more domains queue.
|
|
101
|
+
phase('Audit')
|
|
102
|
+
const domains = surfaces.map(s => ({ key: s.key, path: s.path }))
|
|
103
|
+
.concat([{ key: 'shared', path: (profile.contract && profile.contract.path) || '(everything outside the surface trees)' }])
|
|
104
|
+
.filter(d => !target || target === d.key || String(d.path).startsWith(target) || target.startsWith(String(d.path)))
|
|
105
|
+
if (!domains.length) return { error: `target "${target}" matches no surface/domain` }
|
|
106
|
+
|
|
107
|
+
const audited = await parallel(domains.map(d => () => agent(
|
|
108
|
+
'Audit a target against PIPELINE.md (no spec — audit mode, per your agent instructions). Check ' +
|
|
109
|
+
'conventions for the domain\'s surface, TDD coverage (untested entry points / modules), and — if the ' +
|
|
110
|
+
'profile enables them — mobile-first + design-system usage. Mechanical findings are already staged: ' +
|
|
111
|
+
'read specs/reports/audit-gates.txt and fold the ones in your domain in. Return the prioritized items, ' +
|
|
112
|
+
'capped at 30, one line each, no code excerpts. — Variable slots: domain: ' +
|
|
113
|
+
`${d.key} · tree: ${d.path}${target ? ` · human-requested target: ${target}` : ''}`,
|
|
114
|
+
{ agentType: 'review', label: `audit:${d.key}`, schema: BACKLOG },
|
|
115
|
+
).then(r => r && { key: d.key, items: r.items, overflow: r.overflow || 0 })))
|
|
116
|
+
const perDomain = audited.filter(Boolean)
|
|
117
|
+
|
|
118
|
+
// ── Phase 3 — merge + write the backlog ──────────────────────────────────────
|
|
119
|
+
phase('Backlog')
|
|
120
|
+
const SEV = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 }
|
|
121
|
+
const body = ['# Refactor backlog', '', `> Generated by the cohorte-audit workflow (scope: ${scope}).`]
|
|
122
|
+
let total = 0
|
|
123
|
+
for (const d of perDomain) {
|
|
124
|
+
const items = [...d.items].sort((a, b) => SEV[a.severity] - SEV[b.severity])
|
|
125
|
+
total += items.length
|
|
126
|
+
body.push('', `## ${d.key}`, '')
|
|
127
|
+
for (const it of items) body.push(`- [ ] ${it.severity} · ${it.file}:${it.line} · ${it.kind} · ${it.fix}`)
|
|
128
|
+
if (d.overflow) body.push(`- [ ] (+${d.overflow} more beyond the cap — re-audit ${d.key} after this pass)`)
|
|
129
|
+
}
|
|
130
|
+
await agent(
|
|
131
|
+
`Write EXACTLY this content to specs/refactor-backlog.md (overwrite), then return the single word done:\n<<<BACKLOG\n${body.join('\n')}\nBACKLOG`,
|
|
132
|
+
{ model: 'haiku', label: 'write-backlog', effort: 'low' },
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
backlog: 'specs/refactor-backlog.md',
|
|
137
|
+
mechanicalFailures: mech.length,
|
|
138
|
+
domains: Object.fromEntries(perDomain.map(d => [d.key, d.items.length + (d.overflow || 0)])),
|
|
139
|
+
total,
|
|
140
|
+
top: perDomain.flatMap(d => d.items.map(it => ({ ...it, domain: d.key })))
|
|
141
|
+
.sort((a, b) => SEV[a.severity] - SEV[b.severity]).slice(0, 10)
|
|
142
|
+
.map(it => `[${it.severity}] ${it.domain} · ${it.file}:${it.line} — ${it.fix}`),
|
|
143
|
+
next: 'refactor a domain with /refactor <domain> (or the refactor workflow for big domains)',
|
|
144
|
+
}
|