wei-wuwei 0.2.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 CHANGED
@@ -4,6 +4,14 @@ 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
+
7
15
  ## 0.2.0
8
16
 
9
17
  Execute runs at a fixed tier; the model policy now covers review only.
package/README.md CHANGED
@@ -67,6 +67,7 @@ node bin/wuwei.js status <project> # read-only: streams/claims from .wuwei, dr
67
67
  node bin/wuwei.js update # git pull if behind, diff against what install wrote, apply on yes
68
68
  node bin/wuwei.js review-prep <base> # wuwei-review's prep: dispatcher checks + scan bundle (or --pr <n>)
69
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
70
71
  node bin/wuwei.js digest-check <f> # a review digest's arithmetic before it is published
71
72
  node bin/wuwei.js plan-check [stream] # plan-phases step 11's checkable half, before sign-off
72
73
  node bin/wuwei.js worktree <lens> <sha> # a throwaway worktree no other lens shares (--prune)
@@ -92,8 +93,16 @@ npm in the background, and when a newer version is out it says so on stderr
92
93
  (at most hourly; never from a clone or in CI; `WUWEI_NO_UPDATE_CHECK=1` turns
93
94
  it off).
94
95
 
95
- Releasing: bump `package.json`, add its `## <version>` section to
96
- `CHANGELOG.md` (`wuwei check` fails without one), push a `v<version>` tag.
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.
97
106
 
98
107
  ## Daily use
99
108
 
@@ -189,7 +198,7 @@ wuwei/
189
198
  ├── agents/ # canonical subagent definitions
190
199
  │ └── _template/
191
200
  ├── templates/ # PR / review templates, installed per-repo
192
- ├── 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
193
202
  │ # the per-tool emitters live here too, not in adapters/
194
203
  ├── adapters/ # notes only — see bin/wuwei.js for the actual emitters
195
204
  ├── docs/
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
@@ -1842,6 +1844,90 @@ async function cmdVerify(argv) {
1842
1844
  console.log('\nflags are leads, not findings: open the shot, and confirm against the spec what the page should say')
1843
1845
  }
1844
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
+
1845
1931
  // ── digest-check: wuwei-review §7's arithmetic, before publishing ─────
1846
1932
  // what the collator re-derives by hand: the header count against the table,
1847
1933
  // reporter credits, every file:line against the reviewed tree, every
@@ -2818,6 +2904,7 @@ switch (cmd) {
2818
2904
  case 'update': await cmdUpdate(dry, rest.includes('--no-fetch')); break
2819
2905
  case 'status': cmdStatus(arg); break
2820
2906
  case 'review-prep': cmdReviewPrep(rest); break
2907
+ case 'review-collect': cmdReviewCollect(rest); break
2821
2908
  case 'verify': await cmdVerify(rest); break
2822
2909
  case 'claim': cmdClaim(rest); break
2823
2910
  case 'digest-check': cmdDigestCheck(rest); break
@@ -2827,6 +2914,6 @@ switch (cmd) {
2827
2914
  case 'plan-check': cmdPlanCheck(rest); break
2828
2915
  case 'uninstall': await cmdUninstall(arg); break
2829
2916
  default:
2830
- 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]`)
2831
2918
  process.exit(1)
2832
2919
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wei-wuwei",
3
- "version": "0.2.0",
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": {
@@ -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, as returned — never the session scratchpad
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 (each lens's raw report, as returned)
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. It first writes each lens's report
350
- into the bundle's `reports/<lens>.md` as returned — the harness's output file,
351
- copied, not re-typed. Then one digest
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