cohorte 1.3.4 → 1.5.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 +110 -0
- package/README.md +36 -16
- package/bin/cli.js +34 -4
- package/core/agents/profile-reader.md +22 -0
- package/core/commands/build.md +4 -4
- package/core/commands/doctor.md +10 -8
- package/core/commands/fix.md +5 -5
- package/core/commands/loop.md +61 -0
- package/core/commands/review.md +38 -5
- package/core/hooks/gate.py +4 -4
- package/core/templates/spec.template.md +1 -1
- package/core/templates/steps/init-pipeline/02-interview-gaps.md +1 -1
- package/core/templates/steps/init-pipeline/04-write-render.md +8 -4
- package/core/workflows/audit.js +68 -4
- package/core/workflows/refactor.js +69 -4
- package/core/workflows/review.js +73 -4
- package/dashboard/dist/assets/index-8owBnqyv.js +43 -0
- package/dashboard/dist/assets/{index-AFQnlfjO.css → index-dkO8UUVl.css} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dashboard/server/doctor.js +15 -5
- package/dashboard/server/index.js +7 -0
- package/dashboard/server/metrics.js +6 -5
- package/dashboard/server/usage.js +61 -0
- package/install.ps1 +4 -1
- package/install.sh +5 -2
- package/package.json +1 -1
- package/profile/PIPELINE.template.md +3 -3
- package/profile/SCHEMA.md +21 -44
- package/scripts/loop.sh +189 -0
- package/scripts/metrics/collect.mjs +504 -0
- package/scripts/metrics/prices.json +39 -0
- package/scripts/preflight.sh +2 -2
- package/scripts/telemetry-send.sh +5 -2
- package/scripts/test-dashboard.mjs +29 -3
- package/scripts/test-gate.mjs +1 -2
- package/scripts/test-metrics.mjs +144 -0
- package/scripts/test-workflows.mjs +56 -178
- package/scripts/validate-core.mjs +10 -9
- package/core/agents/smoke.md +0 -63
- package/core/commands/cycle.md +0 -61
- package/core/commands/smoke.md +0 -55
- package/core/workflows/cycle.js +0 -513
- package/dashboard/dist/assets/index-DLBzciIC.js +0 -43
|
@@ -20,7 +20,7 @@ Ask ONLY what you couldn't confidently detect. Batch related questions. Cover:
|
|
|
20
20
|
Recommended option: dot/failures-only reporter (`--reporter=dot` vitest/playwright, `--silent`
|
|
21
21
|
jest, `-q` pytest, `--quiet` eslint/ruff — whatever the detected runner supports). These land in
|
|
22
22
|
`test_quiet_cmd`/`lint_quiet_cmd` + `commands.test_quiet`/`lint_quiet` and are what agents and the
|
|
23
|
-
`/review
|
|
23
|
+
`/review` pre-flight actually run (SCHEMA.md §Output discipline). If the human declines or
|
|
24
24
|
the runner has no such flag, leave `""` — consumers then fall back to `<cmd> 2>&1 | tail -40`.
|
|
25
25
|
- **Contract** — mechanism (`shared-types-zod` / `openapi` / `protobuf` / `json-schema` / `none`) and
|
|
26
26
|
where feature contracts are authored. If `none`, surfaces sync by the spec prose alone.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
(`<SURFACE_EXTRA_NEVER>`, `<SURFACE_DESIGN_INPUT>`, `<SURFACE_TDD_STEP1>` — fill design-related ones
|
|
16
16
|
only when `uses_design`).
|
|
17
17
|
Leave the fixed agents as-is (generic, shipped by the installer): `review.md`, `release.md`,
|
|
18
|
-
`
|
|
18
|
+
`profile-reader.md`.
|
|
19
19
|
4. **Generate `.claude/gate-config.json`** from the `gate` block — copy all five keys verbatim:
|
|
20
20
|
`{"deny": [...], "ask": [...], "ask_on_default_branch": [...], "default_branch": "<vcs.default_branch>",
|
|
21
21
|
"preflight": {"enabled": <gate.preflight.enabled>, "agents": [...], "max_age_minutes": <n>}}`
|
|
@@ -27,7 +27,11 @@
|
|
|
27
27
|
variants** and repo-wide `commands.*` equivalents as `Bash(<cmd>:*)` rules, plus read-only git —
|
|
28
28
|
`Bash(git status:*)`, `Bash(git diff:*)`, `Bash(git log:*)`, `Bash(git rev-parse:*)` — plus the
|
|
29
29
|
shipped pipeline scripts for BOTH cores (`Bash(.claude/pipeline/scripts/:*)` and
|
|
30
|
-
`Bash(~/.claude/pipeline/scripts/:*)` — preflight, kanban-move, telemetry-send)
|
|
30
|
+
`Bash(~/.claude/pipeline/scripts/:*)` — preflight, kanban-move, telemetry-send) **plus the
|
|
31
|
+
`bash`-prefixed form the `/loop` driver uses** (`Bash(bash .claude/pipeline/scripts/loop.sh:*)`
|
|
32
|
+
and `Bash(bash ~/.claude/pipeline/scripts/loop.sh:*)`) — those prefix rules match a command
|
|
33
|
+
*starting* with the path, so `bash <path>` needs its own entry or `/loop` stalls on a permission
|
|
34
|
+
prompt at every launch, and the
|
|
31
35
|
retrieval provider's MCP tools when wired (e.g. `mcp__serena`). Never allowlist anything matching
|
|
32
36
|
a `gate.ask`/`gate.deny` pattern. Mention the human can widen it later with
|
|
33
37
|
`/fewer-permission-prompts`) + the hooks, **conditioned on the install mode:**
|
|
@@ -86,8 +90,8 @@
|
|
|
86
90
|
`build_cmd`s that are non-empty). Derive the setup steps from the detected stack — mirror what a
|
|
87
91
|
sibling workflow does if one exists. `/ship` watches these checks before the merge.
|
|
88
92
|
11. **Metrics sink & report buffer:** add `.claude/pipeline-metrics.jsonl` to `.gitignore` — `/build`,
|
|
89
|
-
`/review
|
|
90
|
-
Also add `specs/reports/` — `/review`
|
|
93
|
+
`/review` and `/fix` append per-dispatch evidence there (SCHEMA §Specialization reads it).
|
|
94
|
+
Also add `specs/reports/` — `/review` stages its last report there so a `/fix` (or
|
|
91
95
|
`/spec` Mode B) survives a `/clear`; it's a derived buffer, not a versioned artifact.
|
|
92
96
|
12. **Design system:** if `design.enabled` with a snapshot dir, note that `/align-ds` is active; else the
|
|
93
97
|
`/align-ds` command will no-op with a clear message.
|
package/core/workflows/audit.js
CHANGED
|
@@ -21,9 +21,67 @@ export const meta = {
|
|
|
21
21
|
],
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
// The Workflow runtime hands `args` to a script verbatim, so a caller that passes a
|
|
25
|
+
// JSON-ENCODED STRING instead of a real object gets that string back here. The old
|
|
26
|
+
// `typeof args === 'string' ? args.trim()` then took the whole blob as the value — which
|
|
27
|
+
// is how a report landed on disk named `specs/reports/{"feature": "x"}.md`, and how
|
|
28
|
+
// maxRounds/smoke were silently dropped on the same run. Parse it back into the object
|
|
29
|
+
// it was meant to be; a bare slug stays valid shorthand.
|
|
30
|
+
const ARGS = (() => {
|
|
31
|
+
if (typeof args === 'string') {
|
|
32
|
+
const t = args.trim()
|
|
33
|
+
if (t.startsWith('{')) {
|
|
34
|
+
try { const o = JSON.parse(t); if (o && typeof o === 'object' && !Array.isArray(o)) return o } catch {}
|
|
35
|
+
}
|
|
36
|
+
return { feature: t, target: t }
|
|
37
|
+
}
|
|
38
|
+
return args && typeof args === 'object' ? args : {}
|
|
39
|
+
})()
|
|
40
|
+
const target = ARGS.target || ''
|
|
25
41
|
|
|
26
|
-
|
|
42
|
+
// The profile-reader returns through a StructuredOutput tool call, and a haiku agent
|
|
43
|
+
// intermittently nests the whole profile as a JSON *string* under a single wrapper field
|
|
44
|
+
// ({"output": "{\"surfaces\": …}"}) instead of putting the profile's keys at the top level.
|
|
45
|
+
// The schema here used to be {type:'object', additionalProperties:true} — no declared
|
|
46
|
+
// properties, no required keys — so that wrapper validated cleanly and every field then read
|
|
47
|
+
// as undefined: `surfaces` fell back to [], parallel([]) dispatched zero agents, the
|
|
48
|
+
// dead-agent guard had no surfaces to find missing, and the run reported a verdict having
|
|
49
|
+
// done nothing. On the surface it is indistinguishable from a clean run with an empty diff.
|
|
50
|
+
// Declaring the shape gives the tool layer something to validate and the agent something to
|
|
51
|
+
// aim at; unwrapProfile() salvages a wrapped return that still gets through; and the
|
|
52
|
+
// zero-surface abort below makes the silent-success path impossible either way.
|
|
53
|
+
// See also the structured-output section of core/agents/profile-reader.md.
|
|
54
|
+
const PROFILE = {
|
|
55
|
+
type: 'object', additionalProperties: true,
|
|
56
|
+
properties: {
|
|
57
|
+
error: { type: 'string', description: 'set ONLY when PIPELINE.md is missing or unparseable' },
|
|
58
|
+
surfaces: {
|
|
59
|
+
type: 'array',
|
|
60
|
+
description: "one entry per surface, at the TOP LEVEL of this object — never a JSON string",
|
|
61
|
+
items: {
|
|
62
|
+
type: 'object', required: ['key'], additionalProperties: true,
|
|
63
|
+
properties: { key: { type: 'string' }, path: { type: 'string' }, agent: { type: 'string' } },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Salvage a profile handed back as JSON text rather than as an object — either the whole
|
|
70
|
+
// return, or nested under a single wrapper field. Anything already shaped like a profile
|
|
71
|
+
// (has `surfaces`, or is the documented `{error}` failure shape) passes through untouched.
|
|
72
|
+
const unwrapProfile = p => {
|
|
73
|
+
if (typeof p === 'string') { try { return JSON.parse(p) } catch { return null } }
|
|
74
|
+
if (!p || typeof p !== 'object') return null
|
|
75
|
+
if (Array.isArray(p.surfaces) || p.error) return p
|
|
76
|
+
for (const v of Object.values(p)) {
|
|
77
|
+
if (typeof v !== 'string') continue
|
|
78
|
+
try {
|
|
79
|
+
const inner = JSON.parse(v)
|
|
80
|
+
if (inner && typeof inner === 'object' && !Array.isArray(inner)) return inner
|
|
81
|
+
} catch {}
|
|
82
|
+
}
|
|
83
|
+
return p
|
|
84
|
+
}
|
|
27
85
|
|
|
28
86
|
const GATES = {
|
|
29
87
|
type: 'object', required: ['failures'], additionalProperties: false,
|
|
@@ -64,15 +122,21 @@ const BACKLOG = {
|
|
|
64
122
|
|
|
65
123
|
// ── Phase 0 — profile ────────────────────────────────────────────────────────
|
|
66
124
|
phase('Profile')
|
|
67
|
-
const profile = await agent(
|
|
125
|
+
const profile = unwrapProfile(await agent(
|
|
68
126
|
'Return this project\'s PIPELINE.md `yaml pipeline-profile` block as JSON, per your instructions.',
|
|
69
127
|
{ agentType: 'profile-reader', label: 'profile', schema: PROFILE, effort: 'low' },
|
|
70
|
-
)
|
|
128
|
+
))
|
|
71
129
|
if (!profile || profile.error) {
|
|
72
130
|
return { error: `profile unreadable: ${(profile && profile.error) || 'profile-reader returned nothing'}` }
|
|
73
131
|
}
|
|
74
132
|
const cmds = profile.commands || {}
|
|
75
133
|
const surfaces = Array.isArray(profile.surfaces) ? profile.surfaces : []
|
|
134
|
+
// A profile with no surfaces cannot do this workflow's work, and every later
|
|
135
|
+
// guard compares against `surfaces` — an empty list makes them all vacuously
|
|
136
|
+
// pass. Fail loudly here instead of finishing with nothing done.
|
|
137
|
+
if (!surfaces.length) {
|
|
138
|
+
return { error: 'profile has no surfaces — nothing would be audited. the `yaml pipeline-profile` block in PIPELINE.md is empty or unparseable, or the profile-reader mis-returned; run /doctor' }
|
|
139
|
+
}
|
|
76
140
|
const quiet = (q, full) => (q && !String(q).startsWith('<') ? q : full ? `${full} 2>&1 | tail -40` : '')
|
|
77
141
|
const scope = target || 'the whole repo'
|
|
78
142
|
|
|
@@ -29,13 +29,72 @@ export const meta = {
|
|
|
29
29
|
// /refactor handles it with less overhead than a workflow run.
|
|
30
30
|
const MIN_ITEMS = 5
|
|
31
31
|
|
|
32
|
+
// The Workflow runtime hands `args` to a script verbatim, so a caller that passes a
|
|
33
|
+
// JSON-ENCODED STRING instead of a real object gets that string back here. The old
|
|
34
|
+
// `typeof args === 'string' ? args.trim()` then took the whole blob as the value — which
|
|
35
|
+
// is how a report landed on disk named `specs/reports/{"feature": "x"}.md`, and how
|
|
36
|
+
// maxRounds/smoke were silently dropped on the same run. Parse it back into the object
|
|
37
|
+
// it was meant to be; a bare slug stays valid shorthand.
|
|
38
|
+
const ARGS = (() => {
|
|
39
|
+
if (typeof args === 'string') {
|
|
40
|
+
const t = args.trim()
|
|
41
|
+
if (t.startsWith('{')) {
|
|
42
|
+
try { const o = JSON.parse(t); if (o && typeof o === 'object' && !Array.isArray(o)) return o } catch {}
|
|
43
|
+
}
|
|
44
|
+
return { feature: t, target: t }
|
|
45
|
+
}
|
|
46
|
+
return args && typeof args === 'object' ? args : {}
|
|
47
|
+
})()
|
|
48
|
+
|
|
32
49
|
const wanted = (() => {
|
|
33
|
-
const d =
|
|
50
|
+
const d = ARGS.domains
|
|
34
51
|
if (!d || d === 'all') return 'all'
|
|
35
52
|
return Array.isArray(d) ? d : [String(d)]
|
|
36
53
|
})()
|
|
37
54
|
|
|
38
|
-
|
|
55
|
+
// The profile-reader returns through a StructuredOutput tool call, and a haiku agent
|
|
56
|
+
// intermittently nests the whole profile as a JSON *string* under a single wrapper field
|
|
57
|
+
// ({"output": "{\"surfaces\": …}"}) instead of putting the profile's keys at the top level.
|
|
58
|
+
// The schema here used to be {type:'object', additionalProperties:true} — no declared
|
|
59
|
+
// properties, no required keys — so that wrapper validated cleanly and every field then read
|
|
60
|
+
// as undefined: `surfaces` fell back to [], parallel([]) dispatched zero agents, the
|
|
61
|
+
// dead-agent guard had no surfaces to find missing, and the run reported a verdict having
|
|
62
|
+
// done nothing. On the surface it is indistinguishable from a clean run with an empty diff.
|
|
63
|
+
// Declaring the shape gives the tool layer something to validate and the agent something to
|
|
64
|
+
// aim at; unwrapProfile() salvages a wrapped return that still gets through; and the
|
|
65
|
+
// zero-surface abort below makes the silent-success path impossible either way.
|
|
66
|
+
// See also the structured-output section of core/agents/profile-reader.md.
|
|
67
|
+
const PROFILE = {
|
|
68
|
+
type: 'object', additionalProperties: true,
|
|
69
|
+
properties: {
|
|
70
|
+
error: { type: 'string', description: 'set ONLY when PIPELINE.md is missing or unparseable' },
|
|
71
|
+
surfaces: {
|
|
72
|
+
type: 'array',
|
|
73
|
+
description: "one entry per surface, at the TOP LEVEL of this object — never a JSON string",
|
|
74
|
+
items: {
|
|
75
|
+
type: 'object', required: ['key'], additionalProperties: true,
|
|
76
|
+
properties: { key: { type: 'string' }, path: { type: 'string' }, agent: { type: 'string' } },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Salvage a profile handed back as JSON text rather than as an object — either the whole
|
|
83
|
+
// return, or nested under a single wrapper field. Anything already shaped like a profile
|
|
84
|
+
// (has `surfaces`, or is the documented `{error}` failure shape) passes through untouched.
|
|
85
|
+
const unwrapProfile = p => {
|
|
86
|
+
if (typeof p === 'string') { try { return JSON.parse(p) } catch { return null } }
|
|
87
|
+
if (!p || typeof p !== 'object') return null
|
|
88
|
+
if (Array.isArray(p.surfaces) || p.error) return p
|
|
89
|
+
for (const v of Object.values(p)) {
|
|
90
|
+
if (typeof v !== 'string') continue
|
|
91
|
+
try {
|
|
92
|
+
const inner = JSON.parse(v)
|
|
93
|
+
if (inner && typeof inner === 'object' && !Array.isArray(inner)) return inner
|
|
94
|
+
} catch {}
|
|
95
|
+
}
|
|
96
|
+
return p
|
|
97
|
+
}
|
|
39
98
|
|
|
40
99
|
const OPEN = {
|
|
41
100
|
type: 'object', required: ['domains'], additionalProperties: false,
|
|
@@ -65,14 +124,20 @@ const VERIFY = {
|
|
|
65
124
|
|
|
66
125
|
// ── Phase 0 — profile ────────────────────────────────────────────────────────
|
|
67
126
|
phase('Profile')
|
|
68
|
-
const profile = await agent(
|
|
127
|
+
const profile = unwrapProfile(await agent(
|
|
69
128
|
'Return this project\'s PIPELINE.md `yaml pipeline-profile` block as JSON, per your instructions.',
|
|
70
129
|
{ agentType: 'profile-reader', label: 'profile', schema: PROFILE, effort: 'low' },
|
|
71
|
-
)
|
|
130
|
+
))
|
|
72
131
|
if (!profile || profile.error) {
|
|
73
132
|
return { error: `profile unreadable: ${(profile && profile.error) || 'profile-reader returned nothing'}` }
|
|
74
133
|
}
|
|
75
134
|
const surfaces = Array.isArray(profile.surfaces) ? profile.surfaces : []
|
|
135
|
+
// A profile with no surfaces cannot do this workflow's work, and every later
|
|
136
|
+
// guard compares against `surfaces` — an empty list makes them all vacuously
|
|
137
|
+
// pass. Fail loudly here instead of finishing with nothing done.
|
|
138
|
+
if (!surfaces.length) {
|
|
139
|
+
return { error: 'profile has no surfaces — nothing would be refactored. the `yaml pipeline-profile` block in PIPELINE.md is empty or unparseable, or the profile-reader mis-returned; run /doctor' }
|
|
140
|
+
}
|
|
76
141
|
const byKey = Object.fromEntries(surfaces.map(s => [s.key, s]))
|
|
77
142
|
const contractPath = (profile.contract && profile.contract.path) || ''
|
|
78
143
|
const base = (profile.vcs && profile.vcs.default_branch) || 'main'
|
package/core/workflows/review.js
CHANGED
|
@@ -26,10 +26,73 @@ export const meta = {
|
|
|
26
26
|
],
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
// The Workflow runtime hands `args` to a script verbatim, so a caller that passes a
|
|
30
|
+
// JSON-ENCODED STRING instead of a real object gets that string back here. The old
|
|
31
|
+
// `typeof args === 'string' ? args.trim()` then took the whole blob as the value — which
|
|
32
|
+
// is how a report landed on disk named `specs/reports/{"feature": "x"}.md`, and how
|
|
33
|
+
// maxRounds/smoke were silently dropped on the same run. Parse it back into the object
|
|
34
|
+
// it was meant to be; a bare slug stays valid shorthand.
|
|
35
|
+
const ARGS = (() => {
|
|
36
|
+
if (typeof args === 'string') {
|
|
37
|
+
const t = args.trim()
|
|
38
|
+
if (t.startsWith('{')) {
|
|
39
|
+
try { const o = JSON.parse(t); if (o && typeof o === 'object' && !Array.isArray(o)) return o } catch {}
|
|
40
|
+
}
|
|
41
|
+
return { feature: t, target: t }
|
|
42
|
+
}
|
|
43
|
+
return args && typeof args === 'object' ? args : {}
|
|
44
|
+
})()
|
|
45
|
+
const isSlug = s => typeof s === 'string' && /^[A-Za-z0-9._-]+$/.test(s)
|
|
46
|
+
const feature = ARGS.feature
|
|
30
47
|
if (!feature) throw new Error('cohorte-review needs args = {feature: "<feature_id>"}')
|
|
48
|
+
if (!isSlug(feature)) {
|
|
49
|
+
throw new Error(`cohorte-review got a feature id that is not a slug: ${JSON.stringify(feature)}. ` +
|
|
50
|
+
'Pass args as a real object, e.g. {feature: "titlebar-project-switcher"} — not a JSON string.')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The profile-reader returns through a StructuredOutput tool call, and a haiku agent
|
|
54
|
+
// intermittently nests the whole profile as a JSON *string* under a single wrapper field
|
|
55
|
+
// ({"output": "{\"surfaces\": …}"}) instead of putting the profile's keys at the top level.
|
|
56
|
+
// The schema here used to be {type:'object', additionalProperties:true} — no declared
|
|
57
|
+
// properties, no required keys — so that wrapper validated cleanly and every field then read
|
|
58
|
+
// as undefined: `surfaces` fell back to [], parallel([]) dispatched zero agents, the
|
|
59
|
+
// dead-agent guard had no surfaces to find missing, and the run reported a verdict having
|
|
60
|
+
// done nothing. On the surface it is indistinguishable from a clean run with an empty diff.
|
|
61
|
+
// Declaring the shape gives the tool layer something to validate and the agent something to
|
|
62
|
+
// aim at; unwrapProfile() salvages a wrapped return that still gets through; and the
|
|
63
|
+
// zero-surface abort below makes the silent-success path impossible either way.
|
|
64
|
+
// See also the structured-output section of core/agents/profile-reader.md.
|
|
65
|
+
const PROFILE = {
|
|
66
|
+
type: 'object', additionalProperties: true,
|
|
67
|
+
properties: {
|
|
68
|
+
error: { type: 'string', description: 'set ONLY when PIPELINE.md is missing or unparseable' },
|
|
69
|
+
surfaces: {
|
|
70
|
+
type: 'array',
|
|
71
|
+
description: "one entry per surface, at the TOP LEVEL of this object — never a JSON string",
|
|
72
|
+
items: {
|
|
73
|
+
type: 'object', required: ['key'], additionalProperties: true,
|
|
74
|
+
properties: { key: { type: 'string' }, path: { type: 'string' }, agent: { type: 'string' } },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
}
|
|
31
79
|
|
|
32
|
-
|
|
80
|
+
// Salvage a profile handed back as JSON text rather than as an object — either the whole
|
|
81
|
+
// return, or nested under a single wrapper field. Anything already shaped like a profile
|
|
82
|
+
// (has `surfaces`, or is the documented `{error}` failure shape) passes through untouched.
|
|
83
|
+
const unwrapProfile = p => {
|
|
84
|
+
if (typeof p === 'string') { try { return JSON.parse(p) } catch { return null } }
|
|
85
|
+
if (!p || typeof p !== 'object') return null
|
|
86
|
+
if (Array.isArray(p.surfaces) || p.error) return p
|
|
87
|
+
for (const v of Object.values(p)) {
|
|
88
|
+
if (typeof v !== 'string') continue
|
|
89
|
+
try {
|
|
90
|
+
const inner = JSON.parse(v)
|
|
91
|
+
if (inner && typeof inner === 'object' && !Array.isArray(inner)) return inner
|
|
92
|
+
} catch {}
|
|
93
|
+
}
|
|
94
|
+
return p
|
|
95
|
+
}
|
|
33
96
|
|
|
34
97
|
const PREFLIGHT = {
|
|
35
98
|
type: 'object', required: ['pass'], additionalProperties: false,
|
|
@@ -88,16 +151,22 @@ const VERDICT = {
|
|
|
88
151
|
|
|
89
152
|
// ── Phase 0 — profile ────────────────────────────────────────────────────────
|
|
90
153
|
phase('Profile')
|
|
91
|
-
const profile = await agent(
|
|
154
|
+
const profile = unwrapProfile(await agent(
|
|
92
155
|
'Return this project\'s PIPELINE.md `yaml pipeline-profile` block as JSON, per your instructions.',
|
|
93
156
|
{ agentType: 'profile-reader', label: 'profile', schema: PROFILE, effort: 'low' },
|
|
94
|
-
)
|
|
157
|
+
))
|
|
95
158
|
if (!profile || profile.error) {
|
|
96
159
|
return { verdict: 'ABORTED', reason: `profile unreadable: ${(profile && profile.error) || 'profile-reader returned nothing'}` }
|
|
97
160
|
}
|
|
98
161
|
const cmds = profile.commands || {}
|
|
99
162
|
const base = (profile.vcs && profile.vcs.default_branch) || 'main'
|
|
100
163
|
const surfaces = Array.isArray(profile.surfaces) ? profile.surfaces : []
|
|
164
|
+
// A profile with no surfaces cannot do this workflow's work, and every later
|
|
165
|
+
// guard compares against `surfaces` — an empty list makes them all vacuously
|
|
166
|
+
// pass. Fail loudly here instead of finishing with nothing done.
|
|
167
|
+
if (!surfaces.length) {
|
|
168
|
+
return { verdict: 'ABORTED', reason: 'profile has no surfaces — nothing would be reviewed. the `yaml pipeline-profile` block in PIPELINE.md is empty or unparseable, or the profile-reader mis-returned; run /doctor' }
|
|
169
|
+
}
|
|
101
170
|
const quiet = (q, full) => (q && !String(q).startsWith('<') ? q : full ? `${full} 2>&1 | tail -40` : '')
|
|
102
171
|
const checks = [cmds.typecheck, quiet(cmds.lint_quiet, cmds.lint), quiet(cmds.test_quiet, cmds.test)]
|
|
103
172
|
.filter(c => c && !String(c).startsWith('<'))
|