kaizen-agent 1.35.0 → 1.36.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/agents/kaizen-builder.md +5 -1
- package/cli/browser.ts +3 -1
- package/cli/install.ts +14 -0
- package/cli/state.test.ts +29 -1
- package/cli/state.ts +35 -2
- package/cli/web.ts +31 -3
- package/package.json +1 -1
- package/skills/kaizen/spec-build.md +15 -4
- package/skills/kaizen/spec-main.md +13 -0
- package/skills/kaizen/spec-review.md +3 -3
- package/web/board/cards.ts +45 -1
- package/web/board/fixes.ts +1 -1
- package/web/board/icebox.ts +4 -0
- package/web/board/panel.ts +2 -1
- package/web/board/update.ts +8 -1
- package/web/board.html +8 -1
- package/web/board.js +20 -20
- package/web/notebook.js +1 -1
- package/web/term.js +1 -1
package/agents/kaizen-builder.md
CHANGED
|
@@ -68,7 +68,11 @@ change — do not refactor around it. Write what you changed to
|
|
|
68
68
|
- **Verification** — each check carried out, with its actual result. Quote failures.
|
|
69
69
|
- **Not done** — steps skipped, out-of-scope observations, known gaps.
|
|
70
70
|
- **Preview** — how to see the result: commands to run it locally and the URL for an
|
|
71
|
-
app; the path to open, or the result itself, for anything else.
|
|
71
|
+
app; the path to open, or the result itself, for anything else. It must embed at
|
|
72
|
+
least one screenshot of the result (``, saved under
|
|
73
|
+
`runs/<id>/attachments/`, e.g. `playwright screenshot <url> <path>`) whenever it shows on
|
|
74
|
+
a screen. On a revise of a run that already shows screenshots, retake them so
|
|
75
|
+
none shows the result from before the change. Then `### How to test`:
|
|
72
76
|
numbered steps from the plan's gate, each an action and what the person should see,
|
|
73
77
|
plus any test command with its expected result. `###` subheads only — the board ends
|
|
74
78
|
the section at the next `##`. The board shows it.
|
package/cli/browser.ts
CHANGED
|
@@ -220,7 +220,9 @@ export async function readPage(page: Page, url: string, platform: string, max =
|
|
|
220
220
|
const re = POST[platform]!;
|
|
221
221
|
await page.go(url);
|
|
222
222
|
if (!onPlatform(await page.js<string>("location.href")) || (await page.js<boolean>(WALL))) return { ok: false, note: signInNote(platform) };
|
|
223
|
-
|
|
223
|
+
// TikTok lays a closable check over posts it has already shown: close it and read on.
|
|
224
|
+
// A check that leaves no posts still fails below.
|
|
225
|
+
await page.js(`document.querySelector("#captcha_close_button")?.click()`);
|
|
224
226
|
const seen = new Map<string, { link: string; text: string; date: string; author?: string; images?: string[] }>();
|
|
225
227
|
for (let i = 0, still = 0; i < 12 && seen.size < max && still < 3; i++) {
|
|
226
228
|
const before = seen.size;
|
package/cli/install.ts
CHANGED
|
@@ -91,6 +91,20 @@ if (Bun.argv[2] === "browser") {
|
|
|
91
91
|
process.exit(0);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// `kaizen card add "<text>" [--parent <run id>]`: what a run files as new work, linked
|
|
95
|
+
// to the run it came from.
|
|
96
|
+
if (Bun.argv[2] === "card") {
|
|
97
|
+
const { locate, label, addCard } = await import("./state.ts");
|
|
98
|
+
const state = locate() ?? join(home, ".kaizen");
|
|
99
|
+
const args = Bun.argv.slice(4), at = args.indexOf("--parent");
|
|
100
|
+
const parent = at >= 0 ? args.splice(at, 2)[1] : undefined;
|
|
101
|
+
if (Bun.argv[3] !== "add" || !args.length || (at >= 0 && !parent)) { console.log(`\n kaizen card add "<text>" [--parent <run id>]\n`); process.exit(1); }
|
|
102
|
+
const r = addCard(state, args.join(" "), parent);
|
|
103
|
+
if (typeof r === "string") { console.log(`\n ${r}\n`); process.exit(1); }
|
|
104
|
+
console.log(`\n ${c.green("added")} ${args.join(" ")}\n ${c.dim(`parent ${r.parent} · project ${label(state)}`)}\n`);
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
107
|
+
|
|
94
108
|
if (Bun.argv[2] === "sources" || Bun.argv[2] === "gather" || Bun.argv[2] === "icebox") {
|
|
95
109
|
const { locate, label } = await import("./state.ts");
|
|
96
110
|
const src = await import("./sources.ts");
|
package/cli/state.test.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { issueUrl, systemInfo, scrubHome, REPO_URL, home as realHome, trustFolde
|
|
|
8
8
|
import * as stateModule from "./state.ts";
|
|
9
9
|
import {
|
|
10
10
|
readRuns, readInbox, writeInbox, replaceIdea, moveIdea, abandonRun, allBacklog, backlog,
|
|
11
|
-
requestTitle, statusOf, columnOf, parseItem, startedRuns, boardCards, readArchive, setArchived,
|
|
11
|
+
requestTitle, statusOf, columnOf, parseItem, startedRuns, boardCards, parentOf, addCard, readArchive, setArchived,
|
|
12
12
|
requestOf, agentFlags, plainModel, readNotes, writeNotes, manualCommand, parseNotes, formatNote, appendNote, saveAttachment,
|
|
13
13
|
reviewFindings, pickFindings, fixPrompt, approvalPrompt, sessionLaunch, closeSessions, sweepSessions, readSessions, ownerOf, CLOSE_GRACE_MS, gitStatus, runGit, cardsGit, commitPush, readCommit, findTerminal, nativePath, pruneRuns, unrechecked, notice, notifier, notifyArgs, LOGO, type Card,
|
|
14
14
|
} from "./state.ts";
|
|
@@ -1047,3 +1047,31 @@ test("requestTitle: drops the launch command, keeps the first sentence, caps at
|
|
|
1047
1047
|
expect(requestTitle("/kaizen lite simplify the card. Then more words")).toBe("simplify the card.");
|
|
1048
1048
|
expect(requestTitle("x".repeat(200)).length).toBe(90);
|
|
1049
1049
|
});
|
|
1050
|
+
|
|
1051
|
+
test("parent/child: a Parent line links a run and an idea to the run that filed them", () => {
|
|
1052
|
+
expect(parentOf("do x\nFollow-up of: 2026-01-01-a.")).toBe("2026-01-01-a");
|
|
1053
|
+
expect(parentOf(" [2026-10-18 20:57] Parent: 2026-01-01-a")).toBe("2026-01-01-a");
|
|
1054
|
+
expect(parentOf("no parent here")).toBeUndefined();
|
|
1055
|
+
const st = join(mkdtempSync(join(tmpdir(), "kaizen-pc-")), ".kaizen");
|
|
1056
|
+
for (const [id, req] of [["2026-01-01-a", "parent work"], ["2026-01-02-b", "child work\nParent: 2026-01-01-a"]]) {
|
|
1057
|
+
mkdirSync(join(st, "runs", id), { recursive: true });
|
|
1058
|
+
writeFileSync(join(st, "runs", id, "00-request.md"), req + "\n");
|
|
1059
|
+
writeFileSync(join(st, "runs", id, "state.json"), JSON.stringify({ stage: "done", awaiting: null }));
|
|
1060
|
+
}
|
|
1061
|
+
writeFileSync(join(st, "inbox.md"), "# Inbox\n\n- open: follow-on idea\n Parent: 2026-01-01-a\n");
|
|
1062
|
+
const cards = boardCards([st], Date.now());
|
|
1063
|
+
expect(cards.find((k) => k.id === "2026-01-01-a")!.children).toEqual(["2026-01-02-b"]);
|
|
1064
|
+
expect(cards.find((k) => k.id === "2026-01-02-b")!.parent).toBe("2026-01-01-a");
|
|
1065
|
+
expect(cards.find((k) => k.kind === "idea")!.parent).toBe("2026-01-01-a");
|
|
1066
|
+
// The done child is noted in its parent once, however often the board refreshes.
|
|
1067
|
+
boardCards([st], Date.now());
|
|
1068
|
+
expect(readFileSync(join(st, "runs", "2026-01-01-a", "notes.md"), "utf8").match(/Child 2026-01-02-b done: child work/g)!.length).toBe(1);
|
|
1069
|
+
// card add: no run in flight, so a parent must be named.
|
|
1070
|
+
expect(addCard(st, "next thing")).toMatch(/no run in flight/);
|
|
1071
|
+
writeFileSync(join(st, "runs", "2026-01-02-b", "state.json"), JSON.stringify({ stage: "build", awaiting: null }));
|
|
1072
|
+
expect(addCard(st, "next thing")).toEqual({ parent: "2026-01-02-b" });
|
|
1073
|
+
expect(addCard(st, "next thing")).toMatch(/already/);
|
|
1074
|
+
expect(addCard(st, "other", "nope")).toMatch(/no such run/);
|
|
1075
|
+
expect(boardCards([st], Date.now()).find((k) => k.text === "next thing")!.parent).toBe("2026-01-02-b");
|
|
1076
|
+
rmSync(dirname(st), { recursive: true, force: true });
|
|
1077
|
+
});
|
package/cli/state.ts
CHANGED
|
@@ -615,8 +615,16 @@ export type Card = {
|
|
|
615
615
|
branch?: string; // runs only: the run's own branch while it has a worktree
|
|
616
616
|
tags?: string[]; // runs only: runner, agent, "yolo", "unrechecked"
|
|
617
617
|
notes?: string; // inbox notes, or runs/<id>/notes.md
|
|
618
|
+
parent?: string; // the run this card was filed from
|
|
619
|
+
children?: string[]; // runs only: runs that name this one as parent
|
|
618
620
|
};
|
|
619
621
|
|
|
622
|
+
// A `Parent: <run id>` line (or the older `Follow-up of: <run id>`) in a request or
|
|
623
|
+
// an idea's notes is what links a card to the run it came from.
|
|
624
|
+
export function parentOf(text?: string) {
|
|
625
|
+
return /^\s*(?:\[[^\]]*\]\s*)?(?:Parent|Follow-up of):\s*(\S+)/im.exec(text ?? "")?.[1]?.replace(/[.,]$/, "");
|
|
626
|
+
}
|
|
627
|
+
|
|
620
628
|
// A run is called running on the evidence that it moved recently; there is no PID
|
|
621
629
|
// to ask. Anything in flight and older than this is stalled, not running -- the
|
|
622
630
|
// difference the board exists to show.
|
|
@@ -770,6 +778,20 @@ export function appendNote(state: string, target: { id: string } | { text: strin
|
|
|
770
778
|
return null;
|
|
771
779
|
}
|
|
772
780
|
|
|
781
|
+
// A card filed by a run: an open idea whose note names the run as its parent. The
|
|
782
|
+
// parent is the one named, else the newest run still in flight.
|
|
783
|
+
export function addCard(state: string, text: string, parent?: string): { parent: string } | string {
|
|
784
|
+
text = text.trim().replace(/\s+/g, " ");
|
|
785
|
+
if (!text) return "empty card";
|
|
786
|
+
const id = parent ?? readRuns(state).find((r) => r.stage !== "done" && r.stage !== "abandoned")?.id;
|
|
787
|
+
if (!id) return "no run in flight to be its parent; pass --parent <run id>";
|
|
788
|
+
if (!existsSync(join(state, "runs", id, "state.json"))) return `no such run: ${id}`;
|
|
789
|
+
const lines = readInbox(state);
|
|
790
|
+
if (lines.some((l) => l.status === "open" && l.text === text)) return "that card is already on the board";
|
|
791
|
+
writeInbox(state, [...lines, { status: "open", text, notes: `Parent: ${id}` }]);
|
|
792
|
+
return { parent: id };
|
|
793
|
+
}
|
|
794
|
+
|
|
773
795
|
// A file beside the notes it belongs to: runs/<id>/attachments/ for a run,
|
|
774
796
|
// <state>/attachments/ for an idea. Returns the path relative to the state dir,
|
|
775
797
|
// which is what the note links and what /file serves.
|
|
@@ -966,8 +988,8 @@ export function boardCards(states: string[], now: number): Card[] {
|
|
|
966
988
|
const t = normalise(it.text);
|
|
967
989
|
if (it.status === "started" ? requests.some((r) => r.includes(t)) : lines.some((l) => l === t || l.endsWith(" " + t))) continue;
|
|
968
990
|
next.push(it.status === "started"
|
|
969
|
-
? { kind: "idea", status: "starting", text: it.text, state: st, where, column: 1, awaiting: null, dim: true, archived: false, notes: it.notes }
|
|
970
|
-
: { kind: "idea", status: "idea", text: it.text, state: st, where, column: 0, awaiting: null, dim: false, archived: it.status === "archived", notes: it.notes });
|
|
991
|
+
? { kind: "idea", status: "starting", text: it.text, state: st, where, column: 1, awaiting: null, dim: true, archived: false, notes: it.notes, parent: parentOf(it.notes) }
|
|
992
|
+
: { kind: "idea", status: "idea", text: it.text, state: st, where, column: 0, awaiting: null, dim: false, archived: it.status === "archived", notes: it.notes, parent: parentOf(it.notes) });
|
|
971
993
|
}
|
|
972
994
|
// Runs started in Yolo. Only yolo records are matched to runs, so a project that
|
|
973
995
|
// never used it pays one directory read.
|
|
@@ -982,9 +1004,20 @@ export function boardCards(states: string[], now: number): Card[] {
|
|
|
982
1004
|
branch: r.branch,
|
|
983
1005
|
tags: [r.runner, r.agent, yolos.has(r.id) ? "yolo" : undefined, unrechecked(st, r.id) ? "unrechecked" : undefined].filter((t): t is string => !!t),
|
|
984
1006
|
notes: readNotes(st, r.id) || undefined,
|
|
1007
|
+
parent: parentOf(body(st, r.id)),
|
|
985
1008
|
});
|
|
986
1009
|
}
|
|
987
1010
|
}
|
|
1011
|
+
for (const k of next) if (k.kind === "run") {
|
|
1012
|
+
const kids = next.filter((c) => c.kind === "run" && c.parent === k.id && c.state === k.state);
|
|
1013
|
+
if (kids.length) k.children = kids.map((c) => c.id!);
|
|
1014
|
+
// A child that is done tells its parent once, so the parent has it on record
|
|
1015
|
+
// whether or not the agent remembered to.
|
|
1016
|
+
for (const c of kids) if (c.status === "done" && !(k.notes ?? "").includes(`Child ${c.id} done`)) {
|
|
1017
|
+
const line = `Child ${c.id} done: ${c.title || c.text}`;
|
|
1018
|
+
if (!appendNote(k.state, { id: k.id! }, line)) k.notes = (k.notes ? k.notes + "\n" : "") + line;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
988
1021
|
// Done reads newest first: by `moved`, the time each card prints as its age.
|
|
989
1022
|
// Sorted into the slots the done cards already hold, so no other card moves.
|
|
990
1023
|
const done = next.filter((k) => k.column === 4).sort((a, b) => (b.moved ?? 0) - (a.moved ?? 0));
|
package/cli/web.ts
CHANGED
|
@@ -77,6 +77,23 @@ export function newer(a: string, b: string) {
|
|
|
77
77
|
return false;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
// What each newer release brings, for the update bar's What's new: the GitHub release
|
|
81
|
+
// titles ("v1.35.0: project logos, ...") above `from`, newest first. Cached like
|
|
82
|
+
// npmLatest; GitHub unreachable gives none and the bar shows no What's new.
|
|
83
|
+
let rel: { list: { v: string; text: string }[]; at: number } = { list: [], at: 0 };
|
|
84
|
+
async function releaseNotes(from: string, fresh = false) {
|
|
85
|
+
if (fresh || !rel.list.length || Date.now() - rel.at > 6 * 3600_000) {
|
|
86
|
+
try {
|
|
87
|
+
const r = await fetch("https://api.github.com/repos/hfadhlullah/kaizen/releases?per_page=30", { signal: AbortSignal.timeout(10000) });
|
|
88
|
+
const list = ((await r.json()) as { tag_name: string; name?: string }[])
|
|
89
|
+
.map((x) => ({ v: x.tag_name.replace(/^v/, ""), text: (x.name || "").replace(/^v?[\d.]+:\s*/, "") }))
|
|
90
|
+
.filter((x) => x.text);
|
|
91
|
+
rel = { list, at: Date.now() };
|
|
92
|
+
} catch {}
|
|
93
|
+
}
|
|
94
|
+
return rel.list.filter((x) => newer(x.v, from));
|
|
95
|
+
}
|
|
96
|
+
|
|
80
97
|
type Opts = { port?: number; open?: boolean };
|
|
81
98
|
|
|
82
99
|
export async function web(repoDir: string, opts: Opts = {}) {
|
|
@@ -230,7 +247,13 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
230
247
|
if (ms) for (const dir of states(true)) {
|
|
231
248
|
let list;
|
|
232
249
|
try { list = readSources(dir); } catch { continue; } // unreadable: left as it is
|
|
233
|
-
|
|
250
|
+
// Social sources need a reader that picks posts: only ChatGPT can do that unattended.
|
|
251
|
+
const gpt = await setting("sources.agent") === "chatgpt" && chatgpt.status().connected;
|
|
252
|
+
for (const s of list) {
|
|
253
|
+
if (!due(s, ms, Date.now())) continue;
|
|
254
|
+
if (!socialOf(s.url)) { if ((await gatherSource(dir, s.url)).added) changed(); }
|
|
255
|
+
else if (gpt) { await chatgpt.gatherSource(dir, s.url, await setting("sources.model"), (await setting("sources.effort")) || "low"); changed(); }
|
|
256
|
+
}
|
|
234
257
|
}
|
|
235
258
|
} catch { /* the next tick tries again */ }
|
|
236
259
|
busy = false;
|
|
@@ -417,11 +440,14 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
417
440
|
const markable = (c: Card) => c.kind === "run" && !c.archived && (c.status === "done" || c.awaiting === "approvals.review") && c.state !== join(home, ".kaizen");
|
|
418
441
|
for (const d of new Set(cards.filter(markable).map((c) => c.state)))
|
|
419
442
|
marks.set(d, cardsGit(d, cards.filter((c) => markable(c) && c.state === d).map((c) => c.id!)));
|
|
443
|
+
// Open findings the panel's Fix findings form would offer, on the same runs.
|
|
444
|
+
const openFindings = (c: Card) => c.kind === "run" && !c.archived && (c.status === "done" || c.awaiting === "findings" || c.awaiting === "approvals.review") && c.state !== join(home, ".kaizen")
|
|
445
|
+
? reviewFindings(c.state, c.id!).filter((f) => !f.done).length || undefined : undefined;
|
|
420
446
|
return json({
|
|
421
447
|
project, all, dir: state,
|
|
422
448
|
projects: picker.map((d) => ({ dir: d, label: label(d), logo: !!projectLogo(d) })),
|
|
423
449
|
fresh: unset.map((d) => ({ dir: join(d, ".kaizen"), label: tilde(d) })),
|
|
424
|
-
cards: cards.map((c) => ({ ...c, notice: notice(c), git: markable(c) ? marks.get(c.state)?.[c.id!] : undefined, pty: ptyOf(c) })),
|
|
450
|
+
cards: cards.map((c) => ({ ...c, notice: notice(c), git: markable(c) ? marks.get(c.state)?.[c.id!] : undefined, open: openFindings(c), pty: ptyOf(c) })),
|
|
425
451
|
now: Date.now(),
|
|
426
452
|
});
|
|
427
453
|
}
|
|
@@ -520,7 +546,9 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
520
546
|
// What this board runs and what npm has: the page shows an Update button on a newer one.
|
|
521
547
|
if (req.method === "GET" && path === "/version") {
|
|
522
548
|
const v = await npmLatest(url.searchParams.has("fresh"));
|
|
523
|
-
|
|
549
|
+
const update = !!v && newer(v, VERSION);
|
|
550
|
+
const notes = update ? await releaseNotes(VERSION, url.searchParams.has("fresh")) : [];
|
|
551
|
+
return json({ version: VERSION, latest: v, update, notes });
|
|
524
552
|
}
|
|
525
553
|
|
|
526
554
|
// `kaizen upgrade`, run here with no window, then a new board on this port: it
|
package/package.json
CHANGED
|
@@ -108,15 +108,26 @@ commands to start it locally and the URL to open, copied from the material's own
|
|
|
108
108
|
where one exists. For anything else — a document, copy, a runbook, a memo — the path
|
|
109
109
|
to open, or the result itself when it fits in a screen. The board shows this section on
|
|
110
110
|
the run's card, so a person can try the work without reading the rest of the report.
|
|
111
|
-
|
|
111
|
+
Every run whose result shows on a screen must put at least one screenshot of it here,
|
|
112
|
+
on any track and in every runner — this is required, not optional: run the result and capture it.
|
|
112
113
|
A UI (a page, a board, a terminal screen, an app window) gets a screenshot of the changed
|
|
113
|
-
screen; a
|
|
114
|
-
in a code block.
|
|
114
|
+
screen; a document gets one of it rendered; a CLI gets one of the terminal running it, with
|
|
115
|
+
the real output also pasted in a code block. Only a result with no screen at all — a
|
|
116
|
+
library, an API — may give command output alone. A report with no screenshot is incomplete. Save each under
|
|
115
117
|
`runs/<id>/attachments/` (letters, digits, `.`, `-`, `_` only) and embed it as
|
|
116
118
|
``; the board shows them in the Preview tab.
|
|
117
|
-
A headless browser does it: `playwright screenshot <url> runs/<id>/attachments/<name>.png
|
|
119
|
+
A headless browser does it: `playwright screenshot <url> runs/<id>/attachments/<name>.png`,
|
|
120
|
+
or `chromium --headless --screenshot=<abs path>.png --window-size=1280,900 <url>` where
|
|
121
|
+
playwright has no browser installed. A file (document, diff, output) opens as `file://<path>`.
|
|
118
122
|
Where no proof can be produced, say why in one line under `## Preview`.
|
|
119
123
|
|
|
124
|
+
**A revise retakes the screenshots.** When the run being revised, or the run a follow-up
|
|
125
|
+
names in `Follow-up of:`, already shows screenshots in its `## Preview`, the build that
|
|
126
|
+
answers the revise captures the same views again after the change. In a revised run,
|
|
127
|
+
overwrite each stale image under the same name (or replace its embed) so the Preview tab
|
|
128
|
+
never shows the result from before the revise; in a follow-up run, embed the new ones in
|
|
129
|
+
its own `## Preview`. A view the revise removed is dropped, not left showing the old result.
|
|
130
|
+
|
|
120
131
|
It also says how to test the result, under a `### How to test` subhead: numbered steps a
|
|
121
132
|
person follows to check the run did what was asked, each an action and what they should
|
|
122
133
|
see — "open Settings → Board, clear the field: it shows `not set`". Derive the steps
|
|
@@ -118,3 +118,16 @@ change is that it is written down rather than spoken and discarded.
|
|
|
118
118
|
Set `stage: "done"` in `state.json` once the user accepts.
|
|
119
119
|
|
|
120
120
|
---
|
|
121
|
+
|
|
122
|
+
## Child cards
|
|
123
|
+
|
|
124
|
+
A run may file new work as its own card instead of growing its scope — under either
|
|
125
|
+
runner. Under `full`, subagents only suggest it in their report; the main thread files it,
|
|
126
|
+
from the project folder: `kaizen card add "<the work, one line>"`. It links the card to
|
|
127
|
+
the run in flight (`--parent <run id>` names another), and the link follows the card into
|
|
128
|
+
the run it becomes. The board shows the parent's name on the child.
|
|
129
|
+
|
|
130
|
+
When a child run reaches `done`, the board appends `Child <child id> done: <title>` to
|
|
131
|
+
the parent's `notes.md` by itself. The parent documents it from there: if it is still in
|
|
132
|
+
flight its next stage reads `notes.md`; if it is already `done`, the entry is the record.
|
|
133
|
+
Never reopen a finished parent.
|
|
@@ -43,9 +43,9 @@ Five checks are universal, in every track:
|
|
|
43
43
|
now is a `high` finding at minimum, whatever else the work achieved. If the builder
|
|
44
44
|
recorded no baseline, that absence is itself the finding. So is a `03-impl.md`
|
|
45
45
|
without a `## Preview` section, or one without `### How to test` steps: `medium`,
|
|
46
|
-
since the user is left with no way to try the work. A
|
|
47
|
-
`## Preview`
|
|
48
|
-
no reason is `high`.
|
|
46
|
+
since the user is left with no way to try the work. A run whose result
|
|
47
|
+
shows on a screen and whose `## Preview` embeds no screenshot, or a `code` run with no
|
|
48
|
+
proof at all, and that gives no reason, is `high`.
|
|
49
49
|
|
|
50
50
|
5. **Acceptance** — every criterion, `G-nn` and `R-nn`, answered against the work
|
|
51
51
|
rather than against `03-impl.md`, by trying to falsify it: name the concrete check
|
package/web/board/cards.ts
CHANGED
|
@@ -31,6 +31,14 @@ const picked = new Set();
|
|
|
31
31
|
export const key = (c) =>
|
|
32
32
|
c._k || (c.kind === "idea" ? "idea:" + c.state + ":" + c.text : "run:" + c.state + ":" + c.id);
|
|
33
33
|
|
|
34
|
+
// Cards filed by a run sit inside it while it is on the board and not done; once the
|
|
35
|
+
// parent is done, archived or gone, they are back in their own columns.
|
|
36
|
+
const kidsOf = (p) => (p.kind === "run" ? store.S.cards.filter((c) => c.parent === p.id && c.state === p.state) : []);
|
|
37
|
+
const nested = (c) =>
|
|
38
|
+
!!c.parent &&
|
|
39
|
+
store.S.cards.some((p) => p.kind === "run" && p.id === c.parent && p.state === c.state && !p.archived && p.column !== 4);
|
|
40
|
+
const kidsOpen = new Set();
|
|
41
|
+
|
|
34
42
|
function card(c) {
|
|
35
43
|
const k = key(c),
|
|
36
44
|
el = document.createElement("div");
|
|
@@ -73,6 +81,9 @@ function card(c) {
|
|
|
73
81
|
// A gathered idea's notes open with the stamp promote() writes (ingest() wrote it before
|
|
74
82
|
// the icebox); its chip names the source.
|
|
75
83
|
const g = c.notes && /^\[[^\]]*\] (?:Gathered from|Promoted from the icebox:) (.+?)\. Outside text/.exec(c.notes);
|
|
84
|
+
// A card filed by a run names that run; the parent itself shows nothing.
|
|
85
|
+
if (c.parent)
|
|
86
|
+
chips.push(`<span class="chip link" title="Filed by ${esc(c.parent)}">${esc(c.parent.replace(/^\d{4}-\d\d-\d\d-/, "").replace(/-/g, " "))}</span>`);
|
|
76
87
|
if (c.branch) chips.push(`<span class="chip" title="Built on its own branch, ${esc(c.branch)}">branch</span>`);
|
|
77
88
|
if (g) chips.push(`<span class="chip" title="From ${esc(g[1])}">gathered</span>`);
|
|
78
89
|
else if (c.notes) chips.push(`<span class="chip">notes</span>`);
|
|
@@ -85,12 +96,45 @@ function card(c) {
|
|
|
85
96
|
c.git === "pushed" ? " ok" : " warn",
|
|
86
97
|
),
|
|
87
98
|
);
|
|
99
|
+
if (c.open) marks.push(mk("i-bug", `${c.open} open finding${c.open === 1 ? "" : "s"} to fix`, " warn"));
|
|
88
100
|
const pick = picking && !archive && c.column === 4;
|
|
89
101
|
el.innerHTML =
|
|
90
102
|
(pick ? `<input type="checkbox" class="pick" ${picked.has(k) ? "checked" : ""} aria-label="Select">` : "") +
|
|
91
103
|
`<div class="c-id"><span class="st d-${st}" role="img" title="${esc(sttl)}" aria-label="${esc(sttl)}"><svg><use href="#s-${st}"/></svg></span><span class="idt">${esc(idt)}</span><span class="age">${c.kind === "run" ? age(c.moved || store.S.now, store.S.now) : ""}</span><button class="ib sm more" data-a="menu" title="Actions" aria-label="Actions" aria-haspopup="menu" aria-expanded="${k === store.menuKey}">${ico("more")}</button></div>
|
|
92
104
|
<div class="c-title"${c.kind === "idea" && brief(c.text).parts.length ? ` title="${esc(c.text)}"` : ""}>${esc(c.kind === "idea" ? brief(c.text).head : c.title || c.id)}</div>
|
|
93
105
|
${chips.length || marks.length ? `<div class="c-foot"><span class="c-chips">${chips.join("")}</span><span class="c-marks">${marks.join("")}</span></div>` : ""}`;
|
|
106
|
+
const kids = kidsOf(c);
|
|
107
|
+
if (kids.length) {
|
|
108
|
+
const on = kidsOpen.has(k),
|
|
109
|
+
nDone = kids.filter((x) => x.status === "done").length,
|
|
110
|
+
wait = kids.some((x) => x.status === "waiting");
|
|
111
|
+
const box = document.createElement("div");
|
|
112
|
+
box.className = "kids";
|
|
113
|
+
box.innerHTML =
|
|
114
|
+
`<button class="kids-t" aria-expanded="${on}">${on ? "▾" : "▸"} ${kids.length} ${kids.length === 1 ? "child" : "children"}${nDone ? ` · ${nDone} done` : ""}${wait ? `<span class="kids-w" title="A child is waiting on you"></span>` : ""}</button>` +
|
|
115
|
+
(on
|
|
116
|
+
? `<ul>${kids
|
|
117
|
+
.map((x, i) => {
|
|
118
|
+
const s = ICON[x.status] || "idea";
|
|
119
|
+
return `<li data-i="${i}" tabindex="0" role="button" title="Open"><span class="st d-${s}"><svg><use href="#s-${s}"/></svg></span><span class="kids-s">${esc(WORD[x.status] || x.status)}</span><span class="kids-n" title="${esc(x.title || x.text || x.id)}">${esc(x.kind === "idea" ? brief(x.text).head : (x.text || x.id || "").replace(/-/g, " "))}</span></li>`;
|
|
120
|
+
})
|
|
121
|
+
.join("")}</ul>`
|
|
122
|
+
: "");
|
|
123
|
+
box.querySelector(".kids-t").onclick = (e) => {
|
|
124
|
+
e.stopPropagation();
|
|
125
|
+
on ? kidsOpen.delete(k) : kidsOpen.add(k);
|
|
126
|
+
render();
|
|
127
|
+
};
|
|
128
|
+
box.querySelectorAll("li").forEach((li) => {
|
|
129
|
+
const go = (e) => {
|
|
130
|
+
e.stopPropagation();
|
|
131
|
+
open(kids[+li.dataset.i]);
|
|
132
|
+
};
|
|
133
|
+
li.onclick = go;
|
|
134
|
+
li.onkeydown = (e) => (e.key === "Enter" || e.key === " ") && (e.preventDefault(), go(e));
|
|
135
|
+
});
|
|
136
|
+
el.appendChild(box);
|
|
137
|
+
}
|
|
94
138
|
if (k === store.menuKey) {
|
|
95
139
|
const items = [];
|
|
96
140
|
if (archive) {
|
|
@@ -423,7 +467,7 @@ export function render() {
|
|
|
423
467
|
}
|
|
424
468
|
COLS.forEach((name, ci) => {
|
|
425
469
|
const s = document.createElement("section");
|
|
426
|
-
const list = cs.filter((c) => c.column === ci && (!filter || c.status === filter));
|
|
470
|
+
const list = cs.filter((c) => c.column === ci && (!filter || c.status === filter) && (archive || !nested(c)));
|
|
427
471
|
// Runs age by their last move, ideas by inbox position (new ones are appended).
|
|
428
472
|
if (ci < 4) {
|
|
429
473
|
const at = (c) => (c.kind === "run" ? c.moved || 0 : store.S.cards.indexOf(c)),
|
package/web/board/fixes.ts
CHANGED
|
@@ -40,7 +40,7 @@ export function fixForm(c) {
|
|
|
40
40
|
fs
|
|
41
41
|
.map((f) => {
|
|
42
42
|
const x = f.done ? " (closed)" : sent(f) ? " (sent)" : "";
|
|
43
|
-
return `<label class="${x ? "off" : ""}"><input type="checkbox" data-n="${f.n}" ${x ? "checked disabled" : fixOff.has(k(f.n)) ? "" : "checked"}><span>${f.n}. ${md(f.text).replace(/^<p>|<\/p>$/g, "")}${x}</span></label>`;
|
|
43
|
+
return `<label class="${f.done ? "off done" : x ? "off" : ""}"><input type="checkbox" data-n="${f.n}" ${x ? "checked disabled" : fixOff.has(k(f.n)) ? "" : "checked"}><span>${f.n}. ${md(f.text).replace(/^<p>|<\/p>$/g, "")}${x}</span></label>`;
|
|
44
44
|
})
|
|
45
45
|
.join("") +
|
|
46
46
|
(open.length
|
package/web/board/icebox.ts
CHANGED
|
@@ -391,6 +391,8 @@ export async function openSources() {
|
|
|
391
391
|
G.use === "ask" && !G.connected
|
|
392
392
|
? `<div class="cg-onb"><b>Gather social posts with your ChatGPT plan</b><p>Connect ChatGPT Plus or Pro and Gather reads X, TikTok, Instagram and Threads sources right here, counted against your plan, with no agent to open. Skip it and Gather keeps using your agent. You can change this in Settings.</p><div class="acts"><button class="btn ghost" id="cg-skip">Skip for now</button><button class="btn pri" id="cg-go">Continue with ChatGPT</button></div></div>`
|
|
393
393
|
: "";
|
|
394
|
+
// A rebuild replaces the drawer: keep where it was scrolled to.
|
|
395
|
+
const at = $("src-sec")?.querySelector(".body")?.scrollTop ?? 0;
|
|
394
396
|
$("pbody").innerHTML =
|
|
395
397
|
d && d.error
|
|
396
398
|
? `<p style="color:var(--dim)">${esc(d.error)}</p>`
|
|
@@ -398,6 +400,8 @@ export async function openSources() {
|
|
|
398
400
|
? `<p style="color:var(--dim)">The icebox could not be read.</p>`
|
|
399
401
|
: onb + bar + bulk + fails + inbox + sec;
|
|
400
402
|
if (!d || d.error) return;
|
|
403
|
+
const body = $("src-sec")?.querySelector(".body");
|
|
404
|
+
if (body) body.scrollTop = at;
|
|
401
405
|
if (iceDraft && iceDraft[0] === iceForm && $("ice-title")) {
|
|
402
406
|
$("ice-title").value = iceDraft[1];
|
|
403
407
|
$("ice-details").value = iceDraft[2];
|
package/web/board/panel.ts
CHANGED
|
@@ -130,7 +130,8 @@ export async function openRun(c, refresh, again) {
|
|
|
130
130
|
// The board's own status mark, then what it waits on: chips, not a dotted line.
|
|
131
131
|
$("psub").innerHTML =
|
|
132
132
|
`<span class="pst"><span class="st d-${ICON[c.status] || "idea"}"><svg><use href="#s-${ICON[c.status] || "idea"}"/></svg></span>${esc(cap(WORD[c.status] || c.status))}</span>` +
|
|
133
|
-
(c.awaiting ? `<span class="pst dim">${esc(cap(need(c.awaiting)))}</span>` : "")
|
|
133
|
+
(c.awaiting ? `<span class="pst dim">${esc(cap(need(c.awaiting)))}</span>` : "") +
|
|
134
|
+
(c.parent ? `<span class="pst link" title="Filed by this run">${esc(c.parent)}</span>` : "");
|
|
134
135
|
// Only tabs with something in them show. A card whose agent runs in the board gets its
|
|
135
136
|
// terminal behind the header's terminal button, chosen once right after a launch from here.
|
|
136
137
|
const tabs = TABS.filter(
|
package/web/board/update.ts
CHANGED
|
@@ -47,7 +47,14 @@ export function updBar() {
|
|
|
47
47
|
}
|
|
48
48
|
const d = document.createElement("div");
|
|
49
49
|
d.className = "updbar";
|
|
50
|
-
|
|
50
|
+
// What's new: each newer release's one-line title, shown on hover, focus or click.
|
|
51
|
+
const notes = (upd.notes || [])
|
|
52
|
+
.map((n) => `<li><b>${esc(n.v)}</b> ${esc(n.text)}</li>`)
|
|
53
|
+
.join("");
|
|
54
|
+
const news = notes
|
|
55
|
+
? `<span class="updnew"><button class="btn" aria-describedby="upd-notes">What's new</button><span class="updpop" id="upd-notes" role="tooltip"><ul>${notes}</ul><a href="https://github.com/hfadhlullah/kaizen/releases" target="_blank" rel="noopener">All release notes</a></span></span>`
|
|
56
|
+
: "";
|
|
57
|
+
d.innerHTML = `<span>kaizen <b>${esc(upd.latest)}</b> is available. You are running ${esc(upd.version)}.</span><span class="updact">${news}<button class="btn pri" id="upd-go">Update kaizen</button></span>`;
|
|
51
58
|
$("pbody").prepend(d);
|
|
52
59
|
const b = $("upd-go");
|
|
53
60
|
b.onclick = async () => {
|
package/web/board.html
CHANGED
|
@@ -179,6 +179,8 @@ section h2 .clear{margin-left:auto;min-height:22px;padding:0 8px}
|
|
|
179
179
|
.chip.accent i{background:var(--accent)}.chip.accent svg{color:var(--accent)}
|
|
180
180
|
.chip.ok svg{color:var(--ok)}
|
|
181
181
|
.chip.run{color:var(--ink)}.chip.run i{background:var(--run)}
|
|
182
|
+
.chip.link,.pst.link{color:var(--accent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 40%,transparent)}.pst.link{padding-left:9px;background:color-mix(in srgb,var(--accent) 10%,transparent)}
|
|
183
|
+
.kids{margin-top:6px;padding-top:6px;border-top:1px solid color-mix(in srgb,var(--line) 70%,transparent)}.kids-t{all:unset;cursor:pointer;display:flex;align-items:center;gap:6px;width:100%;font-size:12px;color:var(--dim)}.kids-t:focus-visible,.kids li:focus-visible{outline:2px solid var(--accent);border-radius:4px}.kids-w{width:7px;height:7px;border-radius:50%;background:var(--warn);margin-left:auto}.kids ul{list-style:none;margin:4px 0 0;padding:0;display:flex;flex-direction:column;gap:1px}.kids li{display:grid;grid-template-columns:16px auto 1fr;gap:8px;align-items:start;font-size:12px;padding:3px 4px;border-radius:6px;cursor:pointer;min-width:0}.kids li:hover{background:var(--hover)}.kids-s{font-family:var(--mono);font-size:11px;color:var(--dim);white-space:nowrap}.kids-n{overflow-wrap:anywhere;line-height:1.35;color:var(--ink)}.kids .st{width:14px;height:14px}
|
|
182
184
|
.st.d-idea{color:var(--dim)}.st.d-start{color:var(--run)}.st.d-run{color:var(--run)}.st.d-stall{color:var(--dim)}.st.d-wait{color:var(--warn)}.st.d-done{color:var(--ok)}.st.d-aband{color:var(--dim)}
|
|
183
185
|
.d-run{color:var(--run)}.d-wait{color:var(--warn)}.d-done{color:var(--ok)}
|
|
184
186
|
.pulse{display:inline-block;width:7px;height:7px;border-radius:50%;background:currentColor;vertical-align:middle}
|
|
@@ -212,7 +214,7 @@ aside.wide{width:100%;border-left:0}#pbody:has(.term){display:flex;flex-directio
|
|
|
212
214
|
.chat{display:flex;flex-direction:column;gap:10px;margin:0 0 14px}.msg{border:1px solid var(--line);border-radius:var(--r);padding:10px 12px;background:var(--bg)}.msg time{display:block;font-size:11px;color:var(--dim);margin-bottom:4px}.msg p:last-child{margin-bottom:0}.msg img{max-width:100%;max-height:260px;border-radius:var(--r-s);display:block;margin:4px 0}
|
|
213
215
|
.notes .tools{display:flex;gap:6px;align-items:center}.notes .tools span{flex:1;font-size:11px;color:var(--dim)}.notes .att{font-size:11px;color:var(--dim)}
|
|
214
216
|
.inl pre.files{margin:0;max-height:140px;overflow:auto;font:12px/1.5 var(--mono);color:var(--dim)}
|
|
215
|
-
.inl.fix{margin-top:16px}.inl.fix label{display:flex;gap:8px;align-items:baseline;font-size:13px;cursor:pointer}.inl.fix label.off{color:var(--dim);cursor:default}.inl.fix input{flex:none;width:auto;margin:0;accent-color:var(--accent);position:relative;top:2px}
|
|
217
|
+
.inl.fix{margin-top:16px}.inl.fix label{display:flex;gap:8px;align-items:baseline;font-size:13px;cursor:pointer}.inl.fix label.off{color:var(--dim);cursor:default}.inl.fix label.done span{text-decoration:line-through}.inl.fix input{flex:none;width:auto;margin:0;accent-color:var(--accent);position:relative;top:2px}
|
|
216
218
|
.inl.qa{max-height:60vh;overflow:auto}.inl.qa fieldset{border:0;padding:0;margin:0;display:flex;flex-direction:column;gap:4px}.inl.qa legend{font-size:13px;font-weight:500;padding:0;margin-bottom:4px}.inl.qa label{display:flex;gap:8px;align-items:baseline;font-size:13px;cursor:pointer}.inl.qa label input{flex:none;width:auto;margin:0;accent-color:var(--accent);position:relative;top:2px}
|
|
217
219
|
.inl .q{font-size:14px;font-weight:500}.inl .q.dim{color:var(--dim);font-size:13px;font-weight:400}
|
|
218
220
|
.empty{grid-column:1/-1;display:flex;flex-direction:column;align-items:center;gap:12px;padding-block:60px;color:var(--dim);text-align:center}
|
|
@@ -312,6 +314,11 @@ aside .foot .more .menu-pop{left:0;right:auto;top:auto;bottom:calc(100% + 4px)}.
|
|
|
312
314
|
.toast.on{opacity:1}
|
|
313
315
|
.rail a.upd svg{overflow:visible}.rail a.upd{position:relative}.rail a.upd::after{content:"";position:absolute;left:28px;top:8px;width:8px;height:8px;border-radius:50%;background:var(--accent);box-shadow:0 0 0 2px var(--bg)}
|
|
314
316
|
.updbar{display:flex;align-items:center;gap:12px;flex-wrap:wrap;justify-content:space-between;margin:0 0 36px;padding:10px 14px;border:1px solid var(--accent);border-radius:var(--r);background:color-mix(in srgb,var(--accent) 10%,var(--bg-2));font-size:13px}
|
|
317
|
+
.updact{display:flex;gap:8px;align-items:center}
|
|
318
|
+
.updnew{position:relative}
|
|
319
|
+
.updpop{display:none;position:absolute;right:0;top:calc(100% + 6px);z-index:20;width:min(360px,80vw);padding:10px 14px;border:1px solid var(--line);border-radius:var(--r);background:var(--bg-2);box-shadow:var(--shadow-1);font-size:13px;line-height:1.5}
|
|
320
|
+
.updnew:hover .updpop,.updnew:focus-within .updpop{display:block}
|
|
321
|
+
.updpop ul{margin:0 0 8px;padding-left:18px}.updpop li{margin:2px 0}.updpop a{color:var(--accent)}
|
|
315
322
|
.updbar.quiet{border-color:var(--line);background:var(--bg-2);color:var(--dim)}
|
|
316
323
|
.toast.on.rep{pointer-events:auto}.toast a{color:inherit;margin-left:8px;text-decoration:underline;text-underline-offset:2px}
|
|
317
324
|
.help{position:fixed;inset:0;z-index:12;display:none;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bg) 60%,transparent);backdrop-filter:blur(3px)}
|