kaizen-agent 1.8.0 → 1.8.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/cli/release.sh +2 -2
- package/cli/state.test.ts +6 -1
- package/cli/state.ts +32 -8
- package/cli/web.ts +9 -3
- package/package.json +1 -1
- package/web/board.html +7 -3
package/cli/release.sh
CHANGED
|
@@ -38,8 +38,8 @@ git log --reverse --format='### %s%n%n%b' ${prev:+"$prev.."}"$tag" \
|
|
|
38
38
|
| grep -vE '^### Release [0-9.]+:' \
|
|
39
39
|
| cat -s > "$notes"
|
|
40
40
|
[ -n "$prev" ] && printf '\n**Full Changelog**: https://github.com/hfadhlullah/kaizen/compare/%s...%s\n' "$prev" "$tag" >> "$notes"
|
|
41
|
-
gh release create "$tag" --title "$title" --notes-file "$notes" >/dev/null
|
|
41
|
+
err="$(gh release create "$tag" --title "$title" --notes-file "$notes" 2>&1 >/dev/null)" \
|
|
42
42
|
&& echo "release: published $tag https://github.com/hfadhlullah/kaizen/releases/tag/$tag" \
|
|
43
|
-
|| echo "release: tag pushed, release not created"
|
|
43
|
+
|| echo "release: tag pushed, release not created: $err"
|
|
44
44
|
rm -f "$notes"
|
|
45
45
|
exit 0
|
package/cli/state.test.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
readRuns, readInbox, writeInbox, replaceIdea, abandonRun, allBacklog, backlog,
|
|
9
9
|
statusOf, columnOf, parseItem, startedRuns, boardCards, readArchive, setArchived,
|
|
10
10
|
requestOf, agentFlags, readNotes, writeNotes, manualCommand, parseNotes, formatNote, appendNote, saveAttachment,
|
|
11
|
-
reviewFindings, pickFindings, fixPrompt, gitStatus, runGit, commitPush, readCommit, findTerminal, pruneRuns, unrechecked, notice, notifier, notifyArgs, LOGO, type Card,
|
|
11
|
+
reviewFindings, pickFindings, fixPrompt, gitStatus, runGit, cardsGit, commitPush, readCommit, findTerminal, pruneRuns, unrechecked, notice, notifier, notifyArgs, LOGO, type Card,
|
|
12
12
|
} from "./state.ts";
|
|
13
13
|
|
|
14
14
|
let state: string;
|
|
@@ -274,6 +274,8 @@ test("commitPush: commits the run's files only, pushes, and records it; the mess
|
|
|
274
274
|
writeFileSync(join(st, "runs", "r1", "03-impl.md"), "## Changed\n\n- `.gitignore`: x\n- `a.txt:1`: y\n- c.txt and `c.txt.bak` are not named\n");
|
|
275
275
|
const files = () => (runGit(st, "r1") as any).files as string[];
|
|
276
276
|
expect(files()).toEqual(["?? .gitignore", "?? a.txt"]);
|
|
277
|
+
// The card's mark: the run with changed files is pending; one with none and no record says nothing.
|
|
278
|
+
expect(cardsGit(st, ["r1", "nope"])).toEqual({ r1: "pending" });
|
|
277
279
|
// An earlier run that named the same file is not offered it: the later report has it.
|
|
278
280
|
mkdirSync(join(st, "runs", "r0"));
|
|
279
281
|
writeFileSync(join(st, "runs", "r0", "03-impl.md"), "- `a.txt`: z\n");
|
|
@@ -303,6 +305,7 @@ test("commitPush: commits the run's files only, pushes, and records it; the mess
|
|
|
303
305
|
expect(sh(proj, "show", "--name-only", "--format=", "HEAD")).toBe(".gitignore\na.txt");
|
|
304
306
|
expect(Bun.spawnSync(["git", "-C", proj, "status", "--porcelain"]).stdout.toString()).toBe("A c.txt\n");
|
|
305
307
|
expect(readCommit(st, "r1")).toEqual({ sha: r.sha, pushed: true });
|
|
308
|
+
expect(cardsGit(st, ["r1", "r0", "nope"])).toEqual({ r1: "pushed", r0: "pushed" }); // r0: no record, but its file is in; c.txt still changed is not its business
|
|
306
309
|
|
|
307
310
|
// A push that fails leaves a record that says so.
|
|
308
311
|
sh(proj, "remote", "set-url", "origin", join(root, "gone.git"));
|
|
@@ -318,6 +321,8 @@ test("commitPush: commits the run's files only, pushes, and records it; the mess
|
|
|
318
321
|
expect(readCommit(st, "r1")!.pushed).toBe(false);
|
|
319
322
|
expect(sh(proj, "show", "--name-only", "--format=", "HEAD")).toBe("b*.txt");
|
|
320
323
|
expect(runGit(st, "r1")).toMatchObject({ files: [], ahead: 1, others: 2 });
|
|
324
|
+
expect(cardsGit(st, ["r1"])).toEqual({ r1: "pending" }); // committed, not pushed
|
|
325
|
+
expect(cardsGit(join(root, ".kaizen"), ["r1"])).toEqual({}); // not a repo
|
|
321
326
|
});
|
|
322
327
|
|
|
323
328
|
test.skipIf(process.platform === "win32")("findTerminal: a running herdr wins, then tmux, else neither; the prompt stays one literal argument", () => {
|
package/cli/state.ts
CHANGED
|
@@ -918,6 +918,17 @@ function runReport(state: string, id: string) {
|
|
|
918
918
|
}
|
|
919
919
|
const names = (report: string, path: string) => report.includes("`" + path + "`") || report.includes("`" + path + ":");
|
|
920
920
|
|
|
921
|
+
type Report = { id: string; text: string; at: number };
|
|
922
|
+
function runReports(state: string): Report[] {
|
|
923
|
+
let ids: string[] = [];
|
|
924
|
+
try { ids = readdirSync(join(state, "runs")); } catch {}
|
|
925
|
+
return ids.map((id) => ({ id, ...runReport(state, id) }));
|
|
926
|
+
}
|
|
927
|
+
const runFiles = (changed: string[], mine: Report, rest: Report[]) => changed.filter((l) => {
|
|
928
|
+
const p = statusPath(l);
|
|
929
|
+
return names(mine.text, p) && !rest.some((r) => r.at > mine.at && names(r.text, p));
|
|
930
|
+
});
|
|
931
|
+
|
|
921
932
|
// The project's status narrowed to one run: a changed file is the run's when its
|
|
922
933
|
// report names the path in backticks and no run that reported later names it too,
|
|
923
934
|
// so a finished run is not offered the work of the ones after it. `others` counts
|
|
@@ -929,14 +940,9 @@ const names = (report: string, path: string) => report.includes("`" + path + "`"
|
|
|
929
940
|
export function runGit(state: string, id: string): GitStatus {
|
|
930
941
|
const dir = dirname(state), s = gitStatus(dir);
|
|
931
942
|
if (!s.repo) return s;
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
const rest = ids.filter((o) => o !== id).map((o) => ({ id: o, ...runReport(state, o) }));
|
|
936
|
-
const files = s.files.filter((l) => {
|
|
937
|
-
const p = statusPath(l);
|
|
938
|
-
return names(mine.text, p) && !rest.some((r) => r.at > mine.at && names(r.text, p));
|
|
939
|
-
});
|
|
943
|
+
const all = runReports(state), mine = all.find((r) => r.id === id) ?? { id, text: "", at: 0 };
|
|
944
|
+
const rest = all.filter((o) => o.id !== id);
|
|
945
|
+
const files = runFiles(s.files, mine, rest);
|
|
940
946
|
const shared = files.flatMap((l) => {
|
|
941
947
|
const p = statusPath(l);
|
|
942
948
|
const since = Number(git(dir, "--literal-pathspecs", "log", "-1", "--format=%ct", "--", p).out) * 1000;
|
|
@@ -955,6 +961,24 @@ export function readCommit(state: string, id: string): { sha: string; pushed: bo
|
|
|
955
961
|
} catch { return null; }
|
|
956
962
|
}
|
|
957
963
|
|
|
964
|
+
// The card's git mark for each run asked about, by the rules of the panel's button
|
|
965
|
+
// (`gitBtn` in web/board.html): one project status and one read of the reports for
|
|
966
|
+
// the lot. A run with nothing to say either way gets no entry.
|
|
967
|
+
export function cardsGit(state: string, ids: string[]): Record<string, "pending" | "pushed"> {
|
|
968
|
+
const s = gitStatus(dirname(state)), out: Record<string, "pending" | "pushed"> = {};
|
|
969
|
+
if (!s.repo) return out;
|
|
970
|
+
const all = runReports(state);
|
|
971
|
+
for (const id of ids) {
|
|
972
|
+
const mine = all.find((r) => r.id === id) ?? { id, text: "", at: 0 }, k = readCommit(state, id);
|
|
973
|
+
const files = runFiles(s.files, mine, all.filter((o) => o.id !== id)).length;
|
|
974
|
+
if (files) out[id] = "pending";
|
|
975
|
+
else if (k?.pushed) out[id] = "pushed";
|
|
976
|
+
else if (s.ahead > 0) out[id] = "pending";
|
|
977
|
+
else if (k || mine.text) out[id] = "pushed";
|
|
978
|
+
}
|
|
979
|
+
return out;
|
|
980
|
+
}
|
|
981
|
+
|
|
958
982
|
// A Conventional Commits subject: every commit the board makes says what kind it is.
|
|
959
983
|
const COMMIT_TYPES = ["feat", "fix", "refactor", "perf", "docs", "test", "build", "ci", "chore", "style", "revert"];
|
|
960
984
|
const COMMIT_TYPE = new RegExp(`^(${COMMIT_TYPES.join("|")})(\\([^)\\n]+\\))?!?: \\S`);
|
package/cli/web.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
import { existsSync, readFileSync, readdirSync, statSync, watch, appendFileSync, mkdirSync, type FSWatcher } from "node:fs";
|
|
4
4
|
import { join, dirname } from "node:path";
|
|
5
5
|
import {
|
|
6
|
-
home, type Item, boardCards, knownProjects, remember, locate, label, tilde, searchRoots, findProjects,
|
|
6
|
+
home, type Item, type Card, boardCards, knownProjects, remember, locate, label, tilde, searchRoots, findProjects,
|
|
7
7
|
readInbox, writeInbox, replaceIdea, abandonRun, launchRun, parseItem, short, setArchived, writeNotes,
|
|
8
|
-
appendNote, saveAttachment, pidOnPort, runGit, readCommit, commitPush, notice, notifier, LOGO,
|
|
8
|
+
appendNote, saveAttachment, pidOnPort, runGit, cardsGit, readCommit, commitPush, notice, notifier, LOGO,
|
|
9
9
|
reviewFindings, pickFindings, fixPrompt,
|
|
10
10
|
} from "./state.ts";
|
|
11
11
|
import { readSources, addSource, removeSource, moveSource, gatherSource, gatherAll, gatherDirs, due } from "./sources.ts";
|
|
@@ -197,11 +197,17 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
197
197
|
// Before the project list: it registers folders that turn out to be set up.
|
|
198
198
|
const unset = await fresh();
|
|
199
199
|
const dirs = states(all);
|
|
200
|
+
const cards = boardCards(dirs, Date.now());
|
|
201
|
+
// The git mark, for the cards whose panel offers commit and push: one status per project.
|
|
202
|
+
const marks = new Map<string, Record<string, string>>();
|
|
203
|
+
const markable = (c: Card) => c.kind === "run" && !c.archived && (c.status === "done" || c.awaiting === "approvals.review") && c.state !== join(home, ".kaizen");
|
|
204
|
+
for (const d of new Set(cards.filter(markable).map((c) => c.state)))
|
|
205
|
+
marks.set(d, cardsGit(d, cards.filter((c) => markable(c) && c.state === d).map((c) => c.id!)));
|
|
200
206
|
return json({
|
|
201
207
|
project, all, dir: state,
|
|
202
208
|
projects: states(true).map((d) => ({ dir: d, label: label(d) })),
|
|
203
209
|
fresh: unset.map((d) => ({ dir: join(d, ".kaizen"), label: tilde(d) })),
|
|
204
|
-
cards:
|
|
210
|
+
cards: cards.map((c) => ({ ...c, notice: notice(c), git: markable(c) ? marks.get(c.state)?.[c.id!] : undefined })),
|
|
205
211
|
now: Date.now(),
|
|
206
212
|
});
|
|
207
213
|
}
|
package/package.json
CHANGED
package/web/board.html
CHANGED
|
@@ -106,6 +106,7 @@ section h2 .clear{margin-left:auto;min-height:22px;padding:0 8px}
|
|
|
106
106
|
.chip svg{width:11px;height:11px;stroke-width:1.7}
|
|
107
107
|
.chip.warn{color:var(--warn);background:color-mix(in srgb,var(--warn) 14%,transparent)}.chip.warn i{background:var(--warn)}
|
|
108
108
|
.chip.accent{color:var(--accent);background:color-mix(in srgb,var(--accent) 14%,transparent)}.chip.accent i{background:var(--accent)}
|
|
109
|
+
.chip.ok{color:color-mix(in srgb,var(--ok) 80%,var(--ink));background:color-mix(in srgb,var(--ok) 14%,transparent)}
|
|
109
110
|
.chip.run{color:var(--run);background:color-mix(in srgb,var(--run) 14%,transparent)}.chip.run i{background:var(--run)}
|
|
110
111
|
.st.d-idea{color:var(--dim)}.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)}
|
|
111
112
|
.d-run{color:var(--run)}.d-wait{color:var(--warn)}.d-done{color:var(--ok)}
|
|
@@ -224,6 +225,7 @@ aside .foot:empty{display:none}
|
|
|
224
225
|
<symbol id="c-cursor" viewBox="0 0 16 16"><path d="M4 3l9 5-4 1-1 4z"/></symbol>
|
|
225
226
|
<symbol id="c-full" viewBox="0 0 16 16"><circle cx="5.5" cy="8" r="3"/><circle cx="11" cy="8" r="3"/></symbol>
|
|
226
227
|
<symbol id="c-lite" viewBox="0 0 16 16"><circle cx="8" cy="8" r="3"/></symbol>
|
|
228
|
+
<symbol id="c-git" viewBox="0 0 16 16"><circle cx="4.5" cy="3.5" r="1.5"/><circle cx="4.5" cy="12.5" r="1.5"/><circle cx="11.5" cy="5.5" r="1.5"/><path d="M4.5 5v6M11.5 7c0 2.5-7 1.5-7 4"/></symbol>
|
|
227
229
|
</defs></svg>
|
|
228
230
|
|
|
229
231
|
<div class="hdr">
|
|
@@ -387,6 +389,8 @@ function card(c){
|
|
|
387
389
|
if(all)chips.push(`<span class="chip">${esc(c.where)}</span>`);
|
|
388
390
|
chips.push(...(c.tags||[]).map(t=>{const mark=MARKS[t]?`<svg><use href="#c-${t}"/></svg>`:"";return `<span class="chip${t==="full"||t==="lite"?" accent":""}">${mark}${esc(NAMES[t]||t)}</span>`}));
|
|
389
391
|
if(c.notes)chips.push(`<span class="chip">notes</span>`);
|
|
392
|
+
// Information only: the panel's button is where it is committed and pushed.
|
|
393
|
+
if(c.git){const t=c.git==="pushed"?"Committed & pushed":"Not committed & pushed yet";chips.push(`<span class="chip ${c.git==="pushed"?"ok":"warn"}" role="img" title="${t}" aria-label="${t}"><svg><use href="#c-git"/></svg></span>`)}
|
|
390
394
|
const pick=picking&&!archive&&c.column===4;
|
|
391
395
|
el.innerHTML=(pick?`<input type="checkbox" class="pick" ${picked.has(k)?"checked":""} aria-label="Select">`:"")+`<div class="c-id"><span class="st d-${st}"><svg><use href="#s-${st}"/></svg></span><span class="idt">${esc(idt)}</span><span class="age">${c.kind==="run"?age(c.moved||S.now,S.now):""}</span><button class="ib sm more" data-a="menu" title="Actions" aria-label="Actions">${ico("more")}</button></div>
|
|
392
396
|
<div class="c-title">${esc(c.kind==="idea"?c.text:(c.title||c.id))}</div>
|
|
@@ -563,9 +567,9 @@ function gitBtn(c){const g=runDoc.git,k=runDoc.commit;
|
|
|
563
567
|
if(k&&k.pushed)return `<button class="btn" disabled title="${esc(k.sha)}">Committed & pushed</button>`+(g.files.length?`<button class="btn" data-a="commit">Commit new changes</button>`:g.ahead>0?`<button class="btn" data-a="push">Push</button>`:"");
|
|
564
568
|
if(g.files.length)return `<button class="btn pri" data-a="commit">Commit & push</button>`;
|
|
565
569
|
if(g.ahead>0)return `<button class="btn pri" data-a="push">Push</button>`;
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
return k
|
|
570
|
+
// None of its files changed and level with the remote: it was committed and pushed
|
|
571
|
+
// somewhere else. With no record and no report, nothing says it was ever committed.
|
|
572
|
+
return k||runDoc.impl?`<button class="btn" disabled>Committed & pushed</button>`:""}
|
|
569
573
|
function confirmCommit(c,msg){const g=runDoc.git,foot=$("pfoot");
|
|
570
574
|
foot.innerHTML=`<div class="inl" style="flex:1"><div class="q">Commit ${g.files.length} changed file${g.files.length===1?"":"s"} on ${esc(g.branch||"this branch")} and push to ${esc(g.remote)}?</div><div class="q dim">Only the files this run's report names.${g.others?` ${g.others} other changed file${g.others===1?" is":"s are"} left alone.`:""}</div><pre class="files">${esc(g.files.join("\n"))}</pre>${g.shared&&g.shared.length?`<div class="q">These files also hold other runs' changes, and a file is committed whole:</div><pre class="files">${esc(g.shared.join("\n"))}</pre>`:""}<textarea aria-label="Commit message" placeholder="feat(scope): what changed">${esc(msg||commitMsg(runDoc.impl)||c.title||c.text)}</textarea><div class="q dim" id="clong" hidden>The first line is over 72 characters; logs will cut it short.</div><div class="row"><button class="btn ghost" data-a="c">Cancel</button><button class="btn pri" data-a="s">Commit & push</button></div></div>`;
|
|
571
575
|
const inp=foot.querySelector("textarea"),go=foot.querySelector("[data-a=s]");inp.focus();
|