wei-wuwei 0.3.0 → 0.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 +8 -0
- package/README.md +3 -2
- package/agents/wuwei-audit-composition/agent.md +4 -0
- package/agents/wuwei-audit-contract/agent.md +4 -0
- package/agents/wuwei-audit-coverage/agent.md +4 -0
- package/agents/wuwei-pre-implementation-doubt/agent.md +4 -0
- package/agents/wuwei-review-claims/agent.md +8 -0
- package/agents/wuwei-review-code/agent.md +4 -0
- package/agents/wuwei-review-defects/agent.md +4 -0
- package/agents/wuwei-review-design/agent.md +4 -0
- package/agents/wuwei-review-drift/agent.md +4 -0
- package/agents/wuwei-review-structure/agent.md +3 -0
- package/agents/wuwei-security-auditor/agent.md +4 -0
- package/bin/wuwei.js +149 -13
- package/package.json +1 -1
- package/skills/wuwei-plan-phases/skill.md +5 -7
- package/skills/wuwei-review/references/lens-rules.md +1 -0
- package/wuwei.config.json +1 -1
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.5.0
|
|
8
|
+
|
|
9
|
+
- feat(plan-check): a script is an e2e harness too (#69)
|
|
10
|
+
|
|
11
|
+
## 0.4.0
|
|
12
|
+
|
|
13
|
+
- feat: review turns — claims-scan, batched lens checks, review-collect breakdown (#68)
|
|
14
|
+
|
|
7
15
|
## 0.3.0
|
|
8
16
|
|
|
9
17
|
- wuwei verify on mobile: Maestro on a simulator + wuwei's mock server (#62)
|
package/README.md
CHANGED
|
@@ -72,7 +72,8 @@ node bin/wuwei.js digest-check <f> # a review digest's arithmetic before it is
|
|
|
72
72
|
node bin/wuwei.js plan-check [stream] # plan-phases step 11's checkable half, before sign-off
|
|
73
73
|
node bin/wuwei.js worktree <lens> <sha> # a throwaway worktree no other lens shares (--prune)
|
|
74
74
|
node bin/wuwei.js mutate <f>:<n> --from … --to … --run … --in <wt> # one mutation: landed, baseline, red count, restored
|
|
75
|
-
node bin/wuwei.js sites <pattern
|
|
75
|
+
node bin/wuwei.js sites <pattern>… [--symbol <name>] # a set derived from the tree, graph and grep both; several names in one call
|
|
76
|
+
node bin/wuwei.js claims-scan <doc>… # a document's paths, names, file:lines and commands checked against the tree in one call
|
|
76
77
|
node bin/wuwei.js verify [page] [--phase NN] # every data-bearing page × 500/403/404/empty/pending/failed refetch; --phase: ACCEPTANCE's ## Verify; mobile: Maestro + mock server
|
|
77
78
|
node bin/wuwei.js uninstall # removes exactly what install wrote, nothing else
|
|
78
79
|
```
|
|
@@ -198,7 +199,7 @@ wuwei/
|
|
|
198
199
|
├── agents/ # canonical subagent definitions
|
|
199
200
|
│ └── _template/
|
|
200
201
|
├── templates/ # PR / review templates, installed per-repo
|
|
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
|
|
202
|
+
├── bin/wuwei.js # CLI: build · check · list · init · install · update · status · review-prep · review-collect · claim · digest-check · plan-check · worktree · mutate · sites · claims-scan · verify · uninstall
|
|
202
203
|
│ # the per-tool emitters live here too, not in adapters/
|
|
203
204
|
├── adapters/ # notes only — see bin/wuwei.js for the actual emitters
|
|
204
205
|
├── docs/
|
|
@@ -56,6 +56,10 @@ Per-phase tidiness already has two owners — `wuwei-execute-phase`'s simplify
|
|
|
56
56
|
step and `review:defects` — and a smell list is the long tail wuwei declines to
|
|
57
57
|
chase. If one phase's diff shows it, it is not yours.
|
|
58
58
|
|
|
59
|
+
**Turns are the cost**: every tool call re-reads your whole context. Put
|
|
60
|
+
independent checks in one Bash call (`a; b; c`), and several names in one
|
|
61
|
+
`wuwei sites a b c`.
|
|
62
|
+
|
|
59
63
|
## Out of scope
|
|
60
64
|
|
|
61
65
|
- **Changing anything.** No Edit or Write; `Bash` reads (`git diff` of the
|
|
@@ -67,6 +67,10 @@ agent's judgment can act on or block on. This section is input to the *next*
|
|
|
67
67
|
plan, not a gate on this one — `## Sign-off`'s decision stands until someone
|
|
68
68
|
with sign-off authority revisits it.
|
|
69
69
|
|
|
70
|
+
**Turns are the cost**: every tool call re-reads your whole context. Put
|
|
71
|
+
independent checks in one Bash call (`a; b; c`), and several names in one
|
|
72
|
+
`wuwei sites a b c`.
|
|
73
|
+
|
|
70
74
|
## Out of scope
|
|
71
75
|
|
|
72
76
|
- **Changing anything.** No Edit or Write; `Bash` reads (`git diff`, `git
|
|
@@ -65,6 +65,10 @@ fully covered.**
|
|
|
65
65
|
**No recorded run is a broken link**, not a pass — the same rule the phase
|
|
66
66
|
pipeline gates on, and **you do not close the gap by running it yourself**.
|
|
67
67
|
|
|
68
|
+
**Turns are the cost**: every tool call re-reads your whole context. Put
|
|
69
|
+
independent checks in one Bash call (`a; b; c`), and several names in one
|
|
70
|
+
`wuwei sites a b c`.
|
|
71
|
+
|
|
68
72
|
## Out of scope
|
|
69
73
|
|
|
70
74
|
- **Changing anything, and that includes running the suite.** No Edit or
|
|
@@ -22,6 +22,10 @@ targets: [claude-code]
|
|
|
22
22
|
coverage。你沒有派工工具:一個視角寫完它的發現,才開下一個,不混著看。審查對象是 stream 的 `SPEC.md` 與 `phases/*.md`,時點在驗收
|
|
23
23
|
契約簽收**之前**:你放行的東西,user 才簽字承擔。
|
|
24
24
|
|
|
25
|
+
**Turns are the cost**: every tool call re-reads your whole context. Put
|
|
26
|
+
independent checks in one Bash call (`a; b; c`), and several names in one
|
|
27
|
+
`wuwei sites a b c`.
|
|
28
|
+
|
|
25
29
|
## Out of scope
|
|
26
30
|
|
|
27
31
|
- **改檔。** 你只有 Read / Grep / Glob,`Bash` 只能查(`git fetch`、
|
|
@@ -28,6 +28,10 @@ Not "is it well written" and not "is it complete" — those belong to
|
|
|
28
28
|
assertion about: the code, the config, the runbook, the ticket, the number's
|
|
29
29
|
source. **A number with no source is a finding.**
|
|
30
30
|
|
|
31
|
+
**Start with `wuwei claims-scan <doc>… --sha <sha>`**: every path, name,
|
|
32
|
+
`file:line` and command in the document, checked in one call. By hand: what
|
|
33
|
+
it lists for judgment, and every assertion no code span carries.
|
|
34
|
+
|
|
31
35
|
**A count carries its derivation** — the set you started from and how you
|
|
32
36
|
narrowed it; a bare count is `unsupported` even when right.
|
|
33
37
|
|
|
@@ -136,6 +140,10 @@ with its derivation), **not from the names the document
|
|
|
136
140
|
handed you** — those are the claim, not the evidence — and report the
|
|
137
141
|
difference.
|
|
138
142
|
|
|
143
|
+
**Turns are the cost**: every tool call re-reads your whole context. Put
|
|
144
|
+
independent checks in one Bash call (`a; b; c`), and several names in one
|
|
145
|
+
`wuwei sites a b c`.
|
|
146
|
+
|
|
139
147
|
## Out of scope
|
|
140
148
|
|
|
141
149
|
- **Changing the dispatched tree.** No Edit or Write; `Bash` reads it
|
|
@@ -126,6 +126,10 @@ agent exists to avoid:
|
|
|
126
126
|
The last one is the hardest to self-check: a downgrade leaves no trace, and
|
|
127
127
|
the downgraded finding ships.
|
|
128
128
|
|
|
129
|
+
**Turns are the cost**: every tool call re-reads your whole context. Put
|
|
130
|
+
independent checks in one Bash call (`a; b; c`), and several names in one
|
|
131
|
+
`wuwei sites a b c`.
|
|
132
|
+
|
|
129
133
|
## Out of scope
|
|
130
134
|
|
|
131
135
|
- **Changing the dispatched tree.** No Edit or Write; `Bash` reads and runs
|
|
@@ -133,6 +133,10 @@ searched or report `PLAUSIBLE`.
|
|
|
133
133
|
change produces, not the base** — the change itself can add the first hit (a
|
|
134
134
|
spec, a fixture, a guard test). The sentence names the ref it holds for.
|
|
135
135
|
|
|
136
|
+
**Turns are the cost**: every tool call re-reads your whole context. Put
|
|
137
|
+
independent checks in one Bash call (`a; b; c`), and several names in one
|
|
138
|
+
`wuwei sites a b c`.
|
|
139
|
+
|
|
136
140
|
## Out of scope
|
|
137
141
|
|
|
138
142
|
- **Changing the dispatched tree.** No Edit or Write; `Bash` reads it. A
|
|
@@ -122,6 +122,10 @@ A phase that changes schema and says nothing about lock duration has not been
|
|
|
122
122
|
reviewed for this; "the constraint is a strict superset so existing rows all
|
|
123
123
|
pass" answers compatibility, not cost.
|
|
124
124
|
|
|
125
|
+
**Turns are the cost**: every tool call re-reads your whole context. Put
|
|
126
|
+
independent checks in one Bash call (`a; b; c`), and several names in one
|
|
127
|
+
`wuwei sites a b c`.
|
|
128
|
+
|
|
125
129
|
## Out of scope
|
|
126
130
|
|
|
127
131
|
- **Changing the dispatched tree.** No Edit or Write; `Bash` reads it
|
|
@@ -129,6 +129,10 @@ is not one. Quote the clause.
|
|
|
129
129
|
**A zero-hit claim is checked against the tree the change produces, not the
|
|
130
130
|
base** — the change itself can add the first hit. Name the ref it holds for.
|
|
131
131
|
|
|
132
|
+
**Turns are the cost**: every tool call re-reads your whole context. Put
|
|
133
|
+
independent checks in one Bash call (`a; b; c`), and several names in one
|
|
134
|
+
`wuwei sites a b c`.
|
|
135
|
+
|
|
132
136
|
## Out of scope
|
|
133
137
|
|
|
134
138
|
- **Changing anything.** No Edit or Write; `Bash` reads (`grep`, `git log`,
|
|
@@ -61,6 +61,9 @@ So: **for every claim a document makes about what is or is not required, find
|
|
|
61
61
|
the procedure section that would have to change with it.** A new section
|
|
62
62
|
asserting a new fact, with the old procedure untouched, is the pattern.
|
|
63
63
|
|
|
64
|
+
**Turns are the cost**: every tool call re-reads your whole context. Put
|
|
65
|
+
alternatives in one pattern (`a|b|c`) rather than one search each.
|
|
66
|
+
|
|
64
67
|
## Out of scope
|
|
65
68
|
|
|
66
69
|
- **Changing anything.** No Edit or Write, and no Bash — everything here is
|
|
@@ -34,6 +34,10 @@ targets: [claude-code]
|
|
|
34
34
|
只取本席轄區的條目,bundle 答不了就直接讀樹——它是 reviewed sha 的快取,
|
|
35
35
|
不是權威,引用的座標一律是 repo 的座標。
|
|
36
36
|
|
|
37
|
+
**Turns are the cost**: every tool call re-reads your whole context. Put
|
|
38
|
+
independent checks in one Bash call (`a; b; c`), and several names in one
|
|
39
|
+
`wuwei sites a b c`.
|
|
40
|
+
|
|
37
41
|
## Out of scope
|
|
38
42
|
|
|
39
43
|
- **改派工給你的那棵樹。** 沒有 Edit / Write。發現以報告回傳,補洞是實作的事。
|
package/bin/wuwei.js
CHANGED
|
@@ -29,8 +29,10 @@
|
|
|
29
29
|
* wuwei worktree <lens> [<sha>] a throwaway worktree no other lens shares; --prune
|
|
30
30
|
* wuwei mutate <f>:<n> --from --to --run one mutation in a wuwei worktree: landed?
|
|
31
31
|
* baseline vs mutated, failing tests, restored, recorded
|
|
32
|
-
* wuwei sites <pattern
|
|
32
|
+
* wuwei sites <pattern>… [--symbol <name>] a set from the tree, with its derivation;
|
|
33
33
|
* graph and grep both for a symbol
|
|
34
|
+
* wuwei claims-scan <doc>… [--sha <rev>] a document's paths, names, file:lines and
|
|
35
|
+
* commands checked against the tree in one pass
|
|
34
36
|
* wuwei verify [<page>…] [--phase NN] [--endpoint <text>] [--states 500,404] every data-bearing page through its states
|
|
35
37
|
* (Playwright on web, Maestro + a mock server on mobile;
|
|
36
38
|
* fixtures from .wuwei/verify.json): a matrix of shots;
|
|
@@ -1419,8 +1421,14 @@ function cmdPlanCheck(argv) {
|
|
|
1419
1421
|
// mobile: Maestro flows, Flutter's patrol/integration_test, an XCUITest target, Espresso
|
|
1420
1422
|
|| existsSync(join(dest, '.maestro')) || /^\s*(patrol|integration_test):/m.test(read(join(dest, 'pubspec.yaml')) ?? '')
|
|
1421
1423
|
|| readdirSync(dest).some(f => /UITests$/.test(f)) || existsSync(join(dest, 'app', 'src', 'androidTest'))
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
+
// a script harness — a CLI, a library, a backend — is named, since nothing on
|
|
1425
|
+
// disk tells an e2e script from a unit test: `e2e harness: sh test/e2e.sh`
|
|
1426
|
+
const declared = acceptance?.match(/^\s*[-*]?\s*e2e harness:\s*`?([^`\n]+?)`?\s*$/im)?.[1]
|
|
1427
|
+
const script = declared?.split(/\s+/).find(t => /[\\/]|\.(sh|bats|[cm]?js|ts|py|rb)$/.test(t))
|
|
1428
|
+
if (declared && script && !existsSync(join(dest, script)))
|
|
1429
|
+
warns.push(`e2e harness \`${declared}\`: ${script} does not exist yet — the first phase with a bound item adds it as its first task`)
|
|
1430
|
+
else if (acceptance && /\be2e\b/i.test(acceptance) && !harness && !declared)
|
|
1431
|
+
warns.push('ACCEPTANCE.md binds e2e, and no harness was found (playwright/cypress/wdio config, one in package.json, .maestro/, patrol, *UITests, androidTest) — name a script one as `e2e harness: <command>`, or the first phase with a bound item adds one as its first task')
|
|
1424
1432
|
|
|
1425
1433
|
// STATE.md agrees with phases/
|
|
1426
1434
|
const row = (readStateRows(join(dest, '.wuwei', 'STATE.md')) ?? []).find(r => r.stream === stream)
|
|
@@ -1557,10 +1565,15 @@ function cmdSites(argv) {
|
|
|
1557
1565
|
const dd = argv.indexOf('--')
|
|
1558
1566
|
const pathspec = dd === -1 ? [] : argv.slice(dd + 1)
|
|
1559
1567
|
const args = dd === -1 ? argv : argv.slice(0, dd)
|
|
1560
|
-
const
|
|
1568
|
+
const patterns = args.filter((a, i) => !a.startsWith('--') && !['--sha', '--symbol'].includes(args[i - 1]))
|
|
1561
1569
|
const sha = flag('--sha') ?? 'HEAD', symbol = flag('--symbol'), regex = args.includes('--regex')
|
|
1562
|
-
if (!
|
|
1563
|
-
|
|
1570
|
+
if (!patterns.length && !symbol) { console.error("usage: wuwei sites <pattern>… [--regex] [--sha <rev>] [--symbol <name>] [-- <pathspec>…]"); process.exit(1) }
|
|
1571
|
+
// several names in one call: every call re-reads the lens's whole context, so one beats many
|
|
1572
|
+
if (patterns.length > 1 && !symbol) {
|
|
1573
|
+
for (const p of patterns) { console.log(`── ${p}`); cmdSites([p, ...(regex ? ['--regex'] : []), '--sha', sha, ...(pathspec.length ? ['--', ...pathspec] : [])]); console.log() }
|
|
1574
|
+
return
|
|
1575
|
+
}
|
|
1576
|
+
const term = patterns[0] ?? symbol
|
|
1564
1577
|
const grepArgs = ['grep', '-n', regex ? '-E' : '-F', '-e', term, sha, '--', ...pathspec]
|
|
1565
1578
|
const g = quiet('git', grepArgs)
|
|
1566
1579
|
if (g.status > 1) { console.error(`✗ git grep failed: ${firstLine(g.stderr)}`); process.exit(1) }
|
|
@@ -1605,6 +1618,116 @@ function cmdSites(argv) {
|
|
|
1605
1618
|
}
|
|
1606
1619
|
}
|
|
1607
1620
|
|
|
1621
|
+
// ── claims-scan: a document's checkable assertions, all in one call ─────
|
|
1622
|
+
// review:claims checked each path, name and line with its own grep: 74–117
|
|
1623
|
+
// turns a document, every one re-reading the lens's whole context — 59M of a
|
|
1624
|
+
// 196M-token review (reward-system, `wuwei review-collect`). This pulls them
|
|
1625
|
+
// out and checks them against the tree in one pass; the lens judges only what
|
|
1626
|
+
// it lists as needing judgment
|
|
1627
|
+
function cmdClaimsScan(argv) {
|
|
1628
|
+
const flag = name => { const i = argv.indexOf(name); return i === -1 ? null : argv[i + 1] }
|
|
1629
|
+
const sha = flag('--sha') ?? 'HEAD', all = argv.includes('--all')
|
|
1630
|
+
const docs = argv.filter((a, i) => !a.startsWith('--') && argv[i - 1] !== '--sha')
|
|
1631
|
+
if (!docs.length) { console.error('usage: wuwei claims-scan <doc>… [--sha <rev>] [--all]'); process.exit(1) }
|
|
1632
|
+
const at = quiet('git', ['rev-parse', '--short', sha]).stdout?.trim()
|
|
1633
|
+
if (!at) { console.error(`✗ ${sha} is not a commit here`); process.exit(1) }
|
|
1634
|
+
const tree = quiet('git', ['ls-tree', '-r', '--name-only', sha]).stdout.split('\n').filter(Boolean)
|
|
1635
|
+
const files = new Set(tree), dirs = new Set()
|
|
1636
|
+
for (const f of tree) { const p = f.split('/'); for (let i = 1; i < p.length; i++) dirs.add(p.slice(0, i).join('/')) }
|
|
1637
|
+
const lengths = new Map()
|
|
1638
|
+
const lineCount = f => { if (!lengths.has(f)) lengths.set(f, (quiet('git', ['show', `${sha}:${f}`]).stdout ?? '').split('\n').length - 1); return lengths.get(f) }
|
|
1639
|
+
let scripts = {}
|
|
1640
|
+
try { scripts = JSON.parse(quiet('git', ['show', `${sha}:package.json`]).stdout).scripts ?? {} } catch {}
|
|
1641
|
+
|
|
1642
|
+
const norm = p => p.replace(/^\.\//, '').replace(/\/$/, '').split('/').reduce((a, s) => s === '..' ? a.slice(0, -1) : s === '.' ? a : [...a, s], []).join('/')
|
|
1643
|
+
// a path as written, relative to the repo or the document; else wherever a file of that name is
|
|
1644
|
+
const resolvePath = (p, docDir) => {
|
|
1645
|
+
for (const c of [norm(p), norm(join(docDir, p))]) if (files.has(c) || dirs.has(c)) return { ok: c }
|
|
1646
|
+
const tail = '/' + norm(p), hits = tree.filter(f => f.endsWith(tail))
|
|
1647
|
+
return hits.length === 1 ? { moved: hits[0] } : hits.length ? { ambiguous: hits.slice(0, 3) } : { missing: true }
|
|
1648
|
+
}
|
|
1649
|
+
const SKIP = new Set(['null', 'true', 'false', 'undefined', 'none', 'yes', 'no', 'and', 'or', 'not', 'the', 'todo', 'fixme'])
|
|
1650
|
+
const COUNT = /\b(\d[\d,]*)\s+(files?|tests?|rows?|items?|endpoints?|keys?|lines?|columns?|tables?|routes?|pages?|steps?|commits?|calls?|call sites?|sites?|places?|locations?|entries|icons?|images?|states?|cases?|checks?|findings?|members?|fields?|migrations?|phases?)\b/gi
|
|
1651
|
+
|
|
1652
|
+
for (const doc of docs) {
|
|
1653
|
+
const text = existsSync(doc) ? readFileSync(doc, 'utf8') : quiet('git', ['show', `${sha}:${doc}`]).stdout
|
|
1654
|
+
if (text == null || text === '') { console.log(`✗ ${doc}: not found here or at ${at}`); continue }
|
|
1655
|
+
const docDir = dirname(norm(doc)), rows = [], ids = new Map(), counts = []
|
|
1656
|
+
let fenced = false
|
|
1657
|
+
text.split('\n').forEach((ln, i) => {
|
|
1658
|
+
if (/^\s*(```|~~~)/.test(ln)) { fenced = !fenced; return }
|
|
1659
|
+
if (fenced) return
|
|
1660
|
+
const where = `${doc}:${i + 1}`
|
|
1661
|
+
for (const m of ln.matchAll(/`([^`\n]+)`/g)) rows.push({ where, raw: m[1].trim() })
|
|
1662
|
+
for (const m of ln.matchAll(/\]\(([^)\s]+)\)/g)) if (!/^(https?:|mailto:|#)/.test(m[1])) rows.push({ where, raw: m[1].split('#')[0], link: true })
|
|
1663
|
+
for (const m of ln.replace(/`[^`]*`/g, '').matchAll(COUNT)) counts.push({ where, raw: m[0] })
|
|
1664
|
+
})
|
|
1665
|
+
const out = []
|
|
1666
|
+
for (const r of rows) {
|
|
1667
|
+
const s = r.raw
|
|
1668
|
+
const fl = s.match(/^([\w./@-]+\.[A-Za-z0-9]+):(\d+)(?:[-–](\d+))?$/)
|
|
1669
|
+
if (fl) {
|
|
1670
|
+
const p = resolvePath(fl[1], docDir), f = p.ok ?? p.moved
|
|
1671
|
+
if (!f) { out.push({ ...r, kind: 'file:line', result: p.ambiguous ? `ambiguous: ${p.ambiguous.join(', ')}` : 'file missing' }); continue }
|
|
1672
|
+
const last = Number(fl[3] ?? fl[2]), n = lineCount(f)
|
|
1673
|
+
out.push({ ...r, kind: 'file:line', result: last > n ? `past the end — ${f} has ${n} lines` : p.moved ? `moved: ${f}:${fl[2]}${fl[3] ? `-${fl[3]}` : ''}` : 'ok' })
|
|
1674
|
+
continue
|
|
1675
|
+
}
|
|
1676
|
+
if (/^(npm|pnpm|yarn|bun) (run )?[\w:.-]+/.test(s)) {
|
|
1677
|
+
const name = s.match(/^(?:npm|pnpm|yarn|bun) (?:run )?([\w:.-]+)/)[1]
|
|
1678
|
+
const builtin = ['install', 'ci', 'test', 'i', 'exec', 'add', 'publish', 'pack', 'view', 'link'].includes(name) && !/ run /.test(s)
|
|
1679
|
+
out.push({ ...r, kind: 'command', result: builtin || scripts[name] ? (builtin ? 'command — not run' : 'script exists — not run') : `no package.json script "${name}"` })
|
|
1680
|
+
continue
|
|
1681
|
+
}
|
|
1682
|
+
if (/^(npx|node|wuwei|ww|git|make|python3?|bash|sh|curl|gh|docker|kubectl|gcloud|bunx|deno|cargo|go)\s/.test(s)) {
|
|
1683
|
+
const path = s.match(/^(?:node|python3?|bash|sh)\s+(\S+\.\w+)/)?.[1]
|
|
1684
|
+
const p = path && resolvePath(path, docDir)
|
|
1685
|
+
out.push({ ...r, kind: 'command', result: p && !p.ok ? (p.moved ? `script moved: ${p.moved}` : 'script missing') : 'command — not run' })
|
|
1686
|
+
continue
|
|
1687
|
+
}
|
|
1688
|
+
// not this repo's to answer: a placeholder, a machine path, a version, a slash command
|
|
1689
|
+
if (/[<>{}]|\bNN\b|^[~$]|^\/[^/]*$|^\/(Users|home|tmp|etc|opt|var)\/|^v?\d+(\.[\dx*]+)+$|^\.\w+$/.test(s)) continue
|
|
1690
|
+
if (!/\s/.test(s) && !/^[a-z]+:\/\//.test(s) && (r.link || s.includes('/') || /\.[A-Za-z][A-Za-z0-9]{0,5}$/.test(s)) && !/^[\d.]+$/.test(s)) {
|
|
1691
|
+
// a path is this repo's when its first directory is; a bare name may be anyone's file
|
|
1692
|
+
const first = norm(s).split('/')[0], ours = s.includes('/') && (dirs.has(first) || dirs.has(norm(join(docDir, first))))
|
|
1693
|
+
if (s.includes('*')) {
|
|
1694
|
+
const re = globRe(norm(s)), n = tree.filter(f => re.test(f)).length
|
|
1695
|
+
out.push({ ...r, kind: 'glob', result: n ? `ok (${n} file${n === 1 ? '' : 's'})` : ours ? 'matches nothing' : 'not in this repo' })
|
|
1696
|
+
} else {
|
|
1697
|
+
const p = resolvePath(s, docDir)
|
|
1698
|
+
out.push({ ...r, kind: 'path', result: p.ok ? 'ok' : !s.includes('/') && (p.moved || p.ambiguous) ? `ok (${p.moved ?? p.ambiguous.join(', ')})`
|
|
1699
|
+
: p.moved ? `moved: ${p.moved}` : p.ambiguous ? `ambiguous: ${p.ambiguous.join(', ')}` : !ours ? 'not in this repo' : 'missing' })
|
|
1700
|
+
}
|
|
1701
|
+
continue
|
|
1702
|
+
}
|
|
1703
|
+
const id = s.replace(/\(\)$/, '')
|
|
1704
|
+
if (/^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/.test(id) && id.length >= 3 && !SKIP.has(id.toLowerCase())) {
|
|
1705
|
+
if (!ids.has(id)) ids.set(id, [])
|
|
1706
|
+
ids.get(id).push(r)
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
// every identifier in one git grep, the document itself excluded
|
|
1710
|
+
if (ids.size) {
|
|
1711
|
+
const g = quiet('git', ['grep', '-o', '-h', '-w', '-F', ...[...ids.keys()].flatMap(id => ['-e', id]), sha, '--', '.', `:(exclude)${norm(doc)}`])
|
|
1712
|
+
const seen = new Map()
|
|
1713
|
+
for (const m of (g.stdout ?? '').split('\n')) if (m) seen.set(m.replace(`${sha}:`, ''), (seen.get(m.replace(`${sha}:`, '')) ?? 0) + 1)
|
|
1714
|
+
for (const [id, rs] of ids) for (const r of rs) out.push({ ...r, kind: 'name', result: seen.get(id) ? `ok (${seen.get(id)} in the tree)` : 'not in this repo' })
|
|
1715
|
+
}
|
|
1716
|
+
const elsewhere = out.filter(o => o.result === 'not in this repo')
|
|
1717
|
+
const bad = out.filter(o => !o.result.startsWith('ok') && !/not run/.test(o.result) && o.result !== 'not in this repo')
|
|
1718
|
+
const judge = [...out.filter(o => /not run/.test(o.result)), ...counts.map(c => ({ ...c, kind: 'count', result: 're-derive' }))]
|
|
1719
|
+
console.log(`claims-scan ${doc} @ ${at}: ${out.length} checked · ${bad.length} wrong · ${elsewhere.length} not in this repo · ${judge.length} for judgment`)
|
|
1720
|
+
if (bad.length) { console.log('\nwrong at the sha:\n| where | claim | kind | at the sha |\n|---|---|---|---|'); for (const o of bad) console.log(`| ${o.where} | \`${o.raw}\` | ${o.kind} | ${o.result} |`) }
|
|
1721
|
+
const byClaim = new Map()
|
|
1722
|
+
for (const o of elsewhere) byClaim.set(o.raw, [...(byClaim.get(o.raw) ?? []), o.where.split(':').pop()])
|
|
1723
|
+
if (byClaim.size) console.log(`\nnot in this repo at the sha — another repo's, a tool's config, removed, or wrong: ${[...byClaim].map(([c, ls]) => `\`${c}\` (${ls.join(',')})`).join(' · ')}`)
|
|
1724
|
+
if (judge.length) { console.log('\nfor judgment — counts to re-derive, commands not run:'); for (const o of judge) console.log(` ${o.where} ${o.kind === 'count' ? o.raw : `\`${o.raw}\``} — ${o.result}`) }
|
|
1725
|
+
if (all) { console.log('\nchecked ok:'); for (const o of out.filter(o => o.result.startsWith('ok'))) console.log(` ${o.where} \`${o.raw}\` ${o.result}`) }
|
|
1726
|
+
console.log('')
|
|
1727
|
+
}
|
|
1728
|
+
console.log('what it cannot see: a claim in prose with no code span, a behaviour, a number with no unit — those stay the lens\'s')
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1608
1731
|
// ── verify: hands and eyes — every data-bearing page through its states ──
|
|
1609
1732
|
// The record: UI state bugs found one round at a time (#268 R1 → R2 → R3:
|
|
1610
1733
|
// a refetch failure that replaces loaded rows, a 403 shown as a generic
|
|
@@ -2126,8 +2249,20 @@ async function verifyMobile(cfg, dest, outDir) {
|
|
|
2126
2249
|
function transcriptStats(file) {
|
|
2127
2250
|
const usage = new Map(), uses = new Map(), reads = new Map()
|
|
2128
2251
|
const t = { turns: 0, built: 0, reread: 0, input: 0, output: 0, first: null, last: null, report: '' }
|
|
2252
|
+
// what a Bash call was for, by its first command: the 'other' that dominated a real review needed names
|
|
2253
|
+
const bashKind = cmd => {
|
|
2254
|
+
const c = cmd.replace(/^\s*(cd [^;&]+(&&|;)\s*)+/, '').trim()
|
|
2255
|
+
return /\bcodegraph\b/.test(c) ? 'Bash:codegraph'
|
|
2256
|
+
: /^(wuwei|ww|node \S*wuwei\.js)\b/.test(c) ? 'Bash:wuwei'
|
|
2257
|
+
: /^(git (-C \S+ )?grep|grep|rg|ag)\b/.test(c) ? 'Bash:grep'
|
|
2258
|
+
: /^git (-C \S+ )?(show|diff|log|blame|cat-file|ls-tree|rev-parse|merge-base)\b/.test(c) ? 'Bash:git'
|
|
2259
|
+
: /^(sed|cat|head|tail|awk|nl|wc|less|jq)\b/.test(c) ? 'Bash:read'
|
|
2260
|
+
: /^(ls|find|fd|tree|stat|git (-C \S+ )?ls-files)\b/.test(c) ? 'Bash:find'
|
|
2261
|
+
: /^(npm|npx|pnpm|yarn|bun|node|vitest|jest|pytest|tsc|biome|make|cargo|go|python3?)\b/.test(c) ? 'Bash:run'
|
|
2262
|
+
: 'Bash:other'
|
|
2263
|
+
}
|
|
2129
2264
|
const kind = (name, inp = {}) => name === 'Read' ? (/\.wuwei-cache/.test(inp.file_path ?? '') ? 'Read:bundle' : 'Read:tree')
|
|
2130
|
-
: name === 'Bash' ? (
|
|
2265
|
+
: name === 'Bash' ? bashKind(inp.command ?? '') : name
|
|
2131
2266
|
for (const line of readFileSync(file, 'utf8').split('\n')) {
|
|
2132
2267
|
let d; try { d = JSON.parse(line) } catch { continue }
|
|
2133
2268
|
if (d.timestamp) { t.first ??= d.timestamp; t.last = d.timestamp }
|
|
@@ -2184,21 +2319,21 @@ function cmdReviewCollect(argv) {
|
|
|
2184
2319
|
const name = n === 1 ? base : `${base}-${n}`
|
|
2185
2320
|
writeFileSync(join(out, `${name}.jsonl`), readFileSync(a.jsonl))
|
|
2186
2321
|
if (a.s.report) writeFileSync(join(out, `${name}.md`), a.s.report.trimEnd() + '\n')
|
|
2187
|
-
rows.push({ name, ...a.s })
|
|
2322
|
+
rows.push({ name, what: (a.meta.description ?? '').slice(0, 40), ...a.s })
|
|
2188
2323
|
}
|
|
2189
2324
|
const k = x => x >= 1e6 ? `${(x / 1e6).toFixed(1)}M` : x >= 1e3 ? `${Math.round(x / 1e3)}k` : String(x)
|
|
2190
2325
|
const total = rows.reduce((t, r) => ({ built: t.built + r.built, reread: t.reread + r.reread }), { built: 0, reread: 0 })
|
|
2191
2326
|
console.log(`session ${session.id} · ${rows.length} subagent(s) → ${out}\n`)
|
|
2192
2327
|
// output tokens are left out: the transcript records a streaming chunk's count, not the message's
|
|
2193
|
-
console.log('| agent | turns | context built | re-read (cached) | where the reading went |\n
|
|
2328
|
+
console.log('| agent | dispatched as | turns | context built | re-read (cached) | where the reading went |\n|---|---|---|---|---|---|')
|
|
2194
2329
|
for (const r of [...rows].sort((a, b) => (b.built + b.reread / 10) - (a.built + a.reread / 10)))
|
|
2195
|
-
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(' · ') || '—'} |`)
|
|
2196
|
-
console.log(`| **total** | | ${k(total.built)} | ${k(total.reread)} | |`)
|
|
2330
|
+
console.log(`| ${r.name} | ${r.what.replace(/\|/g, '/') || '—'} | ${r.turns} | ${k(r.built)} | ${k(r.reread)} | ${r.reads.slice(0, 4).map(([kk, v]) => `${kk} ${v.n}× ≈${k(v.tokens)}`).join(' · ') || '—'} |`)
|
|
2331
|
+
console.log(`| **total** | | ${rows.reduce((t, r) => t + r.turns, 0)} | ${k(total.built)} | ${k(total.reread)} | |`)
|
|
2197
2332
|
const byKind = new Map()
|
|
2198
2333
|
for (const r of rows) for (const [kk, v] of r.reads) byKind.set(kk, (byKind.get(kk) ?? 0) + v.tokens)
|
|
2199
2334
|
const top = [...byKind].sort((a, b) => b[1] - a[1]).slice(0, 3)
|
|
2200
2335
|
if (top.length) console.log(`\nmost read, across the lenses: ${top.map(([kk, v]) => `${kk} ≈${k(v)}`).join(' · ')}`)
|
|
2201
|
-
console.log('re-read is billed at about a tenth of context built
|
|
2336
|
+
console.log('re-read is billed at about a tenth of context built, but every turn pays the whole context again: turns are the cost')
|
|
2202
2337
|
}
|
|
2203
2338
|
|
|
2204
2339
|
// ── digest-check: wuwei-review §7's arithmetic, before publishing ─────
|
|
@@ -3184,9 +3319,10 @@ switch (cmd) {
|
|
|
3184
3319
|
case 'worktree': cmdWorktree(rest); break
|
|
3185
3320
|
case 'mutate': cmdMutate(rest); break
|
|
3186
3321
|
case 'sites': cmdSites(rest); break
|
|
3322
|
+
case 'claims-scan': cmdClaimsScan(rest); break
|
|
3187
3323
|
case 'plan-check': cmdPlanCheck(rest); break
|
|
3188
3324
|
case 'uninstall': await cmdUninstall(arg); break
|
|
3189
3325
|
default:
|
|
3190
|
-
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
|
|
3326
|
+
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>…|claims-scan <doc>…|plan-check [stream]|uninstall [path]|--version] [--dry]`)
|
|
3191
3327
|
process.exit(1)
|
|
3192
3328
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wei-wuwei",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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": {
|
|
@@ -172,9 +172,10 @@ tags: [planning, workflow]
|
|
|
172
172
|
route each to automatable e2e (bound to a phase per step 1's strategy —
|
|
173
173
|
red blocks that phase)
|
|
174
174
|
or manual UAT (steps + expected result). **A repo with no e2e harness is
|
|
175
|
-
not a reason for UAT**: the first phase with a bound item adds one
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
not a reason for UAT**: the first phase with a bound item adds one as its
|
|
176
|
+
first task — Playwright on web, Maestro on mobile (what `wuwei verify`
|
|
177
|
+
runs on), a script for a CLI, library or backend, running the real
|
|
178
|
+
program on a temp fixture and named `e2e harness: <command>`.
|
|
178
179
|
**`## Verify`** lists each page that shows fetched data — `- <page>:
|
|
179
180
|
<route> · phase: NN`, the phase that builds it; its error, 403, empty,
|
|
180
181
|
loading and failed-refetch states are `wuwei verify`'s, not UAT's. No
|
|
@@ -305,10 +306,7 @@ tags: [planning, workflow]
|
|
|
305
306
|
- The contract is what makes auto mode safe: the executor never defines its
|
|
306
307
|
own finish line.
|
|
307
308
|
- In auto mode execute's steps 2–6 run without the user, and its steps 4–6
|
|
308
|
-
loop at least three times inside that; execute's step 7 always stops.
|
|
309
|
-
strategy question is not an exception to that: auto is only ever entered at
|
|
310
|
-
a sign-off, which recorded the answer, so a resumed auto stream reads it
|
|
311
|
-
instead of asking.
|
|
309
|
+
loop at least three times inside that; execute's step 7 always stops.
|
|
312
310
|
- Sign-off is why step 8 sits where it does: the stream stops being paper
|
|
313
311
|
there, and it is the only moment a human is present to confirm what the
|
|
314
312
|
branch is based on. Cutting earlier leaves a branch behind for a stream
|
|
@@ -30,6 +30,7 @@ changed in one is changed in the other.
|
|
|
30
30
|
|
|
31
31
|
| Lens | Rule |
|
|
32
32
|
|---|---|
|
|
33
|
+
| claims | **start with `wuwei claims-scan <doc>… --sha <sha>`**: every path, name, `file:line` and command checked in one call; by hand only what it lists for judgment, and assertions no code span carries |
|
|
33
34
|
| claims | **a document that instructs is reviewed for what it invites**: enumerate the positions a safety claim covers, subtract the ones its mechanism protects, report the rest. Confirming it for the cases that exist proves nothing about the ones the document teaches someone to write. **A position the syntax permits is reachable — the reader is the path**, so "no such case exists yet" does not make it PLAUSIBLE. This governs *claims in documents*, not sinks in code: `audit:security`'s reachability rule is unchanged, and the two lenses reaching opposite severities on one subject is a real disagreement for a human, not a contradiction to resolve by collation |
|
|
34
35
|
| claims | **a checkable gate (an acceptance item, a `grep -c … >= 1`, a `test -f`) is a claim about what will satisfy it** — ask what *else* in the tree satisfies it, not only whether the intended thing does; **a gate reading a measurement is checked for its emitter in the tree the change produces** (a deleted emitter reads as a fake zero); **a gate names its enforcer** or is a registered gap, and **N copies of a claim by one author are one claim**; **every gate is re-run after the fixes land**, since a refactor the panel asked for is how a gate stops matching. Three ways it is false and **the dangerous ones pass**: satisfied by the wrong thing (a grep meant to prove a method exists, whose one hit is a comment about another class — green, certifying work nobody did), never satisfiable (greps a since-renamed name), or its emitter deleted by the change (a fake zero). Run it against the tree as it is now and say which it currently is — **and running its syntax is not running it**, since a fragment that parses can still be case-blind or satisfied by a superstring; a fix that would satisfy the gate on arrival is itself a finding, not a reason to stay quiet |
|
|
35
36
|
| claims | **a claim about history ("never", "since it landed", "every version") is answered by `git log -S` / `git log -- <path>`, not by the tree**; and **a claim to be complete is verified by re-deriving the set, never by checking off the set it lists** — "all four call sites", "every place this is mapped", "the only consumer". Ticking each listed item passes while a fifth one exists, so derive the set from the tree instead: grep the type, the decorator, the shape — **not the names the document handed you**, which are the claim under test — then subtract. This is `defects`' exclusivity rule aimed at prose, and it bites hardest on a plan, where the executor works the list rather than the tree |
|
package/wuwei.config.json
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"agents/wuwei-audit-contract/agent.md": 700,
|
|
40
40
|
"agents/wuwei-audit-coverage/agent.md": 800,
|
|
41
41
|
"agents/wuwei-pre-implementation-doubt/agent.md": 600,
|
|
42
|
-
"agents/wuwei-review-claims/agent.md":
|
|
42
|
+
"agents/wuwei-review-claims/agent.md": 1700,
|
|
43
43
|
"agents/wuwei-review-code/agent.md": 2000,
|
|
44
44
|
"agents/wuwei-review-defects/agent.md": 1500,
|
|
45
45
|
"agents/wuwei-review-design/agent.md": 1600,
|