wei-wuwei 0.1.0 → 0.2.1
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 +27 -0
- package/README.md +14 -4
- package/agents/wuwei-implementer/agent.md +34 -0
- package/bin/wuwei.js +211 -5
- package/package.json +1 -1
- package/skills/wuwei-execute-phase/skill.md +10 -5
- package/skills/wuwei-plan-phases/skill.md +8 -5
- package/skills/wuwei-review/references/prep.md +1 -1
- package/skills/wuwei-review/skill.md +4 -4
- package/wuwei.config.json +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,33 @@ One section per published version, newest first. `wuwei update` prints the
|
|
|
4
4
|
sections between the installed version and the new one before it asks;
|
|
5
5
|
`wuwei check` fails when package.json's version has no section here.
|
|
6
6
|
|
|
7
|
+
## 0.2.1
|
|
8
|
+
|
|
9
|
+
- `wuwei review-collect`: finds the latest Claude Code session that
|
|
10
|
+
dispatched a wuwei lens, copies each lens's transcript and report into
|
|
11
|
+
the review bundle's `reports/`, and prints what each one spent — turns,
|
|
12
|
+
context built, cached re-reads, and where the reading went. wuwei-review
|
|
13
|
+
§7 uses it instead of the collator re-typing each report.
|
|
14
|
+
|
|
15
|
+
## 0.2.0
|
|
16
|
+
|
|
17
|
+
Execute runs at a fixed tier; the model policy now covers review only.
|
|
18
|
+
|
|
19
|
+
- `wuwei-execute-phase` runs at `sonnet` / `xhigh` under every policy: its
|
|
20
|
+
own frontmatter sets the tier for the turn it runs in, and step 2 always
|
|
21
|
+
dispatches the new `wuwei-implementer` agent (pinned `sonnet` / `xhigh`)
|
|
22
|
+
with no model or effort on the call. A stop names `/wuwei-execute-phase`,
|
|
23
|
+
since a reply in chat continues on the session's model.
|
|
24
|
+
- **Changed:** `model policy` under `## Sign-off` is `inherit` | `roles` for
|
|
25
|
+
the review tier (`roles` sends `review:design` and `review:defects` on
|
|
26
|
+
opus). `implementation=…` is refused by `plan-check` and `claim`.
|
|
27
|
+
- `wuwei claim` prints how steps 2 and 5 dispatch, and for Codex how to
|
|
28
|
+
start the session; `wuwei check` ties the skill's and the agent's tier to
|
|
29
|
+
`modelRoles` in `wuwei.config.json`.
|
|
30
|
+
- Codex: `## Sign-off` takes `codex: <model> / <effort>`; `wuwei init`
|
|
31
|
+
offers a `[profiles.wuwei-execute]` in `$CODEX_HOME/config.toml` (removed
|
|
32
|
+
by `wuwei uninstall`); `claim` prints `codex --profile wuwei-execute`.
|
|
33
|
+
|
|
7
34
|
## 0.1.0
|
|
8
35
|
|
|
9
36
|
First release.
|
package/README.md
CHANGED
|
@@ -59,13 +59,15 @@ cd wuwei
|
|
|
59
59
|
npm run check # lint frontmatter
|
|
60
60
|
npm run build # emit repo-local artifacts (.claude/, .agents/, AGENTS.md, CLAUDE.md)
|
|
61
61
|
node bin/wuwei.js init <project> # repo scope: templates + .wuwei/ skeleton, then
|
|
62
|
-
# offers the tools below and
|
|
62
|
+
# offers the tools below (and, with Codex, an execute
|
|
63
|
+
# profile in $CODEX_HOME/config.toml) and names the next step
|
|
63
64
|
node bin/wuwei.js install # user scope: skills/agents → ~/.claude, rules → ~/.claude/CLAUDE.md;
|
|
64
65
|
# Codex: skills → ~/.agents/skills, rules → $CODEX_HOME/AGENTS.md
|
|
65
66
|
node bin/wuwei.js status <project> # read-only: streams/claims from .wuwei, drift vs STATE.md flagged
|
|
66
67
|
node bin/wuwei.js update # git pull if behind, diff against what install wrote, apply on yes
|
|
67
68
|
node bin/wuwei.js review-prep <base> # wuwei-review's prep: dispatcher checks + scan bundle (or --pr <n>)
|
|
68
69
|
node bin/wuwei.js claim [stream] [NN] # list eligible phases, or claim one: cut, claim, push
|
|
70
|
+
node bin/wuwei.js review-collect # each lens's transcript + report into the bundle, and what each one spent
|
|
69
71
|
node bin/wuwei.js digest-check <f> # a review digest's arithmetic before it is published
|
|
70
72
|
node bin/wuwei.js plan-check [stream] # plan-phases step 11's checkable half, before sign-off
|
|
71
73
|
node bin/wuwei.js worktree <lens> <sha> # a throwaway worktree no other lens shares (--prune)
|
|
@@ -91,8 +93,16 @@ npm in the background, and when a newer version is out it says so on stderr
|
|
|
91
93
|
(at most hourly; never from a clone or in CI; `WUWEI_NO_UPDATE_CHECK=1` turns
|
|
92
94
|
it off).
|
|
93
95
|
|
|
94
|
-
Releasing
|
|
95
|
-
`
|
|
96
|
+
Releasing is merging. Every merge to main that changes what the package ships
|
|
97
|
+
(`bin`, `skills`, `agents`, `templates`, `wuwei.config.json`) is released by
|
|
98
|
+
`.github/workflows/publish.yml`: the version is bumped by the merged commits'
|
|
99
|
+
Conventional Commit types — `feat` minor, `type!:` or a `BREAKING CHANGE:`
|
|
100
|
+
footer major (minor while under 1.0), anything else patch — `CHANGELOG.md`
|
|
101
|
+
gains a section from the merged PR titles, and the release commit, the
|
|
102
|
+
`v<version>` tag, the npm publish and the GitHub Release follow. A docs- or
|
|
103
|
+
tests-only merge releases nothing; *Run workflow* (`workflow_dispatch`) with a
|
|
104
|
+
bump releases now. A version already set in `package.json` that npm has not
|
|
105
|
+
seen goes out as is.
|
|
96
106
|
|
|
97
107
|
## Daily use
|
|
98
108
|
|
|
@@ -188,7 +198,7 @@ wuwei/
|
|
|
188
198
|
├── agents/ # canonical subagent definitions
|
|
189
199
|
│ └── _template/
|
|
190
200
|
├── templates/ # PR / review templates, installed per-repo
|
|
191
|
-
├── bin/wuwei.js # CLI: build · check · list · init · install · update · status · review-prep · claim · digest-check · plan-check · worktree · mutate · sites · verify · uninstall
|
|
201
|
+
├── bin/wuwei.js # CLI: build · check · list · init · install · update · status · review-prep · review-collect · claim · digest-check · plan-check · worktree · mutate · sites · verify · uninstall
|
|
192
202
|
│ # the per-tool emitters live here too, not in adapters/
|
|
193
203
|
├── adapters/ # notes only — see bin/wuwei.js for the actual emitters
|
|
194
204
|
├── docs/
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wuwei-implementer
|
|
3
|
+
description: >-
|
|
4
|
+
The implementation subagent of wuwei-execute-phase step 2, under every model
|
|
5
|
+
policy: one task of a phase, dispatched fresh with SPEC, the phase context,
|
|
6
|
+
the ladder and step 4's floor. Exists so implementation runs at sonnet /
|
|
7
|
+
xhigh — a dispatch alone can set a model but not an effort.
|
|
8
|
+
model: sonnet
|
|
9
|
+
effort: xhigh
|
|
10
|
+
version: 0.1.0
|
|
11
|
+
targets: [claude-code]
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# wuwei-implementer
|
|
15
|
+
|
|
16
|
+
## Mandate
|
|
17
|
+
|
|
18
|
+
Implement the one task you were dispatched with, inside the phase you were
|
|
19
|
+
given, and nothing beyond it. What arrives with the dispatch governs: SPEC,
|
|
20
|
+
the phase context, the ladder (climb it before writing), and step 4's floor.
|
|
21
|
+
`wuwei-tdd` governs the task; commit as `wuwei-commit-hygiene` says — one
|
|
22
|
+
concern per commit, the why in the body.
|
|
23
|
+
|
|
24
|
+
## Out of scope
|
|
25
|
+
|
|
26
|
+
- Another task, or the phase's next step — hand back instead.
|
|
27
|
+
- A planning defect (a missing `depends-on`, an interface not there yet):
|
|
28
|
+
report it; do not work around it.
|
|
29
|
+
- Review of your own work — a fresh subagent does that.
|
|
30
|
+
|
|
31
|
+
## Output contract
|
|
32
|
+
|
|
33
|
+
What landed (commit shas, one line each), the tests that ran and their
|
|
34
|
+
result, and anything you stopped short of — with why.
|
package/bin/wuwei.js
CHANGED
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
* wuwei claim [<stream>] [<phase>] [--take-over] [--by <id>] [--dry]
|
|
23
23
|
* wuwei-execute-phase step 1: list what is eligible,
|
|
24
24
|
* or claim one — cut, claim, push
|
|
25
|
+
* wuwei review-collect [--session <id>] each subagent's transcript and report into
|
|
26
|
+
* the bundle's reports/, and what each one spent
|
|
25
27
|
* wuwei digest-check <file> wuwei-review §7's arithmetic before publishing: count,
|
|
26
28
|
* rows, credits, file:line, Action names, redaction
|
|
27
29
|
* wuwei worktree <lens> [<sha>] a throwaway worktree no other lens shares; --prune
|
|
@@ -187,10 +189,8 @@ function validate(items) {
|
|
|
187
189
|
errs.push(`${it.rel}: description too long (${it.data.description.length} > 400)`)
|
|
188
190
|
if (it.data.mode && !MODES.includes(it.data.mode))
|
|
189
191
|
errs.push(`${it.rel}: mode must be one of ${MODES.join('|')}`)
|
|
190
|
-
// a skill
|
|
191
|
-
// and
|
|
192
|
-
if ((it.data.model || it.data.effort) && it.kind !== 'agents')
|
|
193
|
-
errs.push(`${it.rel}: model/effort apply to agents only — a skill is not a subagent`)
|
|
192
|
+
// on a skill, Claude Code applies model/effort to the turn the skill runs
|
|
193
|
+
// in (and only that turn — the session's resume on the next prompt)
|
|
194
194
|
if (it.data.model && !MODEL_ALIASES.includes(it.data.model)
|
|
195
195
|
&& !/^claude-[a-z0-9.-]+$/.test(it.data.model))
|
|
196
196
|
errs.push(`${it.rel}: model must be ${MODEL_ALIASES.join('|')} or a full id (claude-…)`)
|
|
@@ -536,6 +536,21 @@ function lint(items) {
|
|
|
536
536
|
for (const rel of Object.keys(budgets))
|
|
537
537
|
if (!items.some(it => it.rel === rel || (it.refs ?? []).some(r => r.rel === rel)))
|
|
538
538
|
errs.push(`wuwei.config.json: wordBudgets names ${rel}, which is not a definition or reference`)
|
|
539
|
+
// modelRoles names the tier; the agent file is what dispatch actually reads
|
|
540
|
+
const impl = CONFIG.modelRoles?.implementation
|
|
541
|
+
if (impl) {
|
|
542
|
+
const ag = items.find(it => it.kind === 'agents' && it.data.name === impl.agent)
|
|
543
|
+
if (!ag) errs.push(`wuwei.config.json: modelRoles.implementation.agent ${impl.agent} is not an agent`)
|
|
544
|
+
else for (const f of ['model', 'effort']) if (ag.data[f] !== impl[f])
|
|
545
|
+
errs.push(`${ag.rel}: ${f} ${ag.data[f] ?? '(unset)'} ≠ modelRoles.implementation.${f} ${impl[f]} — the roles tier is what this file pins`)
|
|
546
|
+
}
|
|
547
|
+
// the skill that runs implementation runs its own turn at the same tier
|
|
548
|
+
const sk = impl?.skill && items.find(it => it.kind === 'skills' && it.data.name === impl.skill)
|
|
549
|
+
if (impl?.skill && !sk) errs.push(`wuwei.config.json: modelRoles.implementation.skill ${impl.skill} is not a skill`)
|
|
550
|
+
else if (sk) for (const f of ['model', 'effort']) if (sk.data[f] !== impl[f])
|
|
551
|
+
errs.push(`${sk.rel}: ${f} ${sk.data[f] ?? '(unset)'} ≠ modelRoles.implementation.${f} ${impl[f]} — execute runs at the implementer's tier`)
|
|
552
|
+
for (const l of CONFIG.modelRoles?.review?.lenses ?? [])
|
|
553
|
+
if (!items.some(it => it.kind === 'agents' && it.data.name === l)) errs.push(`wuwei.config.json: modelRoles.review.lenses names ${l}, which is not an agent`)
|
|
539
554
|
// update prints this file's sections — a version without one updates in silence
|
|
540
555
|
const changelog = existsSync(join(ROOT, 'CHANGELOG.md')) ? readFileSync(join(ROOT, 'CHANGELOG.md'), 'utf8') : ''
|
|
541
556
|
if (!new RegExp(`^## v?${escapeRe(PKG.version)}\\b`, 'm').test(changelog))
|
|
@@ -692,6 +707,7 @@ async function cmdUninstall(target) {
|
|
|
692
707
|
}
|
|
693
708
|
if (stripManagedBlock(join(codexHome(), 'AGENTS.md')))
|
|
694
709
|
removed.push(join(codexHome(), 'AGENTS.md') + ' (managed block)')
|
|
710
|
+
if (removeCodexProfile()) removed.push(`${codexConfigPath()} ([profiles.${CODEX_PROFILE}])`)
|
|
695
711
|
if (existsSync(manifestPath())) { rmSync(manifestPath()); removed.push(manifestPath()) }
|
|
696
712
|
console.log(`user scope ← ${base}`)
|
|
697
713
|
for (const r of removed) console.log(` removed ${r}`)
|
|
@@ -1152,6 +1168,51 @@ function claimPhases(dest, stream) {
|
|
|
1152
1168
|
return { streamBranch, integration, remote, behind, phases }
|
|
1153
1169
|
}
|
|
1154
1170
|
|
|
1171
|
+
// SPEC ## Sign-off's model policy: `inherit` (default, also when the line is
|
|
1172
|
+
// absent) or `roles`, for the review tier. Implementation is not on the menu:
|
|
1173
|
+
// it is always wuwei-implementer at modelRoles.implementation's tier
|
|
1174
|
+
function modelPolicy(spec) {
|
|
1175
|
+
const raw = (signOffField(spec ?? '', 'model policy') ?? 'inherit').trim()
|
|
1176
|
+
const v = (raw.match(/\breview\s*[=:]\s*(\w+)/i)?.[1] ?? raw).toLowerCase()
|
|
1177
|
+
const error = /\bimplementation\b/i.test(raw)
|
|
1178
|
+
? `model policy '${raw}': implementation is fixed (wuwei-implementer, ${CONFIG.modelRoles?.implementation?.model} / ${CONFIG.modelRoles?.implementation?.effort}) — the policy is inherit or roles, for review`
|
|
1179
|
+
: !['inherit', 'roles'].includes(v) ? `model policy '${raw}': inherit or roles` : null
|
|
1180
|
+
// Codex, when it runs execute: `codex: <model> / <effort>` — the session it is started as
|
|
1181
|
+
const cx = (spec ?? '').split(/^## Sign-off\s*$/m)[1]?.split(/^## /m)[0]?.split('\n').find(l => /^[-*|\s]*codex\s*[:|]/i.test(l))
|
|
1182
|
+
const cv = cx ? (cx.match(/`([^`]+)`/)?.[1] ?? cx.split(/[:|]/).map(x => x.trim()).filter(Boolean).at(-1)) : null
|
|
1183
|
+
const cm = cv?.match(/^(\S+)\s*\/\s*(\S+)$/)
|
|
1184
|
+
const codex = cm ? { model: cm[1], effort: cm[2] } : null
|
|
1185
|
+
const codexError = cx && !cm ? `codex '${cv}': write it as <model> / <effort>`
|
|
1186
|
+
: codex && !CODEX_EFFORTS.includes(codex.effort) ? `codex effort '${codex.effort}': one of ${CODEX_EFFORTS.join('|')}` : null
|
|
1187
|
+
return { raw, tiers: { review: v }, error: error ?? codexError, codex }
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function codexLine(want) {
|
|
1191
|
+
const { profile } = codexProfile()
|
|
1192
|
+
if (!want) return profile
|
|
1193
|
+
? `codex start it with \`codex --profile ${CODEX_PROFILE}\` (${profile.model} / ${profile.effort}) — none recorded under ## Sign-off`
|
|
1194
|
+
: 'codex the model Codex was started with — no `codex:` under ## Sign-off'
|
|
1195
|
+
return profile?.model === want.model && profile?.effort === want.effort
|
|
1196
|
+
? `codex start it with \`codex --profile ${CODEX_PROFILE}\` (${want.model} / ${want.effort})`
|
|
1197
|
+
: `codex start it with \`${codexLaunch(want.model, want.effort)}\`${profile ? ` — profile ${CODEX_PROFILE} is ${profile.model} / ${profile.effort}, not what ## Sign-off says` : ''}`
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// what step 2 and step 5 dispatch, from the policy and wuwei.config.json's
|
|
1201
|
+
// modelRoles — printed, so the tier is read off the screen, not remembered
|
|
1202
|
+
function dispatchLines(policy) {
|
|
1203
|
+
const roles = CONFIG.modelRoles ?? {}
|
|
1204
|
+
const impl = roles.implementation ?? {}, rev = roles.review ?? {}
|
|
1205
|
+
return [
|
|
1206
|
+
`implement (step 2) subagent_type ${impl.agent}, no model or effort on the call — ${impl.model} / ${impl.effort}, pinned in its agent file`,
|
|
1207
|
+
policy.tiers.review === 'roles'
|
|
1208
|
+
? `${(rev.lenses ?? []).map(l => l.replace(/^wuwei-review-/, 'review:')).join(', ')} (step 5) model: ${rev.model} — effort from the agent file`
|
|
1209
|
+
: `${(rev.lenses ?? []).map(l => l.replace(/^wuwei-review-/, 'review:')).join(', ')} (step 5) no model — the session's model; effort from the agent file`,
|
|
1210
|
+
'other lenses no model — the session\'s; wuwei-security-auditor stays fable / high (pinned)',
|
|
1211
|
+
'inline (no subagents, Codex) the session\'s, whatever the policy — say so in the round record',
|
|
1212
|
+
codexLine(policy.codex),
|
|
1213
|
+
]
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1155
1216
|
function cmdClaim(argv) {
|
|
1156
1217
|
const flag = name => { const i = argv.indexOf(name); return i === -1 ? null : argv[i + 1] }
|
|
1157
1218
|
const positional = argv.filter((a, i) => !a.startsWith('--') && argv[i - 1] !== '--by')
|
|
@@ -1175,6 +1236,10 @@ function cmdClaim(argv) {
|
|
|
1175
1236
|
for (const p of r.phases)
|
|
1176
1237
|
console.log(` ${p.name.padEnd(w)} ${p.state}${p.review === 'human' ? ' ← review: human' : ''}`)
|
|
1177
1238
|
|
|
1239
|
+
const policy = modelPolicy(existsSync(join(streamsDir, stream, 'SPEC.md')) ? readFileSync(join(streamsDir, stream, 'SPEC.md'), 'utf8') : '')
|
|
1240
|
+
if (policy.error) { console.error(`\n✗ ${policy.error} — fix ## Sign-off (wuwei plan-check)`); process.exit(1) }
|
|
1241
|
+
console.log(`\ndispatch · model policy: ${policy.raw}\n${dispatchLines(policy).map(l => ` ${l}`).join('\n')}`)
|
|
1242
|
+
|
|
1178
1243
|
if (!phaseArg) {
|
|
1179
1244
|
const ready = r.phases.filter(p => p.state === 'ready')
|
|
1180
1245
|
console.log(ready.length ? `\nclaim one: wuwei claim ${stream} ${ready[0].no}` : '\nnothing ready to claim')
|
|
@@ -1283,6 +1348,8 @@ function cmdPlanCheck(argv) {
|
|
|
1283
1348
|
const open = signLines.filter(l => /:\s*(\?|tbd|todo|pending|—|-|<[^>]+>)?\s*$/i.test(l) && /\w/.test(l.split(':')[0]) && /^\s*[-*|]/.test(l))
|
|
1284
1349
|
const auto = signLines.some(l => /\bmode\b[^|\n]*\bauto\b/i.test(l))
|
|
1285
1350
|
if (auto && open.length) errs.push(`drive mode is auto while ## Sign-off still has open fields: ${open.map(l => l.trim()).join(' / ')} — a partly answered sign-off stays manual`)
|
|
1351
|
+
const policy = spec ? modelPolicy(spec) : null
|
|
1352
|
+
if (policy?.error) errs.push(`## Sign-off: ${policy.error}`)
|
|
1286
1353
|
const tracker = signLines.find(l => /tracker/i.test(l)) ?? ''
|
|
1287
1354
|
const linked = tracker && !/\bnone\b/i.test(tracker)
|
|
1288
1355
|
|
|
@@ -1777,6 +1844,90 @@ async function cmdVerify(argv) {
|
|
|
1777
1844
|
console.log('\nflags are leads, not findings: open the shot, and confirm against the spec what the page should say')
|
|
1778
1845
|
}
|
|
1779
1846
|
|
|
1847
|
+
// ── review-collect: the lenses' own record, from Claude Code's transcripts ──
|
|
1848
|
+
// Claude Code keeps every subagent's transcript under ~/.claude/projects/<cwd,
|
|
1849
|
+
// non-alphanumerics as '-'>/<session>/subagents/agent-<id>.jsonl, its type in
|
|
1850
|
+
// the .meta.json beside it. §7 used to have the collator re-type each report
|
|
1851
|
+
// into the bundle — skipped in practice, and paid for in tokens when not. This
|
|
1852
|
+
// copies them, and reads what each one spent
|
|
1853
|
+
function transcriptStats(file) {
|
|
1854
|
+
const usage = new Map(), uses = new Map(), reads = new Map()
|
|
1855
|
+
const t = { turns: 0, built: 0, reread: 0, input: 0, output: 0, first: null, last: null, report: '' }
|
|
1856
|
+
const kind = (name, inp = {}) => name === 'Read' ? (/\.wuwei-cache/.test(inp.file_path ?? '') ? 'Read:bundle' : 'Read:tree')
|
|
1857
|
+
: name === 'Bash' ? (/codegraph/.test(inp.command ?? '') ? 'Bash:codegraph' : /\b(git )?grep\b|\brg\b/.test(inp.command ?? '') ? 'Bash:grep' : 'Bash:other') : name
|
|
1858
|
+
for (const line of readFileSync(file, 'utf8').split('\n')) {
|
|
1859
|
+
let d; try { d = JSON.parse(line) } catch { continue }
|
|
1860
|
+
if (d.timestamp) { t.first ??= d.timestamp; t.last = d.timestamp }
|
|
1861
|
+
const m = d.message
|
|
1862
|
+
if (!m || typeof m !== 'object') continue
|
|
1863
|
+
// one message arrives as several lines while it streams: its last usage is its total
|
|
1864
|
+
if (d.type === 'assistant' && m.usage) usage.set(m.id, m.usage)
|
|
1865
|
+
for (const x of Array.isArray(m.content) ? m.content : []) {
|
|
1866
|
+
if (x.type === 'tool_use') {
|
|
1867
|
+
uses.set(x.id, kind(x.name, x.input))
|
|
1868
|
+
if (x.name === 'SubagentHandback' && x.input?.message) t.report = x.input.message
|
|
1869
|
+
} else if (x.type === 'tool_result') {
|
|
1870
|
+
const k = uses.get(x.tool_use_id) ?? '?', r = reads.get(k) ?? { n: 0, tokens: 0 }
|
|
1871
|
+
r.n++; r.tokens += Math.round(JSON.stringify(x.content ?? '').length / 4); reads.set(k, r)
|
|
1872
|
+
} else if (x.type === 'text' && d.type === 'assistant' && x.text?.trim()) t.lastText = x.text
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
for (const u of usage.values()) {
|
|
1876
|
+
t.turns++; t.built += u.cache_creation_input_tokens ?? 0; t.reread += u.cache_read_input_tokens ?? 0
|
|
1877
|
+
t.input += u.input_tokens ?? 0; t.output += u.output_tokens ?? 0
|
|
1878
|
+
}
|
|
1879
|
+
t.report ||= t.lastText ?? ''
|
|
1880
|
+
t.reads = [...reads].sort((a, b) => b[1].tokens - a[1].tokens)
|
|
1881
|
+
return t
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
function cmdReviewCollect(argv) {
|
|
1885
|
+
const flag = name => { const i = argv.indexOf(name); return i === -1 ? null : argv[i + 1] }
|
|
1886
|
+
const projects = join(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude'), 'projects')
|
|
1887
|
+
const projDir = join(projects, process.cwd().replace(/[^A-Za-z0-9]/g, '-'))
|
|
1888
|
+
if (!existsSync(projDir)) { console.error(`✗ no Claude Code sessions recorded for ${process.cwd()} (${projDir}) — run it where the review ran (Codex has no subagent transcripts)`); process.exit(1) }
|
|
1889
|
+
const sessions = readdirSync(projDir, { withFileTypes: true }).filter(d => d.isDirectory() && existsSync(join(projDir, d.name, 'subagents')))
|
|
1890
|
+
.map(d => {
|
|
1891
|
+
const dir = join(projDir, d.name, 'subagents')
|
|
1892
|
+
const agents = readdirSync(dir).filter(f => f.endsWith('.meta.json')).map(f => {
|
|
1893
|
+
let meta = {}; try { meta = JSON.parse(readFileSync(join(dir, f), 'utf8')) } catch {}
|
|
1894
|
+
const jsonl = join(dir, f.replace(/\.meta\.json$/, '.jsonl'))
|
|
1895
|
+
return { meta, jsonl, mtime: existsSync(jsonl) ? statSync(jsonl).mtimeMs : 0 }
|
|
1896
|
+
}).filter(a => a.mtime)
|
|
1897
|
+
return { id: d.name, agents, wuwei: agents.some(a => /^wuwei-/.test(a.meta.agentType ?? '')), latest: Math.max(0, ...agents.map(a => a.mtime)) }
|
|
1898
|
+
})
|
|
1899
|
+
const session = flag('--session') ? sessions.find(s => s.id.startsWith(flag('--session')))
|
|
1900
|
+
: sessions.filter(s => s.wuwei).sort((a, b) => b.latest - a.latest)[0]
|
|
1901
|
+
if (!session) { console.error(`✗ ${flag('--session') ? `no session ${flag('--session')}` : 'no session here dispatched a wuwei lens'} under ${projDir}`); process.exit(1) }
|
|
1902
|
+
|
|
1903
|
+
const sha = flag('--sha') ?? quiet('git', ['rev-parse', '--verify', '--quiet', 'HEAD']).stdout?.trim().slice(0, 12) ?? 'nogit'
|
|
1904
|
+
const out = join(process.cwd(), '.wuwei-cache', 'review', sha, 'reports')
|
|
1905
|
+
mkdirSync(out, { recursive: true })
|
|
1906
|
+
const rows = [], used = new Map()
|
|
1907
|
+
for (const a of session.agents.map(a => ({ ...a, s: transcriptStats(a.jsonl) })).sort((x, y) => (x.s.first ?? '').localeCompare(y.s.first ?? ''))) {
|
|
1908
|
+
const type = a.meta.agentType ?? 'agent'
|
|
1909
|
+
const base = (type === 'general-purpose' && a.meta.description ? a.meta.description.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 30) : type).replace(/^wuwei-/, '')
|
|
1910
|
+
const n = (used.get(base) ?? 0) + 1; used.set(base, n)
|
|
1911
|
+
const name = n === 1 ? base : `${base}-${n}`
|
|
1912
|
+
writeFileSync(join(out, `${name}.jsonl`), readFileSync(a.jsonl))
|
|
1913
|
+
if (a.s.report) writeFileSync(join(out, `${name}.md`), a.s.report.trimEnd() + '\n')
|
|
1914
|
+
rows.push({ name, ...a.s })
|
|
1915
|
+
}
|
|
1916
|
+
const k = x => x >= 1e6 ? `${(x / 1e6).toFixed(1)}M` : x >= 1e3 ? `${Math.round(x / 1e3)}k` : String(x)
|
|
1917
|
+
const total = rows.reduce((t, r) => ({ built: t.built + r.built, reread: t.reread + r.reread }), { built: 0, reread: 0 })
|
|
1918
|
+
console.log(`session ${session.id} · ${rows.length} subagent(s) → ${out}\n`)
|
|
1919
|
+
// output tokens are left out: the transcript records a streaming chunk's count, not the message's
|
|
1920
|
+
console.log('| agent | turns | context built | re-read (cached) | where the reading went |\n|---|---|---|---|---|')
|
|
1921
|
+
for (const r of [...rows].sort((a, b) => (b.built + b.reread / 10) - (a.built + a.reread / 10)))
|
|
1922
|
+
console.log(`| ${r.name} | ${r.turns} | ${k(r.built)} | ${k(r.reread)} | ${r.reads.slice(0, 4).map(([kk, v]) => `${kk} ${v.n}× ≈${k(v.tokens)}`).join(' · ') || '—'} |`)
|
|
1923
|
+
console.log(`| **total** | | ${k(total.built)} | ${k(total.reread)} | |`)
|
|
1924
|
+
const byKind = new Map()
|
|
1925
|
+
for (const r of rows) for (const [kk, v] of r.reads) byKind.set(kk, (byKind.get(kk) ?? 0) + v.tokens)
|
|
1926
|
+
const top = [...byKind].sort((a, b) => b[1] - a[1]).slice(0, 3)
|
|
1927
|
+
if (top.length) console.log(`\nmost read, across the lenses: ${top.map(([kk, v]) => `${kk} ≈${k(v)}`).join(' · ')}`)
|
|
1928
|
+
console.log('re-read is billed at about a tenth of context built; a lens with many turns pays its context again on each')
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1780
1931
|
// ── digest-check: wuwei-review §7's arithmetic, before publishing ─────
|
|
1781
1932
|
// what the collator re-derives by hand: the header count against the table,
|
|
1782
1933
|
// reporter credits, every file:line against the reviewed tree, every
|
|
@@ -2620,6 +2771,59 @@ async function promptYesNo(question, def = false) {
|
|
|
2620
2771
|
}
|
|
2621
2772
|
}
|
|
2622
2773
|
|
|
2774
|
+
async function promptText(question, def = '') {
|
|
2775
|
+
if (!isInteractive()) return def
|
|
2776
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
2777
|
+
try { return (await rl.question(`${question}${def ? ` [${def}]` : ''} `)).trim() || def } catch { return def } finally { rl.close() }
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
// ── Codex: a skill cannot set its model — the session is started with one.
|
|
2781
|
+
// So execute's tier on Codex is a profile in $CODEX_HOME/config.toml that the
|
|
2782
|
+
// session is launched under (codex --profile wuwei-execute); init offers to
|
|
2783
|
+
// write it, claim prints how to start. Effort values: codex-rs ReasoningEffort
|
|
2784
|
+
const CODEX_PROFILE = 'wuwei-execute'
|
|
2785
|
+
const CODEX_EFFORTS = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra', 'persistent']
|
|
2786
|
+
const CODEX_PROFILE_MARK = '# added by wuwei init — wuwei-execute-phase runs under this profile; `wuwei uninstall` removes it'
|
|
2787
|
+
const codexConfigPath = () => join(codexHome(), 'config.toml')
|
|
2788
|
+
// [profiles.wuwei-execute]'s model and effort, or null; plus the top-level model
|
|
2789
|
+
function codexProfile() {
|
|
2790
|
+
const text = existsSync(codexConfigPath()) ? readFileSync(codexConfigPath(), 'utf8') : ''
|
|
2791
|
+
const sec = text.split(/^(?=\[)/m).find(s => new RegExp(`^\\[profiles\\.(?:"${CODEX_PROFILE}"|${CODEX_PROFILE})\\]`).test(s))
|
|
2792
|
+
const val = (s, k) => s?.match(new RegExp(`^${k}\\s*=\\s*"([^"]*)"`, 'm'))?.[1] ?? null
|
|
2793
|
+
const top = text.split(/^\[/m)[0]
|
|
2794
|
+
return { profile: sec ? { model: val(sec, 'model'), effort: val(sec, 'model_reasoning_effort') } : null, topModel: val(top, 'model') }
|
|
2795
|
+
}
|
|
2796
|
+
const codexLaunch = (model, effort) => `codex -m ${model} -c model_reasoning_effort=${effort}`
|
|
2797
|
+
|
|
2798
|
+
async function offerCodexProfile(dry) {
|
|
2799
|
+
if (!existsSync(codexHome())) return
|
|
2800
|
+
const { profile, topModel } = codexProfile()
|
|
2801
|
+
if (profile) { console.log(`
|
|
2802
|
+
codex: profile ${CODEX_PROFILE} — ${profile.model ?? '(model unset)'} / ${profile.effort ?? '(effort unset)'} · start execute with \`codex --profile ${CODEX_PROFILE}\``); return }
|
|
2803
|
+
const snippet = m => [CODEX_PROFILE_MARK, `[profiles.${CODEX_PROFILE}]`, `model = "${m.model}"`, `model_reasoning_effort = "${m.effort}"`].join('\n')
|
|
2804
|
+
console.log(`
|
|
2805
|
+
codex: a skill can't choose Codex's model — the session is started with one. wuwei-execute-phase runs under a profile you launch it with.`)
|
|
2806
|
+
if (!isInteractive()) { console.log(` add to ${codexConfigPath()}:\n${snippet({ model: '<model>', effort: 'xhigh' }).replace(/^/gm, ' ')}`); return }
|
|
2807
|
+
if (!await promptYesNo(` Add [profiles.${CODEX_PROFILE}] to ${codexConfigPath()}?`, true)) { console.log(' skipped — re-run `wuwei init` anytime'); return }
|
|
2808
|
+
const model = await promptText(' model for execute:', topModel ?? '')
|
|
2809
|
+
let effort = await promptText(` reasoning effort (${CODEX_EFFORTS.join('|')}):`, 'xhigh')
|
|
2810
|
+
if (!model || !CODEX_EFFORTS.includes(effort)) { console.log(` ✗ ${!model ? 'no model given' : `effort must be one of ${CODEX_EFFORTS.join('|')}`} — nothing written`); return }
|
|
2811
|
+
if (dry) { console.log(` [dry] would append:\n${snippet({ model, effort }).replace(/^/gm, ' ')}`); return }
|
|
2812
|
+
const prev = existsSync(codexConfigPath()) ? readFileSync(codexConfigPath(), 'utf8') : ''
|
|
2813
|
+
writeFileSync(codexConfigPath(), (prev ? prev.replace(/\n*$/, '\n\n') : '') + snippet({ model, effort }) + '\n')
|
|
2814
|
+
console.log(` ✓ written · start execute with \`codex --profile ${CODEX_PROFILE}\`, and write \`codex: ${model} / ${effort}\` under the stream's ## Sign-off`)
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
// only the section init wrote (its marker line above it), never one you made
|
|
2818
|
+
function removeCodexProfile() {
|
|
2819
|
+
if (!existsSync(codexConfigPath())) return false
|
|
2820
|
+
const text = readFileSync(codexConfigPath(), 'utf8')
|
|
2821
|
+
const re = new RegExp(`\\n*${escapeRe(CODEX_PROFILE_MARK)}\\n\\[profiles\\.${CODEX_PROFILE}\\][^\\[]*`)
|
|
2822
|
+
if (!re.test(text)) return false
|
|
2823
|
+
writeFileSync(codexConfigPath(), text.replace(re, '\n').replace(/^\n+/, '').replace(/\n*$/, '\n'))
|
|
2824
|
+
return true
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2623
2827
|
async function cmdInit(target, dry) {
|
|
2624
2828
|
// bootstrap a project: repo-scope install into the target (default: cwd) —
|
|
2625
2829
|
// the entrypoint `npx <pkg> init` runs from anywhere, package files as ROOT
|
|
@@ -2664,6 +2868,7 @@ async function cmdInit(target, dry) {
|
|
|
2664
2868
|
|
|
2665
2869
|
await offerCodegraphSetup(dest, dry, existing)
|
|
2666
2870
|
await offerScannerSetup(dry)
|
|
2871
|
+
await offerCodexProfile(dry)
|
|
2667
2872
|
|
|
2668
2873
|
const installHint = isSkillsInstalled()
|
|
2669
2874
|
? ''
|
|
@@ -2699,6 +2904,7 @@ switch (cmd) {
|
|
|
2699
2904
|
case 'update': await cmdUpdate(dry, rest.includes('--no-fetch')); break
|
|
2700
2905
|
case 'status': cmdStatus(arg); break
|
|
2701
2906
|
case 'review-prep': cmdReviewPrep(rest); break
|
|
2907
|
+
case 'review-collect': cmdReviewCollect(rest); break
|
|
2702
2908
|
case 'verify': await cmdVerify(rest); break
|
|
2703
2909
|
case 'claim': cmdClaim(rest); break
|
|
2704
2910
|
case 'digest-check': cmdDigestCheck(rest); break
|
|
@@ -2708,6 +2914,6 @@ switch (cmd) {
|
|
|
2708
2914
|
case 'plan-check': cmdPlanCheck(rest); break
|
|
2709
2915
|
case 'uninstall': await cmdUninstall(arg); break
|
|
2710
2916
|
default:
|
|
2711
|
-
console.error(`unknown command '${cmd}'\nusage: wuwei [build|check|list|init [path]|install [path]|update|status [path]|review-prep [base] [--pr n] [--path p]|verify [page] [--phase NN]|claim [stream] [phase]|digest-check <digest.md>|worktree <lens> [sha]|mutate <file>:<line>|sites <pattern>|plan-check [stream]|uninstall [path]|--version] [--dry]`)
|
|
2917
|
+
console.error(`unknown command '${cmd}'\nusage: wuwei [build|check|list|init [path]|install [path]|update|status [path]|review-prep [base] [--pr n] [--path p]|review-collect|verify [page] [--phase NN]|claim [stream] [phase]|digest-check <digest.md>|worktree <lens> [sha]|mutate <file>:<line>|sites <pattern>|plan-check [stream]|uninstall [path]|--version] [--dry]`)
|
|
2712
2918
|
process.exit(1)
|
|
2713
2919
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wei-wuwei",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "為無為,則無不治 — a source-of-truth skills & agents framework that compiles to Claude Code and Codex (Cursor and Gemini CLI planned). Write once, run in every agent, govern without forcing.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,8 @@ description: >-
|
|
|
9
9
|
simplify/review/fix → PR — one phase, or a range on request.
|
|
10
10
|
version: 0.2.0
|
|
11
11
|
mode: triggered
|
|
12
|
+
model: sonnet
|
|
13
|
+
effort: xhigh
|
|
12
14
|
targets: [claude-code, codex]
|
|
13
15
|
tags: [execution, workflow]
|
|
14
16
|
---
|
|
@@ -98,9 +100,12 @@ Stopping early is normal — say where, why, and what resumes:
|
|
|
98
100
|
```
|
|
99
101
|
stopped at 03-appcheck-wiring · bound e2e red (auth/refresh.spec.ts)
|
|
100
102
|
02 merged. 03 claimed, branch pushed, not merged.
|
|
101
|
-
next invocation resumes at 03
|
|
103
|
+
next invocation resumes at 03: /wuwei-execute-phase
|
|
102
104
|
```
|
|
103
105
|
|
|
106
|
+
Name the invocation: this skill's sonnet / xhigh holds for the turn it runs
|
|
107
|
+
in, and a reply in chat continues on the session's model.
|
|
108
|
+
|
|
104
109
|
**Scope is distance, never permission.** `mode` is the stream's standing
|
|
105
110
|
authorisation from sign-off; scope is only how far one invocation tries to
|
|
106
111
|
get.
|
|
@@ -152,8 +157,9 @@ get.
|
|
|
152
157
|
context, **the ladder below**, and **step 4's floor** — and nothing else.
|
|
153
158
|
The ladder is a question the subagent writing the helper has to ask; an
|
|
154
159
|
orchestrator holding it while the subagent writes is the same as not having
|
|
155
|
-
it. Dispatch
|
|
156
|
-
|
|
160
|
+
it. Dispatch `wuwei-implementer` with **no `model` or `effort` on the
|
|
161
|
+
call** — its file pins sonnet / xhigh, and a value on the call beats it.
|
|
162
|
+
Inline instead → watch for
|
|
157
163
|
drift, keep the phase small, and note that the model policy has nothing to
|
|
158
164
|
attach to.
|
|
159
165
|
|
|
@@ -333,8 +339,7 @@ get.
|
|
|
333
339
|
`ACCEPTANCE.md`, the **base ref just synced to**, the **tool facts**
|
|
334
340
|
(`wuwei-review` §1, §5), and **the scope** (`wuwei-review` §6) — plus, for the
|
|
335
341
|
document panel, **the intended reader and the facts this phase
|
|
336
|
-
establishes** (`wuwei-review` §3). Models:
|
|
337
|
-
`## Sign-off` policy for design and defects, the session's for the rest
|
|
342
|
+
establishes** (`wuwei-review` §3). Models: as `wuwei claim` printed them
|
|
338
343
|
(`wuwei-review` §2).
|
|
339
344
|
|
|
340
345
|
**Round 1 is full scope; every round after it is delta** — dispatched with
|
|
@@ -202,15 +202,18 @@ tags: [planning, workflow]
|
|
|
202
202
|
- **merge method**: unsquashed by default — a merge commit — so ask only
|
|
203
203
|
whether this stream squashes instead, plus which methods the repo allows
|
|
204
204
|
(table below);
|
|
205
|
-
- **model policy
|
|
205
|
+
- **model policy** for review: `inherit` (default) | `roles`. Ask only
|
|
206
206
|
whether to change the default;
|
|
207
|
+
- **Codex**, when it runs execute: the model and effort it is started
|
|
208
|
+
with — `codex: <model> / <effort>`. A skill cannot set Codex's model;
|
|
209
|
+
`wuwei claim` prints the launch (`wuwei init` offers the profile);
|
|
207
210
|
- whether the plan goes through **its own PR** first — if so, steps 8–9
|
|
208
211
|
wait for that PR to merge.
|
|
209
212
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
213
|
+
Implementation is not on the menu: execute runs, and dispatches its
|
|
214
|
+
implementer, at sonnet / xhigh (`wuwei.config.json` `modelRoles`). The
|
|
215
|
+
policy sets **review** — step 5's `review:design` and `review:defects`,
|
|
216
|
+
which dispatch as subagents. The panel's other lenses inherit the session's
|
|
214
217
|
model, **except `wuwei-security-auditor`**, which pins `fable` / `high` in
|
|
215
218
|
frontmatter and holds under every policy including `inherit`. Say so,
|
|
216
219
|
rather than letting a `roles` answer imply it was chosen.
|
|
@@ -31,5 +31,5 @@ slices they need:
|
|
|
31
31
|
diff.patch git diff <base>...HEAD, three dots, verbatim
|
|
32
32
|
symbols.md definitions the diff references, from `codegraph node` — or "graph absent"
|
|
33
33
|
tools/<tool>.json each detected tool's read-only output over the change (§5), run once
|
|
34
|
-
reports/<lens>.md each lens's raw report
|
|
34
|
+
reports/<lens>.md each lens's raw report — `wuwei review-collect`, or written as returned
|
|
35
35
|
```
|
|
@@ -102,7 +102,7 @@ disk**; lenses pull the slices they need:
|
|
|
102
102
|
```
|
|
103
103
|
.wuwei-cache/review/<reviewed-sha>/ # gitignored — never inside the reviewed tree
|
|
104
104
|
PREP.md · INDEX.md (kind is a first pass by path — reclassify by content) · diff.patch
|
|
105
|
-
thread.json · tools/<tool>.json · reports/<lens>.md
|
|
105
|
+
thread.json · tools/<tool>.json · reports/<lens>.md + .jsonl (`wuwei review-collect`)
|
|
106
106
|
```
|
|
107
107
|
|
|
108
108
|
**No copies of the changed files** — each lens's context holds what it reads
|
|
@@ -346,9 +346,9 @@ finding closes.
|
|
|
346
346
|
|
|
347
347
|
## 7. Collate
|
|
348
348
|
|
|
349
|
-
The collator is the dispatching session.
|
|
350
|
-
|
|
351
|
-
|
|
349
|
+
The collator is the dispatching session. **`wuwei review-collect`** first
|
|
350
|
+
copies each lens's transcript and report into the bundle's `reports/` —
|
|
351
|
+
never re-typed (no subagents, as on Codex: write them yourself). Then one digest
|
|
352
352
|
against `.wuwei/templates/code-review.md`, opening with which
|
|
353
353
|
panels ran and why. **Whichever panels ran share one table** — code and
|
|
354
354
|
document on a mixed target, or the stream panel's five reporters plus §2 on
|
package/wuwei.config.json
CHANGED
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
"failOnDrift": true,
|
|
17
17
|
"stampGeneratedHeader": true
|
|
18
18
|
},
|
|
19
|
+
"modelRoles": {
|
|
20
|
+
"implementation": { "skill": "wuwei-execute-phase", "agent": "wuwei-implementer", "model": "sonnet", "effort": "xhigh" },
|
|
21
|
+
"review": { "lenses": ["wuwei-review-design", "wuwei-review-defects"], "model": "opus" }
|
|
22
|
+
},
|
|
19
23
|
"wordBudgets": {
|
|
20
24
|
"skills/wuwei-brainstorm/skill.md": 500,
|
|
21
25
|
"skills/wuwei-commit-hygiene/skill.md": 200,
|