kandown 0.2.2 → 0.3.0

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/bin/tui.js CHANGED
@@ -26207,7 +26207,7 @@ var require_websocket = __commonJS({
26207
26207
  var http = __require("http");
26208
26208
  var net = __require("net");
26209
26209
  var tls = __require("tls");
26210
- var { randomBytes, createHash } = __require("crypto");
26210
+ var { randomBytes, createHash: createHash2 } = __require("crypto");
26211
26211
  var { Duplex, Readable: Readable2 } = __require("stream");
26212
26212
  var { URL: URL2 } = __require("url");
26213
26213
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -26867,7 +26867,7 @@ var require_websocket = __commonJS({
26867
26867
  abortHandshake(websocket, socket, "Invalid Upgrade header");
26868
26868
  return;
26869
26869
  }
26870
- const digest = createHash("sha1").update(key + GUID).digest("base64");
26870
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
26871
26871
  if (res.headers["sec-websocket-accept"] !== digest) {
26872
26872
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
26873
26873
  return;
@@ -27234,7 +27234,7 @@ var require_websocket_server = __commonJS({
27234
27234
  var EventEmitter4 = __require("events");
27235
27235
  var http = __require("http");
27236
27236
  var { Duplex } = __require("stream");
27237
- var { createHash } = __require("crypto");
27237
+ var { createHash: createHash2 } = __require("crypto");
27238
27238
  var extension2 = require_extension();
27239
27239
  var PerMessageDeflate2 = require_permessage_deflate();
27240
27240
  var subprotocol2 = require_subprotocol();
@@ -27535,7 +27535,7 @@ var require_websocket_server = __commonJS({
27535
27535
  );
27536
27536
  }
27537
27537
  if (this._state > RUNNING) return abortHandshake(socket, 503);
27538
- const digest = createHash("sha1").update(key + GUID).digest("base64");
27538
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
27539
27539
  const headers = [
27540
27540
  "HTTP/1.1 101 Switching Protocols",
27541
27541
  "Upgrade: websocket",
@@ -54328,6 +54328,267 @@ function ValueDisplay({ setting, value, focused }) {
54328
54328
  // src/cli/screens/board.tsx
54329
54329
  var import_react36 = __toESM(require_react(), 1);
54330
54330
 
54331
+ // src/cli/lib/board-reader.ts
54332
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
54333
+ import { dirname, join as join2 } from "path";
54334
+
54335
+ // src/lib/types.ts
54336
+ var DEFAULT_COLUMNS = ["Backlog", "Todo", "In Progress", "Review", "Done"];
54337
+
54338
+ // src/lib/parser.ts
54339
+ function parseSimpleYaml(yaml) {
54340
+ const obj = {};
54341
+ if (!yaml || typeof yaml !== "string") return obj;
54342
+ const lines = yaml.split("\n");
54343
+ for (let i = 0; i < lines.length; i++) {
54344
+ const line = lines[i] ?? "";
54345
+ const m = line.match(/^([a-zA-Z_][\w-]*)\s*:\s*(.*)$/);
54346
+ if (!m) continue;
54347
+ const key = m[1];
54348
+ if (!key) continue;
54349
+ let val = m[2]?.trim() ?? "";
54350
+ if (val === "|") {
54351
+ const block = [];
54352
+ i++;
54353
+ while (i < lines.length && (/^\s+/.test(lines[i] ?? "") || (lines[i] ?? "") === "")) {
54354
+ block.push((lines[i] ?? "").replace(/^ /, ""));
54355
+ i++;
54356
+ }
54357
+ i--;
54358
+ obj[key] = block.join("\n").trimEnd();
54359
+ continue;
54360
+ }
54361
+ if (typeof val !== "string") val = "";
54362
+ if (val.startsWith("[") && val.endsWith("]")) {
54363
+ const arr = val.slice(1, -1).split(",").map((s) => s && typeof s === "string" ? s.trim().replace(/^["']|["']$/g, "") : "").filter(Boolean);
54364
+ obj[key] = arr;
54365
+ } else {
54366
+ obj[key] = typeof val === "string" ? val.replace(/^["']|["']$/g, "") : val;
54367
+ }
54368
+ }
54369
+ return obj;
54370
+ }
54371
+ function parseTaskFile(md) {
54372
+ if (!md || typeof md !== "string") {
54373
+ return { frontmatter: { id: "", title: "" }, body: "" };
54374
+ }
54375
+ const lines = md.split("\n");
54376
+ if (lines[0] && lines[0].trim() === "---") {
54377
+ const fmLines = [];
54378
+ let i = 1;
54379
+ while (i < lines.length && lines[i].trim() !== "---") {
54380
+ fmLines.push(lines[i]);
54381
+ i++;
54382
+ }
54383
+ const body = lines.slice(i + 1).join("\n").trimStart();
54384
+ const fm = parseSimpleYaml(fmLines.join("\n"));
54385
+ return { frontmatter: fm, body };
54386
+ }
54387
+ return { frontmatter: { id: "", title: "" }, body: md };
54388
+ }
54389
+ function normalizeStatus(status) {
54390
+ const value = typeof status === "string" ? status.trim() : "";
54391
+ return value || "Backlog";
54392
+ }
54393
+ function normalizePriority(priority) {
54394
+ if (typeof priority !== "string") return null;
54395
+ const value = priority.toUpperCase();
54396
+ return /^(P1|P2|P3|P4)$/.test(value) ? value : null;
54397
+ }
54398
+ function normalizeOwnerType(ownerType) {
54399
+ if (typeof ownerType !== "string") return "";
54400
+ const value = ownerType.toLowerCase();
54401
+ return value === "human" || value === "ai" ? value : "";
54402
+ }
54403
+ function taskOrder(task) {
54404
+ const value = task.frontmatter.order;
54405
+ if (typeof value === "number" && Number.isFinite(value)) return value;
54406
+ if (typeof value === "string" && value.trim()) {
54407
+ const parsed = Number(value);
54408
+ if (Number.isFinite(parsed)) return parsed;
54409
+ }
54410
+ return Number.MAX_SAFE_INTEGER;
54411
+ }
54412
+ function taskToBoardTask(task) {
54413
+ const { frontmatter, body } = task;
54414
+ const { subtasks } = extractSubtasks(body);
54415
+ const done = subtasks.filter((s) => s.done).length;
54416
+ const total = subtasks.length;
54417
+ const status = normalizeStatus(frontmatter.status);
54418
+ const tags = Array.isArray(frontmatter.tags) ? frontmatter.tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0) : [];
54419
+ return {
54420
+ id: frontmatter.id || "",
54421
+ title: frontmatter.title || frontmatter.id || "Untitled task",
54422
+ checked: /done|termin|closed|complet/i.test(status),
54423
+ tags,
54424
+ assignee: typeof frontmatter.assignee === "string" && frontmatter.assignee ? frontmatter.assignee : null,
54425
+ priority: normalizePriority(frontmatter.priority),
54426
+ ownerType: normalizeOwnerType(frontmatter.ownerType),
54427
+ progress: total > 0 ? { done, total } : null
54428
+ };
54429
+ }
54430
+ function buildColumnsFromTasks(tasks, configuredColumns = DEFAULT_COLUMNS) {
54431
+ const columnNames = configuredColumns.length > 0 ? configuredColumns : DEFAULT_COLUMNS;
54432
+ const columnsByName = /* @__PURE__ */ new Map();
54433
+ const configured = columnNames.map((name) => ({ name, tasks: [] }));
54434
+ for (const column of configured) columnsByName.set(column.name.toLowerCase(), column);
54435
+ const unknownColumns = [];
54436
+ const sortedTasks = [...tasks].filter((task) => Boolean(task.frontmatter.id)).sort((a, b) => {
54437
+ const byOrder = taskOrder(a) - taskOrder(b);
54438
+ if (byOrder !== 0) return byOrder;
54439
+ return a.frontmatter.id.localeCompare(b.frontmatter.id, void 0, { numeric: true });
54440
+ });
54441
+ for (const task of sortedTasks) {
54442
+ const status = normalizeStatus(task.frontmatter.status);
54443
+ let column = columnsByName.get(status.toLowerCase());
54444
+ if (!column) {
54445
+ column = { name: status, tasks: [] };
54446
+ columnsByName.set(status.toLowerCase(), column);
54447
+ unknownColumns.push(column);
54448
+ }
54449
+ column.tasks.push(taskToBoardTask(task));
54450
+ }
54451
+ return [...unknownColumns, ...configured];
54452
+ }
54453
+ function extractSubtasks(body) {
54454
+ const subtasks = [];
54455
+ if (!body || typeof body !== "string") return { subtasks, bodyWithoutSubtasks: body ?? "" };
54456
+ const lines = body.split("\n");
54457
+ const kept = [];
54458
+ let inSubtaskSection = false;
54459
+ for (const line of lines) {
54460
+ if (/^#{1,6}\s+(subtasks?|sous[- ]t[âa]ches?|crit[èe]res?)/i.test(line)) {
54461
+ inSubtaskSection = true;
54462
+ kept.push(line);
54463
+ continue;
54464
+ }
54465
+ if (/^#{1,6}\s+/.test(line) && inSubtaskSection) {
54466
+ inSubtaskSection = false;
54467
+ kept.push(line);
54468
+ continue;
54469
+ }
54470
+ const m = line.match(/^\s*-\s+\[([ xX])\]\s+(.+)$/);
54471
+ if (m && inSubtaskSection) {
54472
+ const text = m[2]?.trim() ?? "";
54473
+ subtasks.push({ done: (m[1]?.toLowerCase() ?? "") === "x", text });
54474
+ continue;
54475
+ }
54476
+ const descMatch = line.match(/^\s*\[DESC\]\s*(.*)$/);
54477
+ if (descMatch && subtasks.length > 0) {
54478
+ subtasks[subtasks.length - 1].description = descMatch[1];
54479
+ continue;
54480
+ }
54481
+ const reportMatch = line.match(/^\s*\[REPORT\]\s*(.*)$/);
54482
+ if (reportMatch && subtasks.length > 0) {
54483
+ subtasks[subtasks.length - 1].report = reportMatch[1];
54484
+ continue;
54485
+ }
54486
+ kept.push(line);
54487
+ }
54488
+ return { subtasks, bodyWithoutSubtasks: kept.join("\n") };
54489
+ }
54490
+
54491
+ // src/lib/serializer.ts
54492
+ function serializeTaskFile(frontmatter, body) {
54493
+ const lines = ["---"];
54494
+ if (frontmatter && typeof frontmatter === "object") {
54495
+ for (const [k, v] of Object.entries(frontmatter)) {
54496
+ if (v === null || v === void 0 || v === "") continue;
54497
+ if (Array.isArray(v)) {
54498
+ if (v.length === 0) continue;
54499
+ lines.push(`${k}: [${v.join(", ")}]`);
54500
+ } else if (typeof v === "string" && v.includes("\n")) {
54501
+ lines.push(`${k}: |`);
54502
+ lines.push(...v.split("\n").map((line) => ` ${line}`));
54503
+ } else if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
54504
+ lines.push(`${k}: ${v}`);
54505
+ }
54506
+ }
54507
+ }
54508
+ lines.push("---");
54509
+ lines.push("");
54510
+ lines.push((body ?? "").trim());
54511
+ lines.push("");
54512
+ return lines.join("\n");
54513
+ }
54514
+
54515
+ // src/cli/lib/board-reader.ts
54516
+ function getProjectRoot(kandownDir) {
54517
+ return dirname(kandownDir);
54518
+ }
54519
+ function listTaskIds(kandownDir) {
54520
+ const tasksDir = join2(kandownDir, "tasks");
54521
+ if (!existsSync3(tasksDir)) return [];
54522
+ return readdirSync(tasksDir).filter((name) => name.endsWith(".md")).map((name) => name.slice(0, -3)).sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
54523
+ }
54524
+ function readBoard(kandownDir) {
54525
+ const config = loadConfig(kandownDir);
54526
+ const tasks = listTaskIds(kandownDir).map((id) => {
54527
+ const task = readTask(kandownDir, id);
54528
+ return {
54529
+ ...task,
54530
+ frontmatter: {
54531
+ ...task.frontmatter,
54532
+ id: task.frontmatter.id || id,
54533
+ status: task.frontmatter.status || "Backlog"
54534
+ }
54535
+ };
54536
+ });
54537
+ return {
54538
+ frontmatter: null,
54539
+ title: "Project Kanban",
54540
+ columns: buildColumnsFromTasks(tasks, config.board.columns)
54541
+ };
54542
+ }
54543
+ function readTask(kandownDir, taskId) {
54544
+ const taskPath = join2(kandownDir, "tasks", `${taskId}.md`);
54545
+ if (!existsSync3(taskPath)) {
54546
+ return {
54547
+ frontmatter: { id: taskId, title: `Task ${taskId}`, status: "Backlog" },
54548
+ body: ""
54549
+ };
54550
+ }
54551
+ const content = readFileSync3(taskPath, "utf8");
54552
+ const parsed = parseTaskFile(content);
54553
+ return {
54554
+ ...parsed,
54555
+ frontmatter: {
54556
+ ...parsed.frontmatter,
54557
+ id: parsed.frontmatter.id || taskId,
54558
+ status: parsed.frontmatter.status || "Backlog"
54559
+ }
54560
+ };
54561
+ }
54562
+ function readAgentDoc(kandownDir) {
54563
+ const root = getProjectRoot(kandownDir);
54564
+ const candidates = [
54565
+ join2(root, "AGENT_KANDOWN.md"),
54566
+ join2(kandownDir, "AGENT.md")
54567
+ ];
54568
+ for (const candidate of candidates) {
54569
+ if (existsSync3(candidate)) {
54570
+ return readFileSync3(candidate, "utf8");
54571
+ }
54572
+ }
54573
+ return "";
54574
+ }
54575
+ function moveTaskToColumn(kandownDir, taskId, targetColumn) {
54576
+ const taskPath = join2(kandownDir, "tasks", `${taskId}.md`);
54577
+ if (!existsSync3(taskPath)) return false;
54578
+ const parsed = readTask(kandownDir, taskId);
54579
+ writeFileSync2(taskPath, serializeTaskFile({
54580
+ ...parsed.frontmatter,
54581
+ id: taskId,
54582
+ status: targetColumn
54583
+ }, parsed.body), "utf8");
54584
+ return true;
54585
+ }
54586
+
54587
+ // src/cli/lib/file-watcher.ts
54588
+ import { createReadStream, statSync } from "fs";
54589
+ import { createHash } from "crypto";
54590
+ import { join as join5 } from "path";
54591
+
54331
54592
  // node_modules/.pnpm/chokidar@4.0.3/node_modules/chokidar/esm/index.js
54332
54593
  import { stat as statcb } from "fs";
54333
54594
  import { stat as stat3, readdir as readdir2 } from "fs/promises";
@@ -56018,264 +56279,211 @@ function watch(paths, options = {}) {
56018
56279
  return watcher;
56019
56280
  }
56020
56281
 
56021
- // src/cli/screens/board.tsx
56022
- import { join as join6 } from "path";
56023
-
56024
- // src/cli/lib/board-reader.ts
56025
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
56026
- import { dirname as dirname3, join as join4 } from "path";
56027
-
56028
- // src/lib/types.ts
56029
- var DEFAULT_COLUMNS = ["Backlog", "Todo", "In Progress", "Review", "Done"];
56030
-
56031
- // src/lib/parser.ts
56032
- function parseSimpleYaml(yaml) {
56033
- const obj = {};
56034
- if (!yaml || typeof yaml !== "string") return obj;
56035
- const lines = yaml.split("\n");
56036
- for (let i = 0; i < lines.length; i++) {
56037
- const line = lines[i] ?? "";
56038
- const m = line.match(/^([a-zA-Z_][\w-]*)\s*:\s*(.*)$/);
56039
- if (!m) continue;
56040
- const key = m[1];
56041
- if (!key) continue;
56042
- let val = m[2]?.trim() ?? "";
56043
- if (val === "|") {
56044
- const block = [];
56045
- i++;
56046
- while (i < lines.length && (/^\s+/.test(lines[i] ?? "") || (lines[i] ?? "") === "")) {
56047
- block.push((lines[i] ?? "").replace(/^ /, ""));
56048
- i++;
56282
+ // src/cli/lib/file-watcher.ts
56283
+ function hashFile(filePath) {
56284
+ return new Promise((resolve3, reject) => {
56285
+ const hash = createHash("sha256");
56286
+ const stream = createReadStream(filePath);
56287
+ stream.on("data", (chunk) => hash.update(chunk));
56288
+ stream.on("end", () => resolve3(hash.digest("hex")));
56289
+ stream.on("error", reject);
56290
+ });
56291
+ }
56292
+ function hashFileSync(filePath) {
56293
+ const content = __require("fs").readFileSync(filePath, "utf8");
56294
+ return createHash("sha256").update(content).digest("hex");
56295
+ }
56296
+ var FileWatcher = class {
56297
+ watcher = null;
56298
+ taskHashes = /* @__PURE__ */ new Map();
56299
+ knownTaskIds = /* @__PURE__ */ new Set();
56300
+ listeners = /* @__PURE__ */ new Map();
56301
+ debounceTimers = /* @__PURE__ */ new Map();
56302
+ debounceDelay = 50;
56303
+ watchDebounceDelay = 100;
56304
+ pollInterval = null;
56305
+ stopped = false;
56306
+ /**
56307
+ * 📖 Start watching task files and kandown.json.
56308
+ * Uses chokidar for immediate FS event detection, plus a fallback 500ms
56309
+ * poll to catch any races or network-mounted file changes.
56310
+ */
56311
+ start(kandownDir) {
56312
+ const tasksDir = join5(kandownDir, "tasks");
56313
+ const configPath = join5(kandownDir, "kandown.json");
56314
+ const existingIds = listTaskIds(kandownDir);
56315
+ for (const id of existingIds) {
56316
+ this.knownTaskIds.add(id);
56317
+ try {
56318
+ const filePath = join5(tasksDir, `${id}.md`);
56319
+ this.taskHashes.set(id, hashFileSync(filePath));
56320
+ } catch {
56049
56321
  }
56050
- i--;
56051
- obj[key] = block.join("\n").trimEnd();
56052
- continue;
56053
- }
56054
- if (typeof val !== "string") val = "";
56055
- if (val.startsWith("[") && val.endsWith("]")) {
56056
- const arr = val.slice(1, -1).split(",").map((s) => s && typeof s === "string" ? s.trim().replace(/^["']|["']$/g, "") : "").filter(Boolean);
56057
- obj[key] = arr;
56058
- } else {
56059
- obj[key] = typeof val === "string" ? val.replace(/^["']|["']$/g, "") : val;
56060
56322
  }
56323
+ this.watcher = watch([join5(tasksDir, "*.md"), configPath], {
56324
+ persistent: true,
56325
+ ignoreInitial: true,
56326
+ awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 25 }
56327
+ });
56328
+ this.watcher.on("all", (event, path) => {
56329
+ this.handleFsEvent(event, path, kandownDir);
56330
+ });
56331
+ this.pollInterval = setInterval(() => {
56332
+ this.pollHashes(kandownDir);
56333
+ }, 500);
56061
56334
  }
56062
- return obj;
56063
- }
56064
- function parseTaskFile(md) {
56065
- if (!md || typeof md !== "string") {
56066
- return { frontmatter: { id: "", title: "" }, body: "" };
56335
+ /**
56336
+ * 📖 Stop watching and clean up all resources.
56337
+ */
56338
+ stop() {
56339
+ this.stopped = true;
56340
+ if (this.pollInterval) {
56341
+ clearInterval(this.pollInterval);
56342
+ this.pollInterval = null;
56343
+ }
56344
+ if (this.watcher) {
56345
+ this.watcher.close();
56346
+ this.watcher = null;
56347
+ }
56348
+ this.debounceTimers.forEach((t) => clearTimeout(t));
56349
+ this.debounceTimers.clear();
56350
+ this.taskHashes.clear();
56351
+ this.knownTaskIds.clear();
56352
+ this.emit("stopped");
56353
+ }
56354
+ /** 📖 Register an event handler. Returns an unsubscribe function. */
56355
+ on(event, handler) {
56356
+ if (!this.listeners.has(event)) {
56357
+ this.listeners.set(event, /* @__PURE__ */ new Set());
56358
+ }
56359
+ this.listeners.get(event).add(handler);
56360
+ return () => {
56361
+ this.listeners.get(event)?.delete(handler);
56362
+ };
56067
56363
  }
56068
- const lines = md.split("\n");
56069
- if (lines[0] && lines[0].trim() === "---") {
56070
- const fmLines = [];
56071
- let i = 1;
56072
- while (i < lines.length && lines[i].trim() !== "---") {
56073
- fmLines.push(lines[i]);
56074
- i++;
56364
+ /** 📖 Current set of known task IDs. */
56365
+ getKnownTaskIds() {
56366
+ return Array.from(this.knownTaskIds);
56367
+ }
56368
+ // ─── Private ───────────────────────────────────────────────────────────────
56369
+ handleFsEvent(event, filePath, kandownDir) {
56370
+ if (this.stopped) return;
56371
+ const tasksDir = join5(kandownDir, "tasks");
56372
+ const configPath = join5(kandownDir, "kandown.json");
56373
+ if (filePath === configPath) {
56374
+ const key = `config:${event}`;
56375
+ const existing = this.debounceTimers.get(key);
56376
+ if (existing) clearTimeout(existing);
56377
+ this.debounceTimers.set(key, setTimeout(() => {
56378
+ this.debounceTimers.delete(key);
56379
+ this.emit("configChanged");
56380
+ }, this.watchDebounceDelay));
56381
+ return;
56075
56382
  }
56076
- const body = lines.slice(i + 1).join("\n").trimStart();
56077
- const fm = parseSimpleYaml(fmLines.join("\n"));
56078
- return { frontmatter: fm, body };
56079
- }
56080
- return { frontmatter: { id: "", title: "" }, body: md };
56081
- }
56082
- function normalizeStatus(status) {
56083
- const value = typeof status === "string" ? status.trim() : "";
56084
- return value || "Backlog";
56085
- }
56086
- function normalizePriority(priority) {
56087
- if (typeof priority !== "string") return null;
56088
- const value = priority.toUpperCase();
56089
- return /^(P1|P2|P3|P4)$/.test(value) ? value : null;
56090
- }
56091
- function normalizeOwnerType(ownerType) {
56092
- if (typeof ownerType !== "string") return "";
56093
- const value = ownerType.toLowerCase();
56094
- return value === "human" || value === "ai" ? value : "";
56095
- }
56096
- function taskOrder(task) {
56097
- const value = task.frontmatter.order;
56098
- if (typeof value === "number" && Number.isFinite(value)) return value;
56099
- if (typeof value === "string" && value.trim()) {
56100
- const parsed = Number(value);
56101
- if (Number.isFinite(parsed)) return parsed;
56102
- }
56103
- return Number.MAX_SAFE_INTEGER;
56104
- }
56105
- function taskToBoardTask(task) {
56106
- const { frontmatter, body } = task;
56107
- const { subtasks } = extractSubtasks(body);
56108
- const done = subtasks.filter((s) => s.done).length;
56109
- const total = subtasks.length;
56110
- const status = normalizeStatus(frontmatter.status);
56111
- const tags = Array.isArray(frontmatter.tags) ? frontmatter.tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0) : [];
56112
- return {
56113
- id: frontmatter.id || "",
56114
- title: frontmatter.title || frontmatter.id || "Untitled task",
56115
- checked: /done|termin|closed|complet/i.test(status),
56116
- tags,
56117
- assignee: typeof frontmatter.assignee === "string" && frontmatter.assignee ? frontmatter.assignee : null,
56118
- priority: normalizePriority(frontmatter.priority),
56119
- ownerType: normalizeOwnerType(frontmatter.ownerType),
56120
- progress: total > 0 ? { done, total } : null
56121
- };
56122
- }
56123
- function buildColumnsFromTasks(tasks, configuredColumns = DEFAULT_COLUMNS) {
56124
- const columnNames = configuredColumns.length > 0 ? configuredColumns : DEFAULT_COLUMNS;
56125
- const columnsByName = /* @__PURE__ */ new Map();
56126
- const configured = columnNames.map((name) => ({ name, tasks: [] }));
56127
- for (const column of configured) columnsByName.set(column.name.toLowerCase(), column);
56128
- const unknownColumns = [];
56129
- const sortedTasks = [...tasks].filter((task) => Boolean(task.frontmatter.id)).sort((a, b) => {
56130
- const byOrder = taskOrder(a) - taskOrder(b);
56131
- if (byOrder !== 0) return byOrder;
56132
- return a.frontmatter.id.localeCompare(b.frontmatter.id, void 0, { numeric: true });
56133
- });
56134
- for (const task of sortedTasks) {
56135
- const status = normalizeStatus(task.frontmatter.status);
56136
- let column = columnsByName.get(status.toLowerCase());
56137
- if (!column) {
56138
- column = { name: status, tasks: [] };
56139
- columnsByName.set(status.toLowerCase(), column);
56140
- unknownColumns.push(column);
56383
+ const taskId = filePath.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/, "") ?? "";
56384
+ if (!taskId) return;
56385
+ if (event === "add" || event === "change") {
56386
+ const key = `task:${taskId}:${event}`;
56387
+ const existing = this.debounceTimers.get(key);
56388
+ if (existing) clearTimeout(existing);
56389
+ this.debounceTimers.set(key, setTimeout(() => {
56390
+ this.debounceTimers.delete(key);
56391
+ void this.checkTaskContentChange(taskId, filePath, true);
56392
+ }, this.watchDebounceDelay));
56393
+ } else if (event === "unlink") {
56394
+ this.taskHashes.delete(taskId);
56395
+ this.knownTaskIds.delete(taskId);
56396
+ }
56397
+ }
56398
+ async checkTaskContentChange(taskId, filePath, isNew) {
56399
+ try {
56400
+ const newHash = await hashFile(filePath);
56401
+ const oldHash = this.taskHashes.get(taskId);
56402
+ if (isNew && !this.knownTaskIds.has(taskId)) {
56403
+ this.knownTaskIds.add(taskId);
56404
+ this.taskHashes.set(taskId, newHash);
56405
+ this.emit("newTaskDetected", taskId);
56406
+ return;
56407
+ }
56408
+ if (oldHash !== void 0 && newHash !== oldHash) {
56409
+ this.taskHashes.set(taskId, newHash);
56410
+ this.emit("taskChanged", taskId);
56411
+ } else if (oldHash === void 0) {
56412
+ this.knownTaskIds.add(taskId);
56413
+ this.taskHashes.set(taskId, newHash);
56414
+ if (isNew) {
56415
+ this.emit("newTaskDetected", taskId);
56416
+ }
56417
+ }
56418
+ } catch {
56141
56419
  }
56142
- column.tasks.push(taskToBoardTask(task));
56143
56420
  }
56144
- return [...unknownColumns, ...configured];
56145
- }
56146
- function extractSubtasks(body) {
56147
- const subtasks = [];
56148
- if (!body || typeof body !== "string") return { subtasks, bodyWithoutSubtasks: body ?? "" };
56149
- const lines = body.split("\n");
56150
- const kept = [];
56151
- let inSubtaskSection = false;
56152
- for (const line of lines) {
56153
- if (/^#{1,6}\s+(subtasks?|sous[- ]t[âa]ches?|crit[èe]res?)/i.test(line)) {
56154
- inSubtaskSection = true;
56155
- kept.push(line);
56156
- continue;
56157
- }
56158
- if (/^#{1,6}\s+/.test(line) && inSubtaskSection) {
56159
- inSubtaskSection = false;
56160
- kept.push(line);
56161
- continue;
56162
- }
56163
- const m = line.match(/^\s*-\s+\[([ xX])\]\s+(.+)$/);
56164
- if (m && inSubtaskSection) {
56165
- const text = m[2]?.trim() ?? "";
56166
- subtasks.push({ done: (m[1]?.toLowerCase() ?? "") === "x", text });
56167
- continue;
56168
- }
56169
- const descMatch = line.match(/^\s*\[DESC\]\s*(.*)$/);
56170
- if (descMatch && subtasks.length > 0) {
56171
- subtasks[subtasks.length - 1].description = descMatch[1];
56172
- continue;
56173
- }
56174
- const reportMatch = line.match(/^\s*\[REPORT\]\s*(.*)$/);
56175
- if (reportMatch && subtasks.length > 0) {
56176
- subtasks[subtasks.length - 1].report = reportMatch[1];
56177
- continue;
56421
+ /** 📖 Fallback poll — catches changes that chokidar missed (network mounts, exotic FS). */
56422
+ async pollHashes(kandownDir) {
56423
+ if (this.stopped) return;
56424
+ const tasksDir = join5(kandownDir, "tasks");
56425
+ const configPath = join5(kandownDir, "kandown.json");
56426
+ try {
56427
+ const newHash = hashFileSync(configPath);
56428
+ } catch {
56178
56429
  }
56179
- kept.push(line);
56180
- }
56181
- return { subtasks, bodyWithoutSubtasks: kept.join("\n") };
56182
- }
56183
-
56184
- // src/lib/serializer.ts
56185
- function serializeTaskFile(frontmatter, body) {
56186
- const lines = ["---"];
56187
- if (frontmatter && typeof frontmatter === "object") {
56188
- for (const [k, v] of Object.entries(frontmatter)) {
56189
- if (v === null || v === void 0 || v === "") continue;
56190
- if (Array.isArray(v)) {
56191
- if (v.length === 0) continue;
56192
- lines.push(`${k}: [${v.join(", ")}]`);
56193
- } else if (typeof v === "string" && v.includes("\n")) {
56194
- lines.push(`${k}: |`);
56195
- lines.push(...v.split("\n").map((line) => ` ${line}`));
56196
- } else if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
56197
- lines.push(`${k}: ${v}`);
56430
+ for (const taskId of this.knownTaskIds) {
56431
+ const filePath = join5(tasksDir, `${taskId}.md`);
56432
+ try {
56433
+ statSync(filePath);
56434
+ const newHash = await hashFile(filePath);
56435
+ const oldHash = this.taskHashes.get(taskId);
56436
+ if (oldHash !== void 0 && newHash !== oldHash) {
56437
+ this.taskHashes.set(taskId, newHash);
56438
+ this.emit("taskChanged", taskId);
56439
+ }
56440
+ } catch {
56441
+ this.taskHashes.delete(taskId);
56442
+ this.knownTaskIds.delete(taskId);
56443
+ }
56444
+ }
56445
+ const currentIds = listTaskIds(kandownDir);
56446
+ for (const id of currentIds) {
56447
+ if (!this.knownTaskIds.has(id)) {
56448
+ const filePath = join5(tasksDir, `${id}.md`);
56449
+ try {
56450
+ const newHash = await hashFile(filePath);
56451
+ this.knownTaskIds.add(id);
56452
+ this.taskHashes.set(id, newHash);
56453
+ this.emit("newTaskDetected", id);
56454
+ } catch {
56455
+ }
56198
56456
  }
56199
56457
  }
56200
56458
  }
56201
- lines.push("---");
56202
- lines.push("");
56203
- lines.push((body ?? "").trim());
56204
- lines.push("");
56205
- return lines.join("\n");
56206
- }
56207
-
56208
- // src/cli/lib/board-reader.ts
56209
- function getProjectRoot(kandownDir) {
56210
- return dirname3(kandownDir);
56211
- }
56212
- function listTaskIds(kandownDir) {
56213
- const tasksDir = join4(kandownDir, "tasks");
56214
- if (!existsSync3(tasksDir)) return [];
56215
- return readdirSync(tasksDir).filter((name) => name.endsWith(".md")).map((name) => name.slice(0, -3)).sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
56216
- }
56217
- function readBoard(kandownDir) {
56218
- const config = loadConfig(kandownDir);
56219
- const tasks = listTaskIds(kandownDir).map((id) => {
56220
- const task = readTask(kandownDir, id);
56221
- return {
56222
- ...task,
56223
- frontmatter: {
56224
- ...task.frontmatter,
56225
- id: task.frontmatter.id || id,
56226
- status: task.frontmatter.status || "Backlog"
56459
+ debouncedEmit(event, ...args) {
56460
+ const key = event + JSON.stringify(args);
56461
+ const existing = this.debounceTimers.get(key);
56462
+ if (existing) clearTimeout(existing);
56463
+ const timer = setTimeout(() => {
56464
+ this.debounceTimers.delete(key);
56465
+ this.emit(event, ...args);
56466
+ }, this.debounceDelay);
56467
+ this.debounceTimers.set(key, timer);
56468
+ }
56469
+ emit(event, ...args) {
56470
+ if (this.stopped) return;
56471
+ const handlers = this.listeners.get(event);
56472
+ handlers?.forEach((handler) => {
56473
+ if (event === "configChanged") {
56474
+ handler();
56475
+ } else if (event === "taskChanged") {
56476
+ handler(...args);
56477
+ } else if (event === "newTaskDetected") {
56478
+ handler(...args);
56479
+ } else {
56480
+ handler();
56227
56481
  }
56228
- };
56229
- });
56230
- return {
56231
- frontmatter: null,
56232
- title: "Project Kanban",
56233
- columns: buildColumnsFromTasks(tasks, config.board.columns)
56234
- };
56235
- }
56236
- function readTask(kandownDir, taskId) {
56237
- const taskPath = join4(kandownDir, "tasks", `${taskId}.md`);
56238
- if (!existsSync3(taskPath)) {
56239
- return {
56240
- frontmatter: { id: taskId, title: `Task ${taskId}`, status: "Backlog" },
56241
- body: ""
56242
- };
56243
- }
56244
- const content = readFileSync3(taskPath, "utf8");
56245
- const parsed = parseTaskFile(content);
56246
- return {
56247
- ...parsed,
56248
- frontmatter: {
56249
- ...parsed.frontmatter,
56250
- id: parsed.frontmatter.id || taskId,
56251
- status: parsed.frontmatter.status || "Backlog"
56252
- }
56253
- };
56254
- }
56255
- function readAgentDoc(kandownDir) {
56256
- const root = getProjectRoot(kandownDir);
56257
- const candidates = [
56258
- join4(root, "AGENT_KANDOWN_COMPACT.md"),
56259
- join4(root, "AGENT_KANDOWN.md"),
56260
- join4(kandownDir, "AGENT.md")
56261
- ];
56262
- for (const candidate of candidates) {
56263
- if (existsSync3(candidate)) {
56264
- return readFileSync3(candidate, "utf8");
56265
- }
56482
+ });
56266
56483
  }
56267
- return "";
56268
- }
56269
- function moveTaskToColumn(kandownDir, taskId, targetColumn) {
56270
- const taskPath = join4(kandownDir, "tasks", `${taskId}.md`);
56271
- if (!existsSync3(taskPath)) return false;
56272
- const parsed = readTask(kandownDir, taskId);
56273
- writeFileSync2(taskPath, serializeTaskFile({
56274
- ...parsed.frontmatter,
56275
- id: taskId,
56276
- status: targetColumn
56277
- }, parsed.body), "utf8");
56278
- return true;
56484
+ };
56485
+ function createWatcher() {
56486
+ return new FileWatcher();
56279
56487
  }
56280
56488
 
56281
56489
  // src/cli/lib/agents.ts
@@ -56409,7 +56617,7 @@ function buildPrompt(agentDoc, taskContent, taskId, kandownDir) {
56409
56617
  // src/cli/lib/launcher.ts
56410
56618
  import { execSync, spawn } from "child_process";
56411
56619
  import { writeFileSync as writeFileSync3 } from "fs";
56412
- import { join as join5 } from "path";
56620
+ import { join as join6 } from "path";
56413
56621
  import { tmpdir } from "os";
56414
56622
  function isInTmux() {
56415
56623
  return !!process.env.TMUX;
@@ -56433,7 +56641,7 @@ function launchAgent(opts) {
56433
56641
  ].join("\n");
56434
56642
  const { systemPrompt, taskPrompt } = buildPrompt(agentDoc, taskFileContent, taskId, kandownDir);
56435
56643
  moveTaskToColumn(kandownDir, taskId, "In Progress");
56436
- const contextFile = join5(tmpdir(), `kandown-${taskId}-context.md`);
56644
+ const contextFile = join6(tmpdir(), `kandown-${taskId}-context.md`);
56437
56645
  writeFileSync3(contextFile, `${systemPrompt}
56438
56646
 
56439
56647
  ---
@@ -56724,27 +56932,27 @@ function Board({ kandownDir }) {
56724
56932
  setBoard(loaded);
56725
56933
  setInstalledAgents(detectInstalledAgents());
56726
56934
  }, [kandownDir]);
56935
+ const colIndexRef = (0, import_react36.useRef)(0);
56936
+ const rowIndexRef = (0, import_react36.useRef)(0);
56727
56937
  (0, import_react36.useEffect)(() => {
56728
- const tasksDir = join6(kandownDir, "tasks");
56729
- const configPath = join6(kandownDir, "kandown.json");
56730
- let debounceTimer = null;
56731
- const watcher = watch([join6(tasksDir, "*.md"), configPath], {
56732
- persistent: true,
56733
- ignoreInitial: true,
56734
- awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
56938
+ const watcher = createWatcher();
56939
+ watcher.on("taskChanged", () => {
56940
+ const loaded = readBoard(kandownDir);
56941
+ setBoard(loaded);
56942
+ });
56943
+ watcher.on("newTaskDetected", (taskId) => {
56944
+ const loaded = readBoard(kandownDir);
56945
+ setBoard(loaded);
56946
+ setStatusMsg(`New task: ${taskId}`);
56947
+ setTimeout(() => setStatusMsg(""), 2e3);
56735
56948
  });
56736
- watcher.on("all", (event) => {
56737
- if (debounceTimer) clearTimeout(debounceTimer);
56738
- debounceTimer = setTimeout(() => {
56739
- const loaded = readBoard(kandownDir);
56740
- setBoard(loaded);
56741
- setStatusMsg(`Reloaded (${event})`);
56742
- setTimeout(() => setStatusMsg(""), 1500);
56743
- }, 100);
56949
+ watcher.on("configChanged", () => {
56950
+ const loaded = readBoard(kandownDir);
56951
+ setBoard(loaded);
56744
56952
  });
56953
+ watcher.start(kandownDir);
56745
56954
  return () => {
56746
- watcher.close();
56747
- if (debounceTimer) clearTimeout(debounceTimer);
56955
+ watcher.stop();
56748
56956
  };
56749
56957
  }, [kandownDir]);
56750
56958
  const reloadBoard = (0, import_react36.useCallback)(() => {