ikanban 0.5.0 → 0.5.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/README.md CHANGED
@@ -31,7 +31,8 @@ exits 1 when the Roadmap differs. With no file it syncs every tracked plan file.
31
31
 
32
32
  `ikanban hooks install` adds a git `pre-push` hook (any git host, never blocks a push) and an
33
33
  agent hook for Claude Code, Gemini CLI and Copilot CLI (`.iKanban/hooks/roadmap-sync.js`).
34
- No CI is needed.
34
+ No CI is needed. `--trace` also wires the kit's trace hook (`.iKanban/hooks/trace.js`) for the
35
+ same agents; tracing stays off until `node .iKanban/hooks/trace.js --on`.
35
36
 
36
37
  ## Infrastructure scans
37
38
 
@@ -3,7 +3,8 @@
3
3
  // after each file write (Claude Code PostToolUse, Gemini CLI AfterTool, Copilot
4
4
  // CLI postToolUse; `npx ikanban hooks install` wires them). It reads the hook's
5
5
  // JSON on stdin, and only when the written file is a plan file (a JSON file with
6
- // a top-level "roadmap" block) it runs `ikanban roadmap sync <file>`.
6
+ // a top-level "roadmap" block) it runs `ikanban roadmap sync <file>`. A written
7
+ // <plan>.graph.json (the plan's diagram) syncs <plan>.json beside it.
7
8
  // It never blocks the agent: it always exits 0 and prints "{}" (the hook answer
8
9
  // every agent accepts); messages go to stderr.
9
10
  const fs = require("node:fs");
@@ -71,7 +72,9 @@ function main(input) {
71
72
  const root = repoRoot(cwd);
72
73
  if (!root) return;
73
74
  const args = event.tool_input || event.toolArgs || event.tool_args || {};
74
- const plans = [...new Set(filePaths(args).map((p) => path.resolve(cwd, p)))].filter((f) => isPlan(f, root));
75
+ // IKA-2870 (plan 45 P7): a written <plan>.graph.json syncs <plan>.json beside it, which sends it.
76
+ const written = filePaths(args).map((p) => path.resolve(cwd, p).replace(/\.graph\.json$/, ".json"));
77
+ const plans = [...new Set(written)].filter((f) => isPlan(f, root));
75
78
  const [cmd, pre] = cliCommand();
76
79
  for (const plan of plans) {
77
80
  // An argument array and no shell: nothing from the hook input or the plan is
package/hooks/trace.js CHANGED
@@ -294,6 +294,23 @@ async function record(cfg, cwd, session, e) {
294
294
  const MAX_PLAN = 256 * 1024;
295
295
  const planState = (key) => path.join(os.homedir(), ".ikanban", "plans", `${key}.sha256`);
296
296
 
297
+ /** IKA-2854 (plan 45): the plan's diagram at docs/plans/<KEY>.graph.json, sent with the plan. Only a
298
+ * regular file inside the repository, at most 256 KB, holding a JSON object; anything else is no
299
+ * diagram (the server and the browser check it again). Returns [text, object] or []. */
300
+ function readGraph(root, key) {
301
+ const file = path.join(root, "docs", "plans", `${key}.graph.json`);
302
+ try {
303
+ const st = fs.lstatSync(file);
304
+ if (!st.isFile() || st.size > MAX_PLAN) return [];
305
+ if (!fs.realpathSync(file).startsWith(fs.realpathSync(root) + path.sep)) return [];
306
+ const text = fs.readFileSync(file, "utf8");
307
+ const doc = JSON.parse(text);
308
+ return doc && typeof doc === "object" && !Array.isArray(doc) ? [text, doc] : [];
309
+ } catch {
310
+ return [];
311
+ }
312
+ }
313
+
297
314
  /** IKA-2741 (plan 7 P6-T6): the task's plan, checked with the kit's own checker, goes to the Plan
298
315
  * stage in Pipelines ("checked on laptop"). Only a regular file at docs/plans/<KEY>.json, and only
299
316
  * when it changed since the last send. It is never a merge verdict: only the runner gives one. */
@@ -310,7 +327,9 @@ async function sendPlan(cfg, cwd, team, agent) {
310
327
  }
311
328
  if (!st.isFile() || st.size > MAX_PLAN) return;
312
329
  const text = fs.readFileSync(file, "utf8");
313
- const sha = require("node:crypto").createHash("sha256").update(text).digest("hex");
330
+ const [graphText = "", graph] = readGraph(root, key);
331
+ // The diagram is in the checksum, so a changed diagram alone is sent too.
332
+ const sha = require("node:crypto").createHash("sha256").update(text).update("\0").update(graphText).digest("hex");
314
333
  try {
315
334
  if (fs.readFileSync(planState(key), "utf8") === sha) return;
316
335
  } catch {
@@ -327,7 +346,7 @@ async function sendPlan(cfg, cwd, team, agent) {
327
346
  const first = errors[0];
328
347
  const detail = first ? `${first.where}: ${first.problem}`.replace(/\s+/g, " ").slice(0, 300) : undefined;
329
348
  await api(cfg, `/api/teams/${team}/laptop-plans`, { task: key, agent, status: errors.length ? "failed" : "passed", detail, plan: plan.phases,
330
- decisions: Array.isArray(plan.decisions) ? plan.decisions : undefined });
349
+ decisions: Array.isArray(plan.decisions) ? plan.decisions : undefined, graph });
331
350
  fs.mkdirSync(path.dirname(planState(key)), { recursive: true, mode: 0o700 });
332
351
  fs.writeFileSync(planState(key), sha, { mode: 0o600 });
333
352
  }
package/lib/plancheck.js CHANGED
@@ -155,12 +155,22 @@ async function serverGaps(file, plan, opts, stages = ["before_done"]) {
155
155
  const { connection } = require("./roadmap.js");
156
156
  const { ikanbanFetch } = safe();
157
157
  const c = connection({ token: opts.token, "base-url": opts["base-url"] }, file);
158
- const team = opts.team || c.team || (plan.roadmap && plan.roadmap.team) || "";
158
+ // IKA-2839: --team, then the plan's own team, then the settings team (another team's plan
159
+ // was read from the settings team and refused with a 404).
160
+ let team = String(opts.team || (plan.roadmap && plan.roadmap.team) || c.team || "");
159
161
  const task = String(opts.task);
160
162
  if (!c.token) return [{ where: task, problem: "no API key to read its critic gaps", fix: "set VIBE_API_TOKEN, pass --token, or set api_key in .iKanban/settings-local.json" }];
161
- if (!/^[0-9a-f-]{36}$/i.test(team)) return [{ where: task, problem: `team "${team}" is not a team id`, fix: "pass --team <team id> or set team in .iKanban/settings-local.json" }];
162
163
  let res;
163
164
  try {
165
+ // The route takes only a team id: an identifier such as "TUT" is looked up among the key's teams.
166
+ if (team && !/^[0-9a-f-]{36}$/i.test(team)) {
167
+ const t = await ikanbanFetch("/api/teams", { base: c.base, token: c.token });
168
+ const body = await t.json().catch(() => ({}));
169
+ const hit = t.ok && Array.isArray(body.data) && body.data.find((x) => String(x.identifier || "").toLowerCase() === team.toLowerCase());
170
+ if (!hit) return [{ where: task, problem: `team "${team}" not found among your teams${t.ok ? "" : ` (HTTP ${t.status})`}`, fix: "set roadmap.team to a team identifier or id the key's account is in, or pass --team <team id>" }];
171
+ team = hit.id;
172
+ }
173
+ if (!/^[0-9a-f-]{36}$/i.test(team)) return [{ where: task, problem: `team "${team}" is not a team id`, fix: "pass --team <team id> or set team in .iKanban/settings-local.json" }];
164
174
  res = await ikanbanFetch(`/api/teams/${team}/tasks/${encodeURIComponent(task)}/critic-reviews`, { base: c.base, token: c.token });
165
175
  } catch (e) {
166
176
  return [{ where: task, problem: `critic gaps not read: ${e.message}`, fix: "check the network and the API address, then run the check again" }];
package/lib/roadmap.js CHANGED
@@ -74,6 +74,17 @@ function toDocument(plan) {
74
74
  };
75
75
  }
76
76
 
77
+ /** IKA-2870 (plan 45 P7): <name>.graph.json beside the plan, one view per phase. Only a regular file
78
+ * of at most 256 KB holding a JSON object; else no diagram, never a failed sync (the server checks). */
79
+ function readGraph(file) {
80
+ const g = file.replace(/\.json$/i, "") + ".graph.json";
81
+ try {
82
+ const st = fs.lstatSync(g);
83
+ const doc = st.isFile() && st.size <= 256 * 1024 ? JSON.parse(fs.readFileSync(g, "utf8")) : null;
84
+ return doc && typeof doc === "object" && !Array.isArray(doc) ? doc : undefined;
85
+ } catch { return undefined; }
86
+ }
87
+
77
88
  /** .iKanban/settings-local.json (or the older .i-kanban/) nearest to `dir`. */
78
89
  function localSettings(dir) {
79
90
  for (let d = path.resolve(dir); ; d = path.dirname(d)) {
@@ -125,6 +136,7 @@ async function syncFile(file, opts) {
125
136
  const doc = toDocument(plan);
126
137
  doc.project = doc.project || c.project;
127
138
  doc.dry_run = Boolean(opts.check || opts["dry-run"]);
139
+ doc.graph = readGraph(file) ?? null; // null clears a diagram the file no longer has
128
140
  // Deleting an empty phase the sync created needs an explicit flag; MCP never deletes.
129
141
  if (opts["allow-delete"]) doc.allow_delete = true;
130
142
  let res;
@@ -199,6 +211,49 @@ function addHook(settings, event, entry, script = "roadmap-sync.js") {
199
211
  return settings;
200
212
  }
201
213
 
214
+ // Plan 43 P4-T2: `--trace` also wires the trace hook, exactly as .iKanban/README.md shows it.
215
+ // Wiring only: the hook does nothing until `node .iKanban/hooks/trace.js --on`.
216
+ const TRACE = "node .iKanban/hooks/trace.js";
217
+ const CLAUDE_TRACE = 'node "$CLAUDE_PROJECT_DIR/.iKanban/hooks/trace.js"';
218
+ const TRACE_AGENTS = {
219
+ claude: (root) => {
220
+ mergeJson(path.join(root, ".claude", "settings.json"), (s) => {
221
+ addHook(s, "PostToolUse", { hooks: [{ type: "command", command: CLAUDE_TRACE, async: true }] }, "trace.js");
222
+ addHook(s, "PostToolUseFailure", { hooks: [{ type: "command", command: CLAUDE_TRACE, async: true }] }, "trace.js");
223
+ // Stop in the foreground: a background Stop hook is stopped when Claude Code exits (README).
224
+ return addHook(s, "Stop", { hooks: [{ type: "command", command: CLAUDE_TRACE }] }, "trace.js");
225
+ });
226
+ return ".claude/settings.json trace (PostToolUse, PostToolUseFailure, Stop)";
227
+ },
228
+ gemini: (root) => {
229
+ mergeJson(path.join(root, ".gemini", "settings.json"), (s) => {
230
+ addHook(s, "AfterTool", { matcher: "*", hooks: [{ name: "ikanban-trace", type: "command", command: TRACE, timeout: 10000 }] }, "trace.js");
231
+ return addHook(s, "AfterAgent", { hooks: [{ name: "ikanban-trace", type: "command", command: TRACE, timeout: 10000 }] }, "trace.js");
232
+ });
233
+ return ".gemini/settings.json trace (AfterTool, AfterAgent)";
234
+ },
235
+ copilot: (root) => {
236
+ mergeJson(path.join(root, ".github", "hooks", "ikanban-trace.json"), (s) => {
237
+ s.version = 1;
238
+ for (const event of ["postToolUse", "postToolUseFailure", "agentStop"]) {
239
+ addHook(s, event, { type: "command", bash: `${TRACE} copilot:${event}`, cwd: ".", timeoutSec: 10 }, "trace.js");
240
+ }
241
+ return s;
242
+ });
243
+ return ".github/hooks/ikanban-trace.json (postToolUse, postToolUseFailure, agentStop)";
244
+ },
245
+ agy: (root) => {
246
+ mergeJson(path.join(root, ".agents", "hooks.json"), (s) => {
247
+ s["ikanban-trace"] = {
248
+ PostToolUse: [{ matcher: ".*", hooks: [{ type: "command", command: "node ../.iKanban/hooks/trace.js agy:PostToolUse", timeout: 10 }] }],
249
+ Stop: [{ type: "command", command: "node ../.iKanban/hooks/trace.js agy:Stop", timeout: 10 }],
250
+ };
251
+ return s;
252
+ });
253
+ return ".agents/hooks.json trace (PostToolUse, Stop)";
254
+ },
255
+ };
256
+
202
257
  const AGENTS = {
203
258
  claude: (root) => {
204
259
  mergeJson(path.join(root, ".claude", "settings.json"), (s) => {
@@ -251,7 +306,7 @@ function installHooks(opts) {
251
306
  const script = path.join(root, ".iKanban", "hooks", "roadmap-sync.js");
252
307
  fs.mkdirSync(path.dirname(script), { recursive: true });
253
308
  // The hook runs its commands through ikanban-safe.js, so both files go together.
254
- for (const name of ["roadmap-sync.js", "deploy-check.js", "ikanban-safe.js", "plan-gate.js"]) {
309
+ for (const name of ["roadmap-sync.js", "deploy-check.js", "ikanban-safe.js", "plan-gate.js", ...(opts.trace ? ["trace.js"] : [])]) {
255
310
  fs.copyFileSync(path.join(__dirname, "..", "hooks", name), path.join(path.dirname(script), name));
256
311
  done.push(`.iKanban/hooks/${name}`);
257
312
  }
@@ -276,11 +331,14 @@ function installHooks(opts) {
276
331
  for (const a of wanted) {
277
332
  if (!AGENTS[a]) throw new Error(`unknown agent "${a}" (claude, gemini, copilot, agy)`);
278
333
  done.push(AGENTS[a](root));
334
+ if (opts.trace) done.push(TRACE_AGENTS[a](root));
279
335
  }
280
336
  for (const d of done) console.log(` installed: ${d}`);
281
337
  console.log("Plan files (JSON with a top-level \"roadmap\" block) now reach the Roadmap on every agent write and every git push.");
282
338
  console.log("Before an edit or a writing shell command, the plan gate needs the task's plan, docs/plans/<TASK-KEY>.json (the key from the branch name), to pass: node .iKanban/hooks/plan-gate.js check docs/plans/<TASK-KEY>.json. How to write one: .iKanban/prompts/feature-plan.md.");
283
339
  console.log("After a deploy, the deploy check reads your site's build marker; name your deploy commands in .iKanban/settings-local.json \"deploy_check\" (README).");
340
+ if (opts.trace && wanted.length) console.log("The trace hook is wired but still off. Turn it on in this repository with: node .iKanban/hooks/trace.js --on");
341
+ else if (opts.trace) console.log("Trace hook not wired: no agent folder found (looked for .claude, .gemini, .github, .agents). Create the agent's folder, or name it: ikanban hooks install --trace --agent claude");
284
342
  // IKA-2244: the sync's default server is prod; say which one this repository will use.
285
343
  const base = connection({}, path.join(root, "plan.json")).base;
286
344
  console.log(` server: ${base} (set api_url in .iKanban/settings-local.json, or IKANBAN_BASE_URL, to use another)`);
@@ -292,9 +350,11 @@ const HELP = `Usage:
292
350
  Make each plan's Roadmap match the file. No file: every tracked JSON file
293
351
  with a top-level "roadmap" block. --check: change nothing, exit 1 on drift.
294
352
  --allow-delete: also delete an emptied phase the sync created (else it stays).
295
- ikanban hooks install [--agent claude,gemini,copilot]
353
+ ikanban hooks install [--agent claude,gemini,copilot,agy] [--trace]
296
354
  Keep the Roadmap in step without CI: a git pre-push hook (any git host) and
297
- a hook for each agent whose folder exists (.claude, .gemini, .github).
355
+ a hook for each agent whose folder exists (.claude, .gemini, .github, .agents).
356
+ --trace: also wire .iKanban/hooks/trace.js for those agents. Tracing stays
357
+ off until: node .iKanban/hooks/trace.js --on
298
358
 
299
359
  Key: VIBE_API_TOKEN, --token, or api_key in .iKanban/settings-local.json.
300
360
  Server: IKANBAN_BASE_URL, --base-url, or api_url there (default https://api.i-kanban.com).`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ikanban",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "iKanban CLI: AI and security scans of your own repo; findings filed into iKanban, reports kept encrypted on your machine",
5
5
  "bin": { "ikanban": "bin/ikanban.js" },
6
6
  "files": ["bin", "lib", "hooks", "scanners", "README.md", "LICENSE.md"],