voidforge-build 23.18.0 → 23.19.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/dist/.claude/workflows/assemble-review.workflow.js +26 -6
- package/dist/.claude/workflows/gauntlet.workflow.js +46 -11
- package/dist/CHANGELOG.md +35 -0
- package/dist/CLAUDE.md +1 -1
- package/dist/VERSION.md +2 -1
- package/dist/docs/methods/WORKFLOWS.md +4 -2
- package/dist/wizard/lib/project-init.js +18 -0
- package/dist/wizard/lib/updater.js +7 -0
- package/package.json +2 -2
|
@@ -22,7 +22,13 @@ export const meta = {
|
|
|
22
22
|
],
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
// Guarded parse: a malformed/empty `args` string must not crash the run before phase 1.
|
|
26
|
+
let input = {}
|
|
27
|
+
try {
|
|
28
|
+
input = typeof args === 'string' ? (args.trim() ? JSON.parse(args) : {}) : (args || {})
|
|
29
|
+
} catch (_e) {
|
|
30
|
+
input = {}
|
|
31
|
+
}
|
|
26
32
|
const diff = input.diff || 'the working-tree diff for this mission (git diff)'
|
|
27
33
|
const roster = Array.isArray(input.roster) && input.roster.length
|
|
28
34
|
? input.roster
|
|
@@ -62,12 +68,23 @@ phase('Review')
|
|
|
62
68
|
const reviews = (await parallel(roster.map((a) => () =>
|
|
63
69
|
agent(
|
|
64
70
|
`You are ${a.name}. Review ONLY ${diff} through the ${a.lens} lens (do not review unchanged code). Evidence-backed findings only — file:line + a quoted CHANGED line or a real repro. For any access/permission/contract finding, name the governing SSOT and reconcile the fix direction (field report #349).`,
|
|
65
|
-
{ label: `${a.name} · review:${a.key}`, phase: 'Review', schema: FINDINGS, agentType: a.
|
|
71
|
+
{ label: `${a.name} · review:${a.key}`, phase: 'Review', schema: FINDINGS, agentType: a.name },
|
|
66
72
|
)
|
|
67
73
|
))).filter(Boolean)
|
|
68
74
|
|
|
75
|
+
const SEV_RANK = { CRITICAL: 5, HIGH: 4, MEDIUM: 3, LOW: 2, WARN: 1 }
|
|
69
76
|
const seen = new Map()
|
|
70
|
-
for (const r of reviews) for (const f of (r.findings || [])) {
|
|
77
|
+
for (const r of reviews) for (const f of (r.findings || [])) {
|
|
78
|
+
const k = key(f)
|
|
79
|
+
if (!seen.has(k)) seen.set(k, { ...f, raisedBy: [r.agent] })
|
|
80
|
+
else {
|
|
81
|
+
const ex = seen.get(k)
|
|
82
|
+
ex.raisedBy.push(r.agent)
|
|
83
|
+
// Keep the highest severity any lens assigned + track who raised it (consensus
|
|
84
|
+
// visibility) — first-write-wins dropped both before.
|
|
85
|
+
if ((SEV_RANK[f.severity] || 0) > (SEV_RANK[ex.severity] || 0)) ex.severity = f.severity
|
|
86
|
+
}
|
|
87
|
+
}
|
|
71
88
|
const claims = [...seen.values()]
|
|
72
89
|
log(`Review: ${reviews.length} lenses → ${claims.length} distinct claims over the diff.`)
|
|
73
90
|
|
|
@@ -83,7 +100,9 @@ const verdicts = await parallel(claims.map((c) => () =>
|
|
|
83
100
|
)).then((votes) => { const v = votes.filter(Boolean); return { claim: c, confirmVotes: v.filter((x) => x.confirm).length } })
|
|
84
101
|
))
|
|
85
102
|
const confirmed = verdicts.filter(Boolean).filter((v) => v.confirmVotes >= 2).map((v) => v.claim)
|
|
86
|
-
|
|
103
|
+
const refuted = verdicts.filter(Boolean).filter((v) => v.confirmVotes < 2)
|
|
104
|
+
.map((v) => ({ title: v.claim.title, file: v.claim.file, confirmVotes: v.confirmVotes }))
|
|
105
|
+
log(`Verify: ${confirmed.length}/${claims.length} survived the 3-lens refute (${refuted.length} refuted, logged in the report).`)
|
|
87
106
|
|
|
88
107
|
// ── Crossfire: adversaries hunt NEW issues the review cleared ─────────────────
|
|
89
108
|
phase('Crossfire')
|
|
@@ -94,7 +113,7 @@ const crossRaw = (await parallel([
|
|
|
94
113
|
].map((a) => () =>
|
|
95
114
|
agent(
|
|
96
115
|
`You are ${a.name}, crossfire adversary over ${diff}. The review already ran — find NEW issues it cleared (bypasses, edge/chaos cases). Evidence-backed only.`,
|
|
97
|
-
{ label: `${a.name} · crossfire:${a.key}`, phase: 'Crossfire', schema: FINDINGS, agentType: a.
|
|
116
|
+
{ label: `${a.name} · crossfire:${a.key}`, phase: 'Crossfire', schema: FINDINGS, agentType: a.name },
|
|
98
117
|
)
|
|
99
118
|
))).filter(Boolean)
|
|
100
119
|
const crossNew = []
|
|
@@ -112,6 +131,7 @@ const all = [...confirmed, ...crossConfirmed]
|
|
|
112
131
|
const sev = (s) => all.filter((f) => f.severity === s)
|
|
113
132
|
return {
|
|
114
133
|
diff,
|
|
115
|
-
counts: { claims: claims.length, confirmed: confirmed.length, crossfireConfirmed: crossConfirmed.length },
|
|
134
|
+
counts: { claims: claims.length, confirmed: confirmed.length, refuted: refuted.length, crossfireConfirmed: crossConfirmed.length },
|
|
116
135
|
critical: sev('CRITICAL'), high: sev('HIGH'), medium: sev('MEDIUM'), low: [...sev('LOW'), ...sev('WARN')],
|
|
136
|
+
refutedLog: refuted, // dropped from the actionable buckets, but never silently — logged per SUB_AGENTS.md
|
|
117
137
|
}
|
|
@@ -28,7 +28,14 @@ export const meta = {
|
|
|
28
28
|
],
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
// Guarded parse: a malformed or empty `args` string must NOT throw and abort the
|
|
32
|
+
// entire run before phase 1 (field report) — fall back to defaults instead.
|
|
33
|
+
let input = {}
|
|
34
|
+
try {
|
|
35
|
+
input = typeof args === 'string' ? (args.trim() ? JSON.parse(args) : {}) : (args || {})
|
|
36
|
+
} catch (_e) {
|
|
37
|
+
input = {}
|
|
38
|
+
}
|
|
32
39
|
const scope = input.scope || 'the working tree / full codebase per gauntlet.md'
|
|
33
40
|
// Surfer-selected specialists (gate-recorded upstream). Fall back to the canonical
|
|
34
41
|
// core leads if no roster was passed (e.g. --light).
|
|
@@ -77,30 +84,43 @@ const VERDICT = {
|
|
|
77
84
|
const key = (f) => `${(f.file || '').toLowerCase()}::${(f.title || '').toLowerCase().slice(0, 60)}`
|
|
78
85
|
|
|
79
86
|
// ── Round 1: Discovery + Round 2/3: Strike ────────────────────────────────────
|
|
87
|
+
const dom = (a) => a.domain || a.key || 'their domain' // avoid literal "undefined" in prompts
|
|
80
88
|
phase('Discovery')
|
|
81
89
|
const discovery = (await parallel(roster.slice(0, 5).map((a) => () =>
|
|
82
90
|
agent(
|
|
83
|
-
`You are ${a.name} (${a
|
|
84
|
-
{ label: `${a.name} · discovery:${a.key}`, phase: 'Discovery', schema: FINDINGS, agentType: a.
|
|
91
|
+
`You are ${a.name} (${dom(a)}). GAUNTLET discovery pass over ${scope}. Map your domain and report concrete, evidence-backed findings only — every finding needs a file:line and a quoted line or a real repro (no speculation). Rate severity honestly.`,
|
|
92
|
+
{ label: `${a.name} · discovery:${a.key}`, phase: 'Discovery', schema: FINDINGS, agentType: a.name },
|
|
85
93
|
)
|
|
86
94
|
))).filter(Boolean)
|
|
87
95
|
|
|
88
96
|
phase('Strike')
|
|
89
|
-
|
|
97
|
+
// Specialists only (index ≥5). When the roster is ≤5 (the default/--light core-leads
|
|
98
|
+
// set) there are NO specialists, so strike is EMPTY — falling back to the full roster
|
|
99
|
+
// here re-ran the identical discovery agents with a "find what discovery missed" prompt,
|
|
100
|
+
// doubling cost for no new coverage (field report). parallel([]) is a harmless no-op.
|
|
101
|
+
const strikeRoster = roster.length > 5 ? roster.slice(5) : []
|
|
102
|
+
if (!strikeRoster.length) log('Strike: no specialists beyond the 5 core leads — skipping (no double-pass).')
|
|
90
103
|
const strike = (await parallel(strikeRoster.map((a) => () =>
|
|
91
104
|
agent(
|
|
92
|
-
`You are ${a.name} (${a
|
|
93
|
-
{ label: `${a.name} · strike:${a.key}`, phase: 'Strike', schema: FINDINGS, agentType: a.
|
|
105
|
+
`You are ${a.name} (${dom(a)}). GAUNTLET strike pass over ${scope}. Deep, adversarial domain review — find what discovery missed. Evidence-backed findings only (file:line + quoted line/repro).`,
|
|
106
|
+
{ label: `${a.name} · strike:${a.key}`, phase: 'Strike', schema: FINDINGS, agentType: a.name },
|
|
94
107
|
)
|
|
95
108
|
))).filter(Boolean)
|
|
96
109
|
|
|
97
110
|
// ── Dedupe across all domains (plain JS — no agent) ───────────────────────────
|
|
111
|
+
const SEV_RANK = { CRITICAL: 5, HIGH: 4, MEDIUM: 3, LOW: 2, WARN: 1 }
|
|
98
112
|
const seen = new Map()
|
|
99
113
|
for (const r of [...discovery, ...strike]) {
|
|
100
114
|
for (const f of (r.findings || [])) {
|
|
101
115
|
const k = key(f)
|
|
102
116
|
if (!seen.has(k)) seen.set(k, { ...f, raisedBy: [r.agent] })
|
|
103
|
-
else
|
|
117
|
+
else {
|
|
118
|
+
const ex = seen.get(k)
|
|
119
|
+
ex.raisedBy.push(r.agent)
|
|
120
|
+
// Keep the HIGHEST severity any agent assigned. First-write-wins silently
|
|
121
|
+
// discarded a later agent's escalation (e.g. one rates MEDIUM, another HIGH).
|
|
122
|
+
if ((SEV_RANK[f.severity] || 0) > (SEV_RANK[ex.severity] || 0)) ex.severity = f.severity
|
|
123
|
+
}
|
|
104
124
|
}
|
|
105
125
|
}
|
|
106
126
|
const claims = [...seen.values()]
|
|
@@ -137,7 +157,7 @@ const ADVERSARIES = [
|
|
|
137
157
|
const crossfireRaw = (await parallel(ADVERSARIES.map((a) => () =>
|
|
138
158
|
agent(
|
|
139
159
|
`You are ${a.name}, a GAUNTLET crossfire adversary over ${scope}. The domain review already ran — hunt NEW issues it cleared (bypasses, chaos/edge cases, exploit chains). Evidence-backed only (file:line + repro).`,
|
|
140
|
-
{ label: `${a.name} · crossfire:${a.key}`, phase: 'Crossfire', schema: FINDINGS, agentType: a.
|
|
160
|
+
{ label: `${a.name} · crossfire:${a.key}`, phase: 'Crossfire', schema: FINDINGS, agentType: a.name },
|
|
141
161
|
)
|
|
142
162
|
))).filter(Boolean)
|
|
143
163
|
// New crossfire claims (not already confirmed) get the same one-pass refute.
|
|
@@ -148,10 +168,23 @@ const crossVerified = await parallel(crossNew.map((c) => () =>
|
|
|
148
168
|
agent(
|
|
149
169
|
`Adversarially verify (default-to-refuted) this crossfire claim, reproducing through the real execution path: "${c.title}" [${c.severity}] at ${c.file}. Evidence: ${c.evidence}.`,
|
|
150
170
|
{ label: `verify:crossfire:${(c.file || '').slice(0, 24)}`, phase: 'Crossfire', schema: VERDICT },
|
|
151
|
-
).then((v) => (
|
|
171
|
+
).then((v) => ({ claim: c, verdict: v }))
|
|
152
172
|
))
|
|
153
|
-
const crossfireConfirmed =
|
|
154
|
-
|
|
173
|
+
const crossfireConfirmed = []
|
|
174
|
+
const crossfireRefuted = []
|
|
175
|
+
for (const cv of crossVerified.filter(Boolean)) {
|
|
176
|
+
const v = cv.verdict
|
|
177
|
+
// The VERDICT schema lets a verdict be survives:true AND finalSeverity:'REFUTED'.
|
|
178
|
+
// Such a claim used to be kept as "confirmed" yet matched NO council severity bucket
|
|
179
|
+
// (bySeverity only checks CRITICAL/HIGH/MEDIUM/LOW/WARN) and silently vanished — breaking
|
|
180
|
+
// the "never silently dropped" invariant. Confirm ONLY a real severity; log the rest.
|
|
181
|
+
if (v && v.survives && v.finalSeverity && v.finalSeverity !== 'REFUTED') {
|
|
182
|
+
crossfireConfirmed.push({ ...cv.claim, finalSeverity: v.finalSeverity })
|
|
183
|
+
} else {
|
|
184
|
+
crossfireRefuted.push({ title: cv.claim.title, finalSeverity: (v && v.finalSeverity) || 'UNVERIFIED', why: (v && v.rationale) || 'verifier returned no verdict' })
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
log(`Crossfire: ${crossNew.length} new claims → ${crossfireConfirmed.length} confirmed, ${crossfireRefuted.length} refuted/unverified (logged, dropped).`)
|
|
155
188
|
|
|
156
189
|
// ── Round 5: Council — synthesize survivors by severity (JS; lead applies fixes) ─
|
|
157
190
|
phase('Council')
|
|
@@ -165,12 +198,14 @@ const report = {
|
|
|
165
198
|
confirmed: confirmed.length,
|
|
166
199
|
refuted: refuted.length,
|
|
167
200
|
crossfireConfirmed: crossfireConfirmed.length,
|
|
201
|
+
crossfireRefuted: crossfireRefuted.length,
|
|
168
202
|
},
|
|
169
203
|
critical: bySeverity('CRITICAL'),
|
|
170
204
|
high: bySeverity('HIGH'),
|
|
171
205
|
medium: bySeverity('MEDIUM'),
|
|
172
206
|
low: [...bySeverity('LOW'), ...bySeverity('WARN')],
|
|
173
207
|
refutedLog: refuted, // dropped, but never silently — logged per SUB_AGENTS.md
|
|
208
|
+
crossfireRefutedLog: crossfireRefuted, // ditto for crossfire claims that failed the one-pass verify
|
|
174
209
|
}
|
|
175
210
|
log(`Council: ${report.critical.length} Critical · ${report.high.length} High · ${report.medium.length} Medium · ${report.low.length} Low/Warn. Lead applies fixes (workflow takes no mid-run input), then re-runs to re-verify.`)
|
|
176
211
|
return report
|
package/dist/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,41 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [23.19.0] - 2026-06-13
|
|
10
|
+
|
|
11
|
+
### Gauntlet acceptance test → 14 fixes
|
|
12
|
+
|
|
13
|
+
The ADR-067 workflow re-platform validated by running `gauntlet.workflow.js` live on the v23.13–v23.18 platform code (a 10-agent Surfer roster fanned to 347 agents → 99 distinct claims → 66 confirmed + 24 crossfire, **0 Critical**). The acceptance test passed *and* surfaced real bugs; the 3-lens-confirmed High/Medium findings are fixed here. (An earlier run crashed the machine ~44 min in at peak load; the relaunch completed cleanly — the workflow is resumable via `resumeFromRunId`.)
|
|
14
|
+
|
|
15
|
+
### Fixed — Silver Surfer gate (security/correctness)
|
|
16
|
+
|
|
17
|
+
- **`_paths.sh` reap could delete the entire `sessions/` tree.** `find "$SESSIONS_DIR" -maxdepth 1 -type d -mmin +60` matches the search root itself; without `-mindepth 1` a stale `sessions/` root mtime made the sweep `rm -rf` every live session's roster + bypass flag. Added `-mindepth 1`.
|
|
18
|
+
- **Reap-vs-fresh-roster/bypass race (the one VERSION.md flagged for a field report).** The TTL refresh touched the roster *file* but the reaper keys on the session *directory* mtime (touching a child never bumps the parent). `check.sh` now touches `$SESSION_DIR` on every roster/bypass ALLOW, `bypass.sh` touches it on write, and the reap threshold (+120m) is held strictly above the roster TTL (3600s) — an active session is never reaped while still valid.
|
|
19
|
+
- **Gate silently broke on Alpine/minimal Linux:** `_paths.sh` hashed paths with `shasum` only (Perl, absent there). Added a `sha256sum` fallback via a shared `surfer_gate_repo_hash` helper.
|
|
20
|
+
- **`bypass.sh` run before the first hook fire was a silent no-op** (the documented orchestrator order). It now records a repo-scoped *pending* bypass that `check.sh` promotes to the session flag on the first fire.
|
|
21
|
+
|
|
22
|
+
### Fixed — Dynamic Workflow scripts
|
|
23
|
+
|
|
24
|
+
- **Strike phase double-ran the roster** when ≤5 agents (the default/`--light` core-leads set): `strikeRoster` fell back to the full roster, re-running discovery agents under a "find what discovery missed" prompt. Now empty when there are no specialists.
|
|
25
|
+
- **Crossfire `survives:true` + `finalSeverity:'REFUTED'` verdicts silently vanished** — kept as "confirmed" yet matched no council severity bucket. Now excluded and logged in `crossfireRefutedLog`, honoring the never-silently-dropped invariant.
|
|
26
|
+
- **Dedup kept the first raiser's severity**, discarding a later agent's higher rating. Now keeps the max severity and tracks `raisedBy` (gauntlet + assemble-review). assemble-review also gains a `refutedLog`.
|
|
27
|
+
- **Unguarded `JSON.parse(args)`** could crash the whole run before phase 1; now falls back to defaults. Undefined-`domain` roster items no longer render literal `undefined` in prompts.
|
|
28
|
+
|
|
29
|
+
### Fixed — distribution & CI
|
|
30
|
+
|
|
31
|
+
- **`npx voidforge-build init` now copies `.claude/workflows/`** (+ `AGENT_CLASSIFICATION.md`). gauntlet.md/assemble.md referenced workflow scripts that the CLI init path never shipped — v23.18.0 only patched the prepack/dist paths, not `project-init.ts`.
|
|
32
|
+
- **`npx voidforge-build update` now propagates `.claude/workflows` + `scripts/surfer-gate`** — both were absent from the updater's diff list, so existing projects never received workflow or gate fixes.
|
|
33
|
+
- **`publish.yml`:** `recover-partial` derives the version from `package.json`, not `github.ref_name` (a branch name on `workflow_dispatch`, which mis-targeted `npm deprecate`); the Playwright cache key keys on the committed manifests instead of the deleted-and-regenerated lockfile (was a permanent cache miss).
|
|
34
|
+
|
|
35
|
+
### Added / Changed
|
|
36
|
+
|
|
37
|
+
- **`scripts/validate-workflows.sh`** (+ `npm run validate:workflows`, wired into `pretest`): a real syntax gate for `.workflow.js` scripts. Their top-level `await`/`return` make a bare `node --check` fail ("Illegal return statement"), so the v23.18.0 "passes node --check" claim was inaccurate; the validator reproduces the runtime's async wrapper before checking, catching syntax errors before they ship.
|
|
38
|
+
- **`WORKFLOWS.md`** example corrected (`agentType: a.id` → `a.name`) + two new gotchas (agentType resolves by the `name:` display field; how to validate workflow scripts). Stale pre-ADR-060 `/tmp/voidforge-*` paths fixed in the gate `README.md` and `CLAUDE.md`.
|
|
39
|
+
|
|
40
|
+
### Validation
|
|
41
|
+
|
|
42
|
+
Gate suite **23 → 27** (added reap-root-preservation + pending-bypass cases). Full suite **1390 → 1392** (added init-copies-workflows + updater-tracks-workflows/gate regression tests). typecheck clean. Deferred as field-report candidates: concurrent same-repo pointer collision, `workflow_dispatch` branch guard. Dep `^23.18.0` → `^23.19.0` (ADR-062).
|
|
43
|
+
|
|
9
44
|
## [23.18.0] - 2026-06-13
|
|
10
45
|
|
|
11
46
|
### Workflow re-platform of `/gauntlet` + `/assemble` (ADR-067)
|
package/dist/CLAUDE.md
CHANGED
|
@@ -39,7 +39,7 @@ ADR-051 enforces this gate at the hook level (PreToolUse). The prose below is th
|
|
|
39
39
|
2. When the user's command includes `--light` or `--solo`, BEFORE launching the Surfer or any other agent: `[ -x scripts/surfer-gate/bypass.sh ] && bash scripts/surfer-gate/bypass.sh --light || true` (or `--solo`). **Fails closed on unknown flag values** (ADR-060 v23.8.18 hardening, SEC-003) — passing anything other than `--light` or `--solo` exits 2 with an error. No silent bypass.
|
|
40
40
|
3. **Normalize roster names before dispatch** (field report #345, DEAL-001). The Silver Surfer returns agent names from a Haiku pre-scan, which can drift from the actual filenames in `.claude/agents/` (extra `silver-surfer-` prefix, a stray `.md` suffix, a hyphen/underscore mismatch). Before you launch, validate each name against `ls .claude/agents/`: if it matches a file (with or without the `.md` extension), keep it; if not, attempt exactly one correction — strip a known prefix/suffix or normalize separators — and re-check. If it still doesn't resolve, DROP that single name and proceed with the rest of the roster. Never block the whole dispatch over one unresolved name; log the dropped name so the Herald roster can be corrected upstream. The gate's job is to enforce *that* a roster ran, not to fail the run over a typo in *one* name.
|
|
41
41
|
|
|
42
|
-
If `scripts/surfer-gate/check.sh` exists but you skip step 1, your first non-Surfer Agent call in that turn will be blocked with a clear message and your own log line in `/tmp/voidforge-session
|
|
42
|
+
If `scripts/surfer-gate/check.sh` exists but you skip step 1, your first non-Surfer Agent call in that turn will be blocked with a clear message and your own log line in the gate's session log (`<gate-root>/sessions/$SESSION_ID/gate.log`, where `<gate-root>` is `$XDG_RUNTIME_DIR/voidforge-gate` or `$HOME/.voidforge/gate` per ADR-060 — the old `/tmp/voidforge-session-*` path was retired in v23.8.18). You are expected to comply with the block (launch Surfer / run record-roster), not to fight it. If the script does not exist, your project predates v23.10.0; pull the gate from `tmcleod3/voidforge:scripts/surfer-gate/` and merge `settings-snippet.json` into `.claude/settings.json`, or re-run `npx voidforge-build init` against the methodology source.
|
|
43
43
|
|
|
44
44
|
**Why.** Seven field incidents (logged in `.claude/agents/silver-surfer-herald.md`) document the cost of skipping: the orchestrator cannot predict cross-domain relevance from the command name alone. The hook makes skipping mechanically impossible for non-bypass cases. Launch the Surfer. Every time.
|
|
45
45
|
|
package/dist/VERSION.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Version
|
|
2
2
|
|
|
3
|
-
**Current:** 23.
|
|
3
|
+
**Current:** 23.19.0
|
|
4
4
|
|
|
5
5
|
## Versioning Scheme
|
|
6
6
|
|
|
@@ -14,6 +14,7 @@ This project uses [Semantic Versioning](https://semver.org/):
|
|
|
14
14
|
|
|
15
15
|
| Version | Date | Summary |
|
|
16
16
|
|---------|------|---------|
|
|
17
|
+
| 23.19.0 | 2026-06-13 | **Gauntlet acceptance test → 14 fixes (the ADR-067 re-platform, validated by running it on itself).** Ran the new `gauntlet.workflow.js` live on the v23.13–v23.18 platform code (10-agent Surfer roster → 347 agents → 99 distinct claims → 66 confirmed + 24 crossfire, 0 Critical) and fixed the 3-lens-confirmed findings. **Gate (security):** `_paths.sh` reap was missing `-mindepth 1` → could `rm -rf` the entire `sessions/` tree (every live roster/bypass); the reaper now refreshes `$SESSION_DIR` mtime on activity + threshold raised above the TTL, closing the documented reap-vs-fresh-roster/bypass race; `shasum`→`sha256sum` fallback (gate silently broke on Alpine); `bypass.sh` run before the first hook fire now records a repo-scoped *pending* bypass that `check.sh` promotes (was a silent no-op). **Workflows:** strike no longer re-runs the same ≤5-agent roster twice; crossfire `survives:true+REFUTED` verdicts no longer vanish into no bucket (logged in `crossfireRefutedLog`); dedup keeps the **highest** severity + `raisedBy` (was first-write-wins); guarded `JSON.parse(args)`; undefined-domain prompt guard. **Distribution:** `npx voidforge-build init` now copies `.claude/workflows/` + `AGENT_CLASSIFICATION.md`; `update` now propagates `.claude/workflows` + `scripts/surfer-gate` (both were stranded). **Validation:** new `scripts/validate-workflows.sh` (wraps the runtime shape, then `node --check`) wired into `pretest` — corrects the false "scripts pass `node --check`" claim and gates syntax errors from shipping. **Docs:** `WORKFLOWS.md` example `agentType: a.id`→`a.name` + new gotchas; stale `/tmp/voidforge-*` paths fixed in gate README + CLAUDE.md (ADR-060). **CI:** `recover-partial` derives the version from `package.json` not `github.ref_name` (broke on dispatch); Playwright cache key off the committed manifests not the regenerated lockfile. Gate suite 23→27, full suite 1390→1392. Deferred (field-report candidates): concurrent same-repo pointer collision, `workflow_dispatch` branch guard. Dep `^23.18.0` → `^23.19.0`. |
|
|
17
18
|
| 23.18.0 | 2026-06-13 | **Workflow re-platform of `/gauntlet` + `/assemble` (ADR-067)** — the opportunity ADR-064 unblocked. New `.claude/workflows/gauntlet.workflow.js` (discovery → JS dedupe → 3-lens adversarial REFUTE → crossfire → council, schema-validated) and `assemble-review.workflow.js` (engage+sentinel over a mission diff; build/arch/devops stay prose). New **`docs/methods/WORKFLOWS.md`** authoring standard (API, the #348/#363 gotchas, 16/1000 caps, and the ADR-064 gate-launch sequence: Surfer→record-roster→Workflow). `gauntlet.md`/`assemble.md` gain workflow-execution sections; personas + fix-application + Debate Protocol stay prose. **Distribution gate (Phase 12.75):** `.claude/workflows/` is a new shared category — added to `prepack.sh` (npm) + `copy-assets.sh` (init) so the scripts ship to consumers. Both scripts `node --check`-validated (ESM async-wrapped); the live end-to-end gauntlet run is the acceptance test. Dep `^23.17.0` → `^23.18.0`. |
|
|
18
19
|
| 23.17.0 | 2026-06-13 | **Effort-tiering fleet edit (ADR-054) — verified + applied.** Verified against the official Claude Code sub-agents docs that `effort` is a supported sub-agent frontmatter field (values `low`/`medium`/`high`/`xhigh`/`max`; "available levels depend on the model"). Applied across all 264 agent definitions: **20 leads (`model: inherit`) → `effort: xhigh`, 201 Sonnet specialists → `effort: medium`, 43 Haiku scouts → omitted** (Haiku doesn't support the parameter). Per-agent reasoning-spend lever, independent of model tier — the largest cost lever in the fleet (200 specialists no longer pay lead-level reasoning for read-and-report review). Frontmatter-only, idempotent insert after the `model:` line; `validate-agent-refs` + full suite (1390/1390) green; integrity preserved. Closes the M2 deferral from v23.16.0. Updated ADR-054 (status→fleet-applied), SUB_AGENTS.md, COMPATIBILITY.md. Dep `^23.16.0` → `^23.17.0`. (Aside: confirmed the v23.16.0 gate fix live — a Workflow launch was correctly BLOCKED this session until a `--light` bypass was set; noted a reap-vs-fresh-bypass timing race for a future field report.) |
|
|
19
20
|
| 23.16.0 | 2026-06-13 | Platform-alignment campaign — implements the ADR set from `/architect --plan` (→ `/campaign`). **ADR-064 (gate↔Workflow) IMPLEMENTED:** the Silver Surfer `PreToolUse` hook matcher is now `Agent\|Workflow` and `check.sh` gates the **Workflow tool launch** on a recorded roster (closes the proven bypass where workflow-spawned agents skipped the gate); `test.sh` gains 3 Workflow cases (**23/23**), mirrored to the methodology package. **Behavior change:** a Workflow run now requires a recorded roster or a `--light`/`--solo` bypass — build/apply/research workflows should set a bypass. **ADR-065 (platform floor):** `docs/COMPATIBILITY.md` gains a Claude Code platform-floor + per-feature maturity table; informational `claudeCodeFloor` field; semver rule (raising the floor = breaking) + release-checklist item. **ADR-066 (native-capability tracker):** new `docs/NATIVE_CAPABILITIES.md` audits all commands vs native skills with dispositions (`/qa`,`/test` coexist+document; `/git` keep) — realizes ADR-050's deferred follow-up; release re-audit item added. Amended **ADR-051** (workflow-exemption→closure), **ADR-054** (effort tiers + Haiku 200K/no-effort). M2 effort fleet-edit deferred per ADR-054 precondition (runtime `effort:`-frontmatter honoring unverified). Dep `^23.15.0` → `^23.16.0`. |
|
|
@@ -27,8 +27,8 @@ export const meta = { // MUST be a pure literal — no var
|
|
|
27
27
|
const roster = typeof args === 'string' ? JSON.parse(args) : args // see Gotcha 1
|
|
28
28
|
phase('Find')
|
|
29
29
|
const found = (await parallel(roster.map(a => () =>
|
|
30
|
-
agent(prompt(a), { label: `${a.name} · find:${a.key}`, phase: 'Find', schema: FINDINGS, agentType: a.
|
|
31
|
-
))).filter(Boolean)
|
|
30
|
+
agent(prompt(a), { label: `${a.name} · find:${a.key}`, phase: 'Find', schema: FINDINGS, agentType: a.name })
|
|
31
|
+
))).filter(Boolean) // agentType resolves by the agent's `name:` display field — see Gotcha 6
|
|
32
32
|
const claims = dedupe(found.flatMap(f => f.findings)) // plain JS reduce — no agent
|
|
33
33
|
phase('Verify')
|
|
34
34
|
const verdicts = await parallel(claims.map(c => () =>
|
|
@@ -51,6 +51,8 @@ return { confirmed: claims.filter((c,i) => verdicts[i]?.survives) }
|
|
|
51
51
|
3. **No `Date.now()` / `Math.random()` / argless `new Date()`** — they throw (they'd break resume). Pass timestamps via `args`; vary by index for "randomness."
|
|
52
52
|
4. **Concurrency caps (ADR-059):** ~16 concurrent / ~1,000 total per run. `parallel([...])` accepts 100s of items but only ~16 run at once. **Batch** unbounded fan-outs (glob-then-partition, `SUB_AGENTS.md`); never one-agent-per-file on a large repo.
|
|
53
53
|
5. **Cost lever:** route cheap stages with `agent(p, {model:'haiku'})` (scout pre-scans) and reserve the default model for synthesis — the way the Surfer already runs on Haiku.
|
|
54
|
+
6. **`agentType` resolves by the agent's `name:` display field, NOT the filename** (e.g. `'Picard'`, not `'picard-architecture'`). A filename-style `agentType` fails to resolve and the `agent()` call returns `null` (silently filtered by `.filter(Boolean)`), so the agent simply never runs. If a roster carries both, pass `a.name`. Same rule as the Agent tool's `subagent_type`.
|
|
55
|
+
7. **Validate before shipping:** a workflow script's top-level `await`/`return` make a bare `node --check` fail ("Illegal return statement") — that is expected (the runtime wraps the body in an async fn). Use `npm run validate:workflows` (wired into `pretest`), which reproduces the wrapper before checking, so a real syntax error is caught in CI rather than shipping to npm.
|
|
54
56
|
|
|
55
57
|
## Gate interop (ADR-064) — REQUIRED
|
|
56
58
|
|
|
@@ -89,6 +89,15 @@ async function copyMethodology(methodologyRoot, projectDir, core) {
|
|
|
89
89
|
if (existsSync(agentsSrc)) {
|
|
90
90
|
count += await copyDir(agentsSrc, join(projectDir, '.claude', 'agents'));
|
|
91
91
|
}
|
|
92
|
+
// Dynamic Workflow scripts (ADR-067 — gauntlet/assemble review skeletons).
|
|
93
|
+
// gauntlet.md / assemble.md reference `.claude/workflows/*.workflow.js`; without this
|
|
94
|
+
// a fresh `npx voidforge-build init` ships the command docs but NOT the scripts they
|
|
95
|
+
// invoke. prepack.sh + copy-assets.sh ship them to the npm package and dist/, but this
|
|
96
|
+
// CLI init copy path (methodologyRoot = the installed package) was missed in v23.18.0.
|
|
97
|
+
const workflowsSrc = join(methodologyRoot, '.claude', 'workflows');
|
|
98
|
+
if (existsSync(workflowsSrc)) {
|
|
99
|
+
count += await copyDir(workflowsSrc, join(projectDir, '.claude', 'workflows'));
|
|
100
|
+
}
|
|
92
101
|
// Methods
|
|
93
102
|
const methodsSrc = join(methodologyRoot, 'docs', 'methods');
|
|
94
103
|
if (existsSync(methodsSrc)) {
|
|
@@ -108,6 +117,15 @@ async function copyMethodology(methodologyRoot, projectDir, core) {
|
|
|
108
117
|
await cp(registrySrc, join(projectDir, 'docs', 'NAMING_REGISTRY.md'));
|
|
109
118
|
count++;
|
|
110
119
|
}
|
|
120
|
+
// Agent classification — single source of truth for agent counts (v23.7.0). Command
|
|
121
|
+
// docs derive their counts from it; prepack ships it to the npm package, but this init
|
|
122
|
+
// path omitted it, so init'd projects couldn't resolve the count SSOT.
|
|
123
|
+
const classificationSrc = join(methodologyRoot, 'docs', 'AGENT_CLASSIFICATION.md');
|
|
124
|
+
if (existsSync(classificationSrc)) {
|
|
125
|
+
await mkdir(join(projectDir, 'docs'), { recursive: true });
|
|
126
|
+
await cp(classificationSrc, join(projectDir, 'docs', 'AGENT_CLASSIFICATION.md'));
|
|
127
|
+
count++;
|
|
128
|
+
}
|
|
111
129
|
// Thumper scripts
|
|
112
130
|
const thumperSrc = join(methodologyRoot, 'scripts', 'thumper');
|
|
113
131
|
if (existsSync(thumperSrc)) {
|
|
@@ -58,9 +58,16 @@ export async function diffMethodology(projectDir) {
|
|
|
58
58
|
const dirs = [
|
|
59
59
|
{ src: '.claude/commands', dest: '.claude/commands' },
|
|
60
60
|
{ src: '.claude/agents', dest: '.claude/agents' },
|
|
61
|
+
// Dynamic Workflow scripts (ADR-067) and the Silver Surfer gate (ADR-051/060/064):
|
|
62
|
+
// both ship to new projects via init but were absent from the updater's diff list, so
|
|
63
|
+
// `npx voidforge-build update` never propagated them — existing projects were stranded
|
|
64
|
+
// on whatever gate/workflow scripts they were created with (e.g. a gate before this
|
|
65
|
+
// release's reap fix). Invocation is via `bash <script>` so exec bits are not required.
|
|
66
|
+
{ src: '.claude/workflows', dest: '.claude/workflows' },
|
|
61
67
|
{ src: 'docs/methods', dest: 'docs/methods' },
|
|
62
68
|
{ src: 'docs/patterns', dest: 'docs/patterns' },
|
|
63
69
|
{ src: 'scripts/thumper', dest: 'scripts/thumper' },
|
|
70
|
+
{ src: 'scripts/surfer-gate', dest: 'scripts/surfer-gate' },
|
|
64
71
|
];
|
|
65
72
|
// Single files to compare
|
|
66
73
|
const singleFiles = ['CLAUDE.md', 'HOLOCRON.md', 'VERSION.md'];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "voidforge-build",
|
|
3
|
-
"version": "23.
|
|
3
|
+
"version": "23.19.0",
|
|
4
4
|
"description": "From nothing, everything. A methodology framework for building with Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"@aws-sdk/client-rds": "^3.700.0",
|
|
46
46
|
"@aws-sdk/client-s3": "^3.700.0",
|
|
47
47
|
"@aws-sdk/client-sts": "^3.700.0",
|
|
48
|
-
"voidforge-build-methodology": "^23.
|
|
48
|
+
"voidforge-build-methodology": "^23.19.0",
|
|
49
49
|
"node-pty": "^1.2.0-beta.12",
|
|
50
50
|
"ws": "^8.19.0"
|
|
51
51
|
},
|