@webappwiz/cli 0.0.9 → 0.0.11
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/README.md +56 -34
- package/documents.d.ts +51 -0
- package/index-d8p57dk5.js +593 -0
- package/index.js +40 -44
- package/package.json +4 -12
- package/rules/add.d.ts +7 -0
- package/rules/ls.d.ts +7 -0
- package/rules/new.d.ts +16 -0
- package/rules/review.d.ts +21 -0
- package/rules/rule-set.d.ts +16 -0
- package/rules/update.d.ts +7 -0
- package/skills/skill.d.ts +3 -13
- package/update.d.ts +3 -1
- package/webappwiz.d.ts +0 -2
- package/webappwiz.js +39 -38
- package/changed.d.ts +0 -30
- package/index-g81gg9gz.js +0 -343
- package/index-htwb54c6.js +0 -44
- package/index-pyjg1rtk.js +0 -373
- package/judge.d.ts +0 -63
- package/judge.js +0 -6
- package/mode.d.ts +0 -21
- package/progress.d.ts +0 -64
- package/report.d.ts +0 -69
- package/rules.d.ts +0 -10
- package/rules.js +0 -6
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
// package.json
|
|
2
|
+
var version = "0.0.11";
|
|
3
|
+
|
|
4
|
+
// documents.ts
|
|
5
|
+
import { dirname } from "node:path";
|
|
6
|
+
import { ConsoleLogger } from "webappwiz/log";
|
|
7
|
+
import { NodeFs } from "webappwiz/system";
|
|
8
|
+
function versionOf(md) {
|
|
9
|
+
const frontmatter = md.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "";
|
|
10
|
+
return frontmatter.match(/^version:\s*(.+)$/m)?.[1]?.trim() ?? null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class Documents {
|
|
14
|
+
docs;
|
|
15
|
+
layout;
|
|
16
|
+
log;
|
|
17
|
+
fs;
|
|
18
|
+
constructor(docs, layout, opts = {}) {
|
|
19
|
+
this.docs = docs;
|
|
20
|
+
this.layout = layout;
|
|
21
|
+
this.log = opts.log ?? new ConsoleLogger;
|
|
22
|
+
this.fs = opts.fs ?? new NodeFs;
|
|
23
|
+
}
|
|
24
|
+
available() {
|
|
25
|
+
return Object.entries(this.docs).toSorted(([left], [right]) => left.localeCompare(right));
|
|
26
|
+
}
|
|
27
|
+
path(dir, name) {
|
|
28
|
+
return `${dir}/${this.layout.root}/${name}/${this.layout.file}`;
|
|
29
|
+
}
|
|
30
|
+
async installed(dir) {
|
|
31
|
+
const names = await this.fs.readdir(`${dir}/${this.layout.root}`).catch(() => []);
|
|
32
|
+
const present = [];
|
|
33
|
+
for (const name of names.toSorted()) {
|
|
34
|
+
if (await this.fs.exists(this.path(dir, name))) {
|
|
35
|
+
present.push(name);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return present;
|
|
39
|
+
}
|
|
40
|
+
async installedVersion(dir, name) {
|
|
41
|
+
return this.fs.read(this.path(dir, name)).then(versionOf).catch(() => null);
|
|
42
|
+
}
|
|
43
|
+
async add(name, dir) {
|
|
44
|
+
const doc = this.docs[name];
|
|
45
|
+
if (doc === undefined) {
|
|
46
|
+
const have = this.available().map(([known]) => known);
|
|
47
|
+
throw new Error(`no such ${this.layout.noun}: ${name} (have ${have.join(", ")})`);
|
|
48
|
+
}
|
|
49
|
+
await this.copy(name, doc, dir);
|
|
50
|
+
}
|
|
51
|
+
async update(dir) {
|
|
52
|
+
const installed = await this.installed(dir);
|
|
53
|
+
const ours = this.available().filter(([name]) => installed.includes(name));
|
|
54
|
+
for (const [name, doc] of ours) {
|
|
55
|
+
await this.copy(name, doc, dir);
|
|
56
|
+
}
|
|
57
|
+
return ours.map(([name]) => name);
|
|
58
|
+
}
|
|
59
|
+
async copy(name, doc, dir) {
|
|
60
|
+
const target = this.path(dir, name);
|
|
61
|
+
await this.fs.mkdir(dirname(target));
|
|
62
|
+
await this.fs.write(target, doc);
|
|
63
|
+
this.log.info(`wrote ${target}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// rules/rule-set.ts
|
|
68
|
+
import { catalog } from "@webappwiz/rules/catalog";
|
|
69
|
+
var RULES = {
|
|
70
|
+
root: ".wiz/rules",
|
|
71
|
+
file: "RULE.md",
|
|
72
|
+
noun: "rule"
|
|
73
|
+
};
|
|
74
|
+
var offered = (opts) => opts.rules ?? catalog;
|
|
75
|
+
|
|
76
|
+
// rules/add.ts
|
|
77
|
+
async function add(opts) {
|
|
78
|
+
const documents = new Documents(offered(opts), RULES, opts);
|
|
79
|
+
await documents.add(opts.rule, opts.dir);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// rules/ls.ts
|
|
83
|
+
import { Rule, Rules } from "@webappwiz/rules";
|
|
84
|
+
import { ConsoleLogger as ConsoleLogger2, color as color2 } from "webappwiz/log";
|
|
85
|
+
|
|
86
|
+
// table.ts
|
|
87
|
+
import { color } from "webappwiz/log";
|
|
88
|
+
var table = (rows) => {
|
|
89
|
+
const width = (cell) => color.strip(cell).length;
|
|
90
|
+
const widths = rows[0]?.map((_, i) => Math.max(...rows.map((row) => width(row[i] ?? ""))));
|
|
91
|
+
return rows.map((row) => row.map((cell, i) => cell.padEnd((widths?.[i] ?? 0) + cell.length - width(cell))).join(" ").trimEnd());
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// rules/ls.ts
|
|
95
|
+
async function ls(opts) {
|
|
96
|
+
const log = opts.log ?? new ConsoleLogger2;
|
|
97
|
+
const documents = new Documents(offered(opts), RULES, opts);
|
|
98
|
+
const local = await Rules.load(opts.dir, { fs: opts.fs });
|
|
99
|
+
const shipped = new Map(documents.available().map(([id, doc]) => [id, Rule.parse(doc, { id })]));
|
|
100
|
+
const ids = new Set([...local.all.map((rule) => rule.id), ...shipped.keys()]);
|
|
101
|
+
const rows = [
|
|
102
|
+
[
|
|
103
|
+
"rule",
|
|
104
|
+
"level",
|
|
105
|
+
"complexity",
|
|
106
|
+
"files",
|
|
107
|
+
"ships",
|
|
108
|
+
"installed",
|
|
109
|
+
"description"
|
|
110
|
+
].map(color2.dim)
|
|
111
|
+
];
|
|
112
|
+
let stale = 0;
|
|
113
|
+
for (const id of [...ids].toSorted()) {
|
|
114
|
+
const rule = local.get(id) ?? shipped.get(id);
|
|
115
|
+
if (!rule) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const ships = shipped.get(id)?.version ?? null;
|
|
119
|
+
const installed = local.get(id) ? local.get(id)?.version ?? "local" : null;
|
|
120
|
+
if (ships !== null && installed !== null && ships !== installed) {
|
|
121
|
+
stale++;
|
|
122
|
+
}
|
|
123
|
+
rows.push([
|
|
124
|
+
id,
|
|
125
|
+
rule.level,
|
|
126
|
+
rule.complexity,
|
|
127
|
+
rule.files,
|
|
128
|
+
ships ?? "-",
|
|
129
|
+
installed ?? "-",
|
|
130
|
+
rule.description
|
|
131
|
+
]);
|
|
132
|
+
}
|
|
133
|
+
const lines = table(rows);
|
|
134
|
+
if (stale > 0) {
|
|
135
|
+
lines.push("", `${stale} out of date: run \`rules update\``);
|
|
136
|
+
}
|
|
137
|
+
log.info(lines.join(`
|
|
138
|
+
`));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// rules/new.ts
|
|
142
|
+
import { RULE_FILE, RULES_ROOT, template } from "@webappwiz/rules";
|
|
143
|
+
import { ConsoleLogger as ConsoleLogger3 } from "webappwiz/log";
|
|
144
|
+
import { NodeFs as NodeFs2 } from "webappwiz/system";
|
|
145
|
+
async function newRule(opts) {
|
|
146
|
+
const log = opts.log ?? new ConsoleLogger3;
|
|
147
|
+
const fs = opts.fs ?? new NodeFs2;
|
|
148
|
+
const dir = `${opts.dir}/${RULES_ROOT}/${opts.name}`;
|
|
149
|
+
const path = `${dir}/${RULE_FILE}`;
|
|
150
|
+
if (await fs.exists(path)) {
|
|
151
|
+
throw new Error(`${path} already exists`);
|
|
152
|
+
}
|
|
153
|
+
const doc = template(opts.name);
|
|
154
|
+
await fs.mkdir(dir);
|
|
155
|
+
await fs.write(path, doc);
|
|
156
|
+
log.info(`wrote ${path}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// rules/review.ts
|
|
160
|
+
import { changed, Rules as Rules2 } from "@webappwiz/rules";
|
|
161
|
+
import { ConsoleLogger as ConsoleLogger4 } from "webappwiz/log";
|
|
162
|
+
async function review(opts) {
|
|
163
|
+
const log = opts.log ?? new ConsoleLogger4;
|
|
164
|
+
const rules = await Rules2.load(opts.dir, { fs: opts.fs });
|
|
165
|
+
if (rules.all.length === 0) {
|
|
166
|
+
throw new Error(`no rules in ${opts.dir}/.wiz/rules: copy one in with \`rules add\`, or write one with \`rules new\``);
|
|
167
|
+
}
|
|
168
|
+
const files = await changed(opts.dir, opts.since, { ps: opts.ps });
|
|
169
|
+
if (files.length === 0) {
|
|
170
|
+
log.info(`nothing has changed since ${opts.since}`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const blocks = rules.review(files, { chunk: opts.chunk, glob: opts.glob });
|
|
174
|
+
const count = (total, noun) => `${total} ${noun}${total === 1 ? "" : "s"}`;
|
|
175
|
+
if (blocks.length === 0) {
|
|
176
|
+
log.info(`no rule matches the ${count(files.length, "file")} changed since ${opts.since}`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const matched = new Set(blocks.map((block) => block.rule.id)).size;
|
|
180
|
+
log.info(`${count(files.length, "file")} changed since ${opts.since}; ` + `${count(matched, "rule")} matched, ${count(blocks.length, "block")} to review`);
|
|
181
|
+
for (const block of blocks) {
|
|
182
|
+
log.info("");
|
|
183
|
+
log.info(block.prompt(opts.since));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// rules/update.ts
|
|
188
|
+
import { ConsoleLogger as ConsoleLogger5 } from "webappwiz/log";
|
|
189
|
+
async function update(opts) {
|
|
190
|
+
const log = opts.log ?? new ConsoleLogger5;
|
|
191
|
+
const documents = new Documents(offered(opts), RULES, opts);
|
|
192
|
+
const refreshed = await documents.update(opts.dir);
|
|
193
|
+
if (refreshed.length === 0) {
|
|
194
|
+
log.info(`no webappwiz rules in ${opts.dir}: add one with \`rules add\``);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// templates/arbor.skill.md
|
|
199
|
+
var arbor_skill_default = `---
|
|
200
|
+
name: arbor
|
|
201
|
+
description: Use the @webappwiz/arbor CLI to land your work on trunk, or a base branch given as an argument, from an isolated git worktree without pull requests. Read this before making any code change in an arbor repository, since it decides where the work happens, and whenever you need to add, claim, merge, remove, list, show, locate, or escalate a task.
|
|
202
|
+
version: 0.0.11
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
# Using arbor
|
|
206
|
+
|
|
207
|
+
\`arbor\` runs many agents on one repo, each in its own git worktree, landing on
|
|
208
|
+
trunk without pull requests. Run it with \`bunx @webappwiz/arbor <command>\` (or
|
|
209
|
+
\`arbor\` if on PATH). \`arbor --help\` explains the commands; this file covers
|
|
210
|
+
only what the CLI cannot tell you.
|
|
211
|
+
|
|
212
|
+
**Rule:** never use raw git for state transitions arbor covers. Every landing
|
|
213
|
+
goes through \`arbor merge\`. The only exception is finishing an in-progress
|
|
214
|
+
rebase (\`git add\`, \`git rebase --continue\`), then merging again.
|
|
215
|
+
|
|
216
|
+
A failed command prints \`{reason}\` JSON on stdout and instructions on stderr:
|
|
217
|
+
do what stderr says. The one case to memorize is exit 4 \`lease_lost\`: stop,
|
|
218
|
+
do not retry, another agent owns the tree.
|
|
219
|
+
|
|
220
|
+
## Before you start
|
|
221
|
+
|
|
222
|
+
Other agents may already be working. Before creating anything, list the files
|
|
223
|
+
you expect to touch, then \`arbor ls\`, and for each task in flight compare with
|
|
224
|
+
its changed files:
|
|
225
|
+
\`git -C "$(arbor path <task>)" diff --name-only main...task/<task>\`
|
|
226
|
+
(\`arbor show <task>\` for its plan; neither takes its lease).
|
|
227
|
+
|
|
228
|
+
If nothing overlaps, carry on. If something does, \`arbor add\` your task if you
|
|
229
|
+
have not already and record the overlap in \`ARBOR.md\` (which task, which
|
|
230
|
+
files). Some overlap is normal: work alongside and accept the rebase. Only
|
|
231
|
+
when the overlap is significant and you expect merge conflicts that would be
|
|
232
|
+
hard to resolve, \`arbor wait <task>\` on the task you overlap with instead:
|
|
233
|
+
let it land first and your rebase is onto its work rather than against it.
|
|
234
|
+
|
|
235
|
+
Waiting is caution, reserved for overlap that warrants it. Escalate
|
|
236
|
+
instead only when the other task is doing something majorly different from
|
|
237
|
+
yours, or contrary to it: rewriting what you are extending, or asked for the
|
|
238
|
+
opposite of what you were. Then \`arbor escalate\` and ask the user whether to
|
|
239
|
+
wait for it, work alongside it and accept the rebase, or drop yours.
|
|
240
|
+
|
|
241
|
+
Act on how the wait ends:
|
|
242
|
+
|
|
243
|
+
- \`removed\`: it landed or was dropped. Redo the overlap check (trunk moved)
|
|
244
|
+
and carry on.
|
|
245
|
+
- \`escalated\`: your work is blocked on a person too. Tell the human what it
|
|
246
|
+
is blocked on and wait.
|
|
247
|
+
- \`orphaned\`, \`stray\`, \`unrecorded\` or \`unknown\`: that tree is broken. A tree
|
|
248
|
+
mid-merge can read as \`orphaned\` for a moment, so \`wait\` once more before
|
|
249
|
+
believing it, then say so and ask.
|
|
250
|
+
- exit 14 \`timeout\`, still \`working\` or \`merging\`: \`wait\` again (with
|
|
251
|
+
\`--timeout-secs\` if the task looks close), or offer the choice of
|
|
252
|
+
working alongside it or picking up something else, saying what you have not
|
|
253
|
+
started.
|
|
254
|
+
|
|
255
|
+
A \`stale\` lease on a \`working\` task is normal (arbor only heartbeats while a
|
|
256
|
+
command runs): watch a task's status, never its lease.
|
|
257
|
+
|
|
258
|
+
## Workflow
|
|
259
|
+
|
|
260
|
+
1. \`arbor add <task>\`, or \`arbor claim <task>\` to resume one. When this skill
|
|
261
|
+
is invoked with a branch argument (\`/arbor feature/auth\`), or the user
|
|
262
|
+
names the branch the work should land on, pass it as \`--base\` to every
|
|
263
|
+
task you create for that request. Otherwise omit \`--base\`; never guess a
|
|
264
|
+
base from the currently checked-out branch.
|
|
265
|
+
2. Fill in the \`ARBOR.md\` stub \`add\` wrote at the worktree root (see below)
|
|
266
|
+
before touching code.
|
|
267
|
+
3. Do the work, updating \`ARBOR.md\` as you go; commit with git (arbor never
|
|
268
|
+
commits for you).
|
|
269
|
+
4. \`arbor merge\`. On failure, do what stderr says and merge again.
|
|
270
|
+
|
|
271
|
+
A successful merge deletes the worktree, and your working directory with it:
|
|
272
|
+
\`cd\` to the main tree (merge prints its path) before running anything else.
|
|
273
|
+
|
|
274
|
+
## Escalation
|
|
275
|
+
|
|
276
|
+
Merge only work you verified yourself. Escalate instead when verification
|
|
277
|
+
needs a person: external services, destructive migrations, anything tests
|
|
278
|
+
cannot confirm. And if the user asked to see the work before it lands,
|
|
279
|
+
escalate regardless.
|
|
280
|
+
|
|
281
|
+
1. \`arbor escalate <reason>\`.
|
|
282
|
+
2. Under \`## Blocked\` in \`ARBOR.md\`, state what needs verifying, ending in a
|
|
283
|
+
question a yes/no or a sentence can answer.
|
|
284
|
+
3. Leave something the human can look at and print its **absolute path**
|
|
285
|
+
(start from \`arbor path <task>\`). For anything visual or UX, that means a
|
|
286
|
+
screenshot; if producing one is expensive or has side effects, ask before
|
|
287
|
+
starting and say what it will cost.
|
|
288
|
+
|
|
289
|
+
If you claim a tree whose \`## Blocked\` question is unanswered, do not resume
|
|
290
|
+
or merge: ask the user and wait for the answer.
|
|
291
|
+
|
|
292
|
+
## Reporting
|
|
293
|
+
|
|
294
|
+
However a task ends, say so in one block; only a merge names a base:
|
|
295
|
+
|
|
296
|
+
\`\`\`markdown
|
|
297
|
+
### ✅ Merged \`<task>\` onto \`<base>\`
|
|
298
|
+
|
|
299
|
+
One sentence blending what the task set out to do with where it ended up.
|
|
300
|
+
\`\`\`
|
|
301
|
+
|
|
302
|
+
\`\`\`markdown
|
|
303
|
+
### ⚠️ Escalated \`<task>\`
|
|
304
|
+
|
|
305
|
+
One sentence blending what the task set out to do with what it now waits on.
|
|
306
|
+
\`\`\`
|
|
307
|
+
|
|
308
|
+
\`\`\`markdown
|
|
309
|
+
### 🛑 Removed \`<task>\`
|
|
310
|
+
|
|
311
|
+
One sentence blending what the task set out to do with why you \`arbor rm\`ed
|
|
312
|
+
it instead.
|
|
313
|
+
\`\`\`
|
|
314
|
+
|
|
315
|
+
Anything else worth saying goes after this block, not instead of it.
|
|
316
|
+
|
|
317
|
+
## ARBOR.md
|
|
318
|
+
|
|
319
|
+
Your session can die at any moment; \`ARBOR.md\` is what lets a stranger
|
|
320
|
+
\`arbor claim\` the task and continue. Fill in the stub \`add\` wrote to this
|
|
321
|
+
shape:
|
|
322
|
+
|
|
323
|
+
\`\`\`\`markdown
|
|
324
|
+
# <task>
|
|
325
|
+
|
|
326
|
+
## Goal
|
|
327
|
+
|
|
328
|
+
One or two lines on what done means.
|
|
329
|
+
|
|
330
|
+
## Files
|
|
331
|
+
|
|
332
|
+
- every/path/you/plan/to/touch.ts
|
|
333
|
+
|
|
334
|
+
## Done
|
|
335
|
+
|
|
336
|
+
- [x] finished steps move here: these checkboxes are the only progress the
|
|
337
|
+
task reports
|
|
338
|
+
|
|
339
|
+
## Next
|
|
340
|
+
|
|
341
|
+
- [ ] every step you can foresee, roughly one commit each
|
|
342
|
+
|
|
343
|
+
## Notes
|
|
344
|
+
|
|
345
|
+
Decisions, dead ends, and how to verify.
|
|
346
|
+
\`\`\`\`
|
|
347
|
+
|
|
348
|
+
Keep the whole file current throughout implementation, not at the end: after
|
|
349
|
+
each step lands, check it off and move it to \`## Done\`, and when the set of
|
|
350
|
+
files you are touching changes, change \`## Files\` to match. A stale plan is
|
|
351
|
+
worse than none, and a session that dies mid-task reports nothing.
|
|
352
|
+
|
|
353
|
+
\`arbor show <task>\` prints the file and every way it departs from the
|
|
354
|
+
expected shape; run it on your own task after writing the file. \`add\` excludes
|
|
355
|
+
\`ARBOR.md\` from git for you: never commit it, and never mention it in a commit
|
|
356
|
+
message.
|
|
357
|
+
|
|
358
|
+
## Committing
|
|
359
|
+
|
|
360
|
+
Plain, human-style commit messages with **no attribution**: no
|
|
361
|
+
\`Co-authored-by:\` trailers, no "Generated with", no agent or model names, no
|
|
362
|
+
\`--author\` overrides. Commit as often as it helps you; a task usually takes
|
|
363
|
+
fewer than 5 commits, and wanting many more means the task wants splitting,
|
|
364
|
+
not squashing.
|
|
365
|
+
`;
|
|
366
|
+
|
|
367
|
+
// templates/review.skill.md
|
|
368
|
+
var review_skill_default = `---
|
|
369
|
+
name: review
|
|
370
|
+
description: "Review a change against the RULE.md rules in this project's .wiz/rules directory by handing each rule to a subagent, without reading a rule yourself. Use only when the project has a .wiz/rules directory and the user asks to run, check, or apply the rules, the wiz rules, or the webappwiz rules to a change, or asks to write, add, or edit a rule there. Not for a general code review, a pull request review, a security review, or any review that does not name the rules."
|
|
371
|
+
version: 0.0.11
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
# Reviewing against the rules
|
|
375
|
+
|
|
376
|
+
The rules are markdown, one per directory under \`.wiz/rules\`, each a \`RULE.md\`
|
|
377
|
+
a subagent reads. You never read one. Reading rules loads their prose into
|
|
378
|
+
your context and you start reasoning about style instead of running the
|
|
379
|
+
review, so the whole loop is built to keep them out of your sight: the CLI
|
|
380
|
+
divides the work into blocks that name a rule's file, and a subagent opens it.
|
|
381
|
+
|
|
382
|
+
Run the CLI with \`bunx @webappwiz/cli rules <command>\` (or \`wiz cli rules\`
|
|
383
|
+
if \`wiz\` is on PATH). \`rules --help\` lists the commands; this file covers
|
|
384
|
+
only what the CLI cannot tell you.
|
|
385
|
+
|
|
386
|
+
## The loop
|
|
387
|
+
|
|
388
|
+
1. Make sure the work is finished and its tests pass. Review is the last step,
|
|
389
|
+
not a way to find out what to build.
|
|
390
|
+
2. Run \`rules review --since <ref>\`, with the ref the change is measured from:
|
|
391
|
+
\`main\` for a branch, \`HEAD\` for uncommitted work. It prints a summary line
|
|
392
|
+
and then one block per unit of work, each starting with a \`## \` heading.
|
|
393
|
+
3. Spawn one subagent per block, all in parallel, and give each the whole
|
|
394
|
+
block verbatim as its prompt. Add nothing. The block already says which
|
|
395
|
+
file to read, which files to judge, how to see the change, and how to
|
|
396
|
+
answer.
|
|
397
|
+
4. Collect the replies. Each is a JSON array of \`{file, line, message}\`, empty
|
|
398
|
+
when the rule found nothing.
|
|
399
|
+
5. Report the findings grouped by file, each with its rule id and the rule's
|
|
400
|
+
level from the block heading. Then stop. Fixing is a separate decision.
|
|
401
|
+
|
|
402
|
+
If a block's reply is not a JSON array, run that block again once, then
|
|
403
|
+
report it as unanswered rather than guessing what it found.
|
|
404
|
+
|
|
405
|
+
## Choosing a model
|
|
406
|
+
|
|
407
|
+
Every heading carries the rule's complexity:
|
|
408
|
+
|
|
409
|
+
\`\`\`
|
|
410
|
+
## no-em-dashes (3 files, complexity low)
|
|
411
|
+
\`\`\`
|
|
412
|
+
|
|
413
|
+
Complexity is how hard the rule is to judge. \`low\` is a grep or a count:
|
|
414
|
+
give it the cheapest, fastest model available. \`high\` is design judgment
|
|
415
|
+
across a whole file: give it the strongest. \`medium\` is whatever the harness
|
|
416
|
+
uses by default.
|
|
417
|
+
|
|
418
|
+
## Fixing
|
|
419
|
+
|
|
420
|
+
When asked to fix what the review found, spawn one subagent per finding with
|
|
421
|
+
a prompt like this, and nothing else about the rule:
|
|
422
|
+
|
|
423
|
+
\`\`\`
|
|
424
|
+
Read \`.wiz/rules/<id>/RULE.md\`. In \`<file>\` at line <line>, the code <message>.
|
|
425
|
+
Change the code so it follows the rule, touching as little as you can, and
|
|
426
|
+
reply with the diff.
|
|
427
|
+
\`\`\`
|
|
428
|
+
|
|
429
|
+
The subagent reads the rule and decides the fix. You still have not read it.
|
|
430
|
+
|
|
431
|
+
## Writing a rule
|
|
432
|
+
|
|
433
|
+
When asked to write a rule, or to add one from the catalog:
|
|
434
|
+
|
|
435
|
+
- \`rules ls\` lists every rule that ships and every rule the project has.
|
|
436
|
+
- \`rules add <id>\` copies a shipped rule into \`.wiz/rules/<id>/RULE.md\`,
|
|
437
|
+
where it runs and can be edited.
|
|
438
|
+
- \`rules new <name>\` scaffolds \`.wiz/rules/<name>/RULE.md\` to fill in. The
|
|
439
|
+
frontmatter has \`description\`, \`files\` (the glob a review matches changed
|
|
440
|
+
files against), \`level\` (\`error\` or \`warning\`), \`complexity\` (\`low\`,
|
|
441
|
+
\`medium\` or \`high\`). The body is yours, as a skill's is: the template
|
|
442
|
+
suggests a title, the prose a subagent judges by, and \`## Good\` and
|
|
443
|
+
\`## Bad\` sections with examples, and nothing checks for them. Fill it in,
|
|
444
|
+
delete the comment that explains it, and run \`rules ls\`: it validates the
|
|
445
|
+
frontmatter of every rule and names the line that is wrong.
|
|
446
|
+
|
|
447
|
+
Rules are English. A rule that only concerns some files says so in its glob;
|
|
448
|
+
a rule that only concerns files with some construct says so in its prose, and
|
|
449
|
+
the subagent that reads it greps or writes a script to find them. There is no
|
|
450
|
+
code to write for a rule, ever.
|
|
451
|
+
|
|
452
|
+
Writing a rule is the one time you read one, and only the one you are
|
|
453
|
+
writing. To remove a rule, delete its directory.
|
|
454
|
+
`;
|
|
455
|
+
|
|
456
|
+
// templates/webappwiz.skill.md
|
|
457
|
+
var webappwiz_skill_default = `---
|
|
458
|
+
name: webappwiz
|
|
459
|
+
description: "Check whether the webappwiz package already covers a piece of infrastructure before writing it by hand or adding a dependency for it. Read this before writing any of: time, clocks, durations or timers; logging; id generation; HTTP serving; CLI argument parsing; background tasks or queues; web workers; markdown parsing; typed event emitters; 2D geometry or spatial indexes; filesystem, env or process access; typed RPC over fetch; schema validation; AbortSignal plumbing; disposable resources; browser scroll, animation frames or visibility. Also use whenever the user says webappwiz."
|
|
460
|
+
version: 0.0.11
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
# Using webappwiz
|
|
464
|
+
|
|
465
|
+
\`webappwiz\` is the parts of a web app that get written again every time, behind
|
|
466
|
+
interfaces a test can replace. One package, one subpath per module. Before
|
|
467
|
+
writing any of that here, find out whether it already exists there.
|
|
468
|
+
|
|
469
|
+
Its README carries the whole catalogue, a table of every subpath and what it is
|
|
470
|
+
for. Read it from \`node_modules/webappwiz/README.md\`, or, in a project that has
|
|
471
|
+
not installed it yet, from
|
|
472
|
+
\`https://raw.githubusercontent.com/jaredjj3/webappwiz/main/packages/webappwiz/README.md\`.
|
|
473
|
+
|
|
474
|
+
Nothing in the table is close: say so in a line and write it here. Something is:
|
|
475
|
+
read that module's own README and the exports of its \`index.ts\`, and judge
|
|
476
|
+
against what is actually needed rather than the one-line blurb.
|
|
477
|
+
|
|
478
|
+
## It fits
|
|
479
|
+
|
|
480
|
+
\`bun add webappwiz\` and import the subpath. There is no package entry point, so
|
|
481
|
+
import \`webappwiz/time\`, never \`webappwiz\`. Fakes live under \`/testing\` beside
|
|
482
|
+
what they replace.
|
|
483
|
+
|
|
484
|
+
## It nearly fits
|
|
485
|
+
|
|
486
|
+
Do not vendor it, fork it, or patch \`node_modules\`. Write what this project
|
|
487
|
+
needs here so nobody is blocked, leave a \`TODO: webappwiz/<subpath> once <gap>\`
|
|
488
|
+
on it, and hand the gap over: print the block below and tell the user to give it
|
|
489
|
+
to an agent working on the webappwiz repo.
|
|
490
|
+
|
|
491
|
+
\`\`\`markdown
|
|
492
|
+
In \`packages/webappwiz/<subpath>\`: <the gap, in a sentence>.
|
|
493
|
+
|
|
494
|
+
Wanted by <this project> for <the usecase, concretely>.
|
|
495
|
+
|
|
496
|
+
What is there now: <the export that comes closest, and where it stops>.
|
|
497
|
+
What is missing: <the smallest change that closes the gap: one more method, a
|
|
498
|
+
widened parameter, another implementation of an interface>.
|
|
499
|
+
Called like: <the call site, written the way the caller wants to write it>.
|
|
500
|
+
\`\`\`
|
|
501
|
+
|
|
502
|
+
Describe the gap and stop. Do not design the API in the handoff: that repo has a
|
|
503
|
+
style guide and a review, and neither of them is here.
|
|
504
|
+
|
|
505
|
+
## It does not fit
|
|
506
|
+
|
|
507
|
+
One line naming the subpath you read and why it is not the one, then write it
|
|
508
|
+
here. A wrong module taken up is worse than one written twice.
|
|
509
|
+
|
|
510
|
+
## Rules
|
|
511
|
+
|
|
512
|
+
- Never edit the webappwiz repository from this project's thread.
|
|
513
|
+
- Never copy its source into this project.
|
|
514
|
+
- Reading the table is the whole check, and it is cheap. Do it before adding a
|
|
515
|
+
dependency, not after.
|
|
516
|
+
`;
|
|
517
|
+
|
|
518
|
+
// skills/skill.ts
|
|
519
|
+
var bundled = { arbor: arbor_skill_default, review: review_skill_default, webappwiz: webappwiz_skill_default };
|
|
520
|
+
var SKILLS = {
|
|
521
|
+
root: ".agents/skills",
|
|
522
|
+
file: "SKILL.md",
|
|
523
|
+
noun: "skill"
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
// skills/add.ts
|
|
527
|
+
async function add2(opts) {
|
|
528
|
+
const documents = new Documents(opts.skills ?? bundled, SKILLS, opts);
|
|
529
|
+
await documents.add(opts.skill, opts.dir);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// skills/ls.ts
|
|
533
|
+
import { ConsoleLogger as ConsoleLogger6, color as color3 } from "webappwiz/log";
|
|
534
|
+
async function ls2(opts) {
|
|
535
|
+
const log = opts.log ?? new ConsoleLogger6;
|
|
536
|
+
const documents = new Documents(opts.skills ?? bundled, SKILLS, opts);
|
|
537
|
+
const rows = [["skill", "ships", "installed"].map(color3.dim)];
|
|
538
|
+
let stale = 0;
|
|
539
|
+
for (const [name, doc] of documents.available()) {
|
|
540
|
+
const ships = versionOf(doc) ?? "?";
|
|
541
|
+
const installed = await documents.installedVersion(opts.dir, name);
|
|
542
|
+
if (installed !== null && installed !== ships) {
|
|
543
|
+
stale++;
|
|
544
|
+
}
|
|
545
|
+
rows.push([name, ships, installed ?? "-"]);
|
|
546
|
+
}
|
|
547
|
+
const lines = table(rows);
|
|
548
|
+
if (stale > 0) {
|
|
549
|
+
lines.push("", `${stale} out of date: run \`skills update\``);
|
|
550
|
+
}
|
|
551
|
+
log.info(lines.join(`
|
|
552
|
+
`));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// skills/update.ts
|
|
556
|
+
import { ConsoleLogger as ConsoleLogger7 } from "webappwiz/log";
|
|
557
|
+
async function update2(opts) {
|
|
558
|
+
const log = opts.log ?? new ConsoleLogger7;
|
|
559
|
+
const documents = new Documents(opts.skills ?? bundled, SKILLS, opts);
|
|
560
|
+
const refreshed = await documents.update(opts.dir);
|
|
561
|
+
if (refreshed.length === 0) {
|
|
562
|
+
log.info(`no webappwiz skills in ${opts.dir}: add one with \`skills add\``);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// update.ts
|
|
567
|
+
import { basename } from "node:path";
|
|
568
|
+
import { ConsoleLogger as ConsoleLogger8 } from "webappwiz/log";
|
|
569
|
+
import { NodeFs as NodeFs3, walk } from "webappwiz/system";
|
|
570
|
+
var DEPENDENCY = /("(?:webappwiz|@webappwiz\/[^"]+)"\s*:\s*")(?!workspace:)[^"]*(")/g;
|
|
571
|
+
async function update3(opts) {
|
|
572
|
+
const log = opts.log ?? new ConsoleLogger8;
|
|
573
|
+
const fs = opts.fs ?? new NodeFs3;
|
|
574
|
+
let count = 0;
|
|
575
|
+
for await (const path of walk(opts.dir, { fs })) {
|
|
576
|
+
if (basename(path) !== "package.json") {
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
const before = await fs.read(path);
|
|
580
|
+
const after = before.replace(DEPENDENCY, `$1${opts.version}$2`);
|
|
581
|
+
if (after === before) {
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
await fs.write(path, after);
|
|
585
|
+
log.info(`updated ${path}`);
|
|
586
|
+
count++;
|
|
587
|
+
}
|
|
588
|
+
log.info(`${count} package.json pinned to ${opts.version}`);
|
|
589
|
+
await update2({ dir: opts.dir, log, fs, skills: opts.skills });
|
|
590
|
+
await update({ dir: opts.dir, log, fs, rules: opts.rules });
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export { version, add, ls, newRule, review, update, add2 as add1, ls2 as ls1, update2 as update1, update3 as update2 };
|