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 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 2>&1 \
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 mine = runReport(state, id);
933
- let ids: string[] = [];
934
- try { ids = readdirSync(join(state, "runs")); } catch {}
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: boardCards(dirs, Date.now()).map((c) => ({ ...c, notice: notice(c) })),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kaizen-agent",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "Staged workflow for AI agents — code, writing, research, operations: plan, human approval, build, independent review, bounded fix loop.",
5
5
  "type": "module",
6
6
  "bin": {
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 &amp; 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 &amp; push</button>`;
565
569
  if(g.ahead>0)return `<button class="btn pri" data-a="push">Push</button>`;
566
- // Clean and level with the remote: it was committed and pushed somewhere else. With
567
- // other files still changed and no record, nothing says this run was ever committed.
568
- return k||!g.others?`<button class="btn" disabled>Committed &amp; pushed</button>`:""}
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 &amp; 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 &amp; push</button></div></div>`;
571
575
  const inp=foot.querySelector("textarea"),go=foot.querySelector("[data-a=s]");inp.focus();