@doquflow/cli 1.5.1 → 1.5.2

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.
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.registerInGlobalRegistry = registerInGlobalRegistry;
7
7
  exports.buildClaudeMd = buildClaudeMd;
8
8
  exports.buildAgentsMd = buildAgentsMd;
9
+ exports.runInit = runInit;
9
10
  exports.run = run;
10
11
  const node_fs_1 = __importDefault(require("node:fs"));
11
12
  const promises_1 = __importDefault(require("node:fs/promises"));
@@ -157,7 +158,7 @@ lint_wiki({ project_path: "${projectDir}" })
157
158
 
158
159
  \`\`\`
159
160
  .docuflow/
160
- ├── specs/ Legacy spec files written by write_spec
161
+ ├── specs/ Spec files written by write_spec
161
162
  ├── wiki/ LLM-generated wiki pages
162
163
  │ ├── entities/ Named things (services, APIs, databases)
163
164
  │ ├── concepts/ Design patterns, principles, integrations
@@ -276,7 +277,16 @@ async function writeAgentsMd(projectDir) {
276
277
  await promises_1.default.writeFile(agentsMdPath, newSection, "utf8");
277
278
  }
278
279
  }
279
- async function run() {
280
+ /**
281
+ * Core init logic — creates the .docuflow/ structure, registers MCP configs,
282
+ * writes CLAUDE.md/AGENTS.md, installs git hook, and registers in global registry.
283
+ *
284
+ * Accepts the target projectDir explicitly so it can be called from both the CLI
285
+ * (which uses process.cwd()) and the API (/api/init which receives the path from
286
+ * the request body).
287
+ */
288
+ async function runInit(projectDir) {
289
+ const details = [];
280
290
  const configPath = getClaudeDesktopConfigPath();
281
291
  const vscodeConfigPath = getVSCodeMcpConfigPath();
282
292
  const copilotCliConfigPath = getCopilotCliMcpConfigPath();
@@ -284,61 +294,59 @@ async function run() {
284
294
  const serverBin = resolveServerBin();
285
295
  const nodeBin = process.execPath;
286
296
  // Register in Claude Desktop config
287
- let config = {};
288
297
  try {
289
- const raw = await promises_1.default.readFile(configPath, "utf8");
290
- config = JSON.parse(raw);
298
+ let config = {};
299
+ try {
300
+ const raw = await promises_1.default.readFile(configPath, "utf8");
301
+ config = JSON.parse(raw);
302
+ }
303
+ catch { /* File doesn't exist yet — that's fine */ }
304
+ if (!config.mcpServers)
305
+ config.mcpServers = {};
306
+ config.mcpServers.docuflow = { command: nodeBin, args: [serverBin] };
307
+ await promises_1.default.mkdir(node_path_1.default.dirname(configPath), { recursive: true });
308
+ await promises_1.default.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
309
+ details.push("Claude Desktop MCP registered");
291
310
  }
292
311
  catch {
293
- // File doesn't exist yet that's fine, we'll create it
312
+ details.push("Claude Desktopskipped (not installed)");
294
313
  }
295
- if (!config.mcpServers)
296
- config.mcpServers = {};
297
- config.mcpServers.docuflow = { command: nodeBin, args: [serverBin] };
298
- await promises_1.default.mkdir(node_path_1.default.dirname(configPath), { recursive: true });
299
- await promises_1.default.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
300
314
  // Register in VS Code (GitHub Copilot) user MCP config
301
315
  let vscodeRegistered = false;
302
- let vscodeConfig = {};
303
- try {
304
- const raw = await promises_1.default.readFile(vscodeConfigPath, "utf8");
305
- vscodeConfig = JSON.parse(raw);
306
- }
307
- catch {
308
- // File may not exist — create it
309
- }
310
- if (!vscodeConfig.servers)
311
- vscodeConfig.servers = {};
312
- vscodeConfig.servers.docuflow = { command: nodeBin, args: [serverBin], type: "stdio" };
313
316
  try {
317
+ let vscodeConfig = {};
318
+ try {
319
+ const raw = await promises_1.default.readFile(vscodeConfigPath, "utf8");
320
+ vscodeConfig = JSON.parse(raw);
321
+ }
322
+ catch { /* File may not exist */ }
323
+ if (!vscodeConfig.servers)
324
+ vscodeConfig.servers = {};
325
+ vscodeConfig.servers.docuflow = { command: nodeBin, args: [serverBin], type: "stdio" };
314
326
  await promises_1.default.mkdir(node_path_1.default.dirname(vscodeConfigPath), { recursive: true });
315
327
  await promises_1.default.writeFile(vscodeConfigPath, JSON.stringify(vscodeConfig, null, 2) + "\n", "utf8");
316
328
  vscodeRegistered = true;
329
+ details.push("VS Code Copilot MCP registered");
317
330
  }
318
- catch {
319
- // VS Code not installed or config dir not writable — skip silently
320
- }
331
+ catch { /* VS Code not installed — skip */ }
321
332
  // Register in GitHub Copilot CLI MCP config (~/.copilot/mcp-config.json)
322
333
  let copilotCliRegistered = false;
323
- let copilotCliConfig = {};
324
- try {
325
- const raw = await promises_1.default.readFile(copilotCliConfigPath, "utf8");
326
- copilotCliConfig = JSON.parse(raw);
327
- }
328
- catch {
329
- // File may not exist yet
330
- }
331
- if (!copilotCliConfig.mcpServers)
332
- copilotCliConfig.mcpServers = {};
333
- copilotCliConfig.mcpServers.docuflow = { type: "local", command: nodeBin, args: [serverBin], tools: ["*"] };
334
334
  try {
335
+ let copilotCliConfig = {};
336
+ try {
337
+ const raw = await promises_1.default.readFile(copilotCliConfigPath, "utf8");
338
+ copilotCliConfig = JSON.parse(raw);
339
+ }
340
+ catch { /* File may not exist yet */ }
341
+ if (!copilotCliConfig.mcpServers)
342
+ copilotCliConfig.mcpServers = {};
343
+ copilotCliConfig.mcpServers.docuflow = { type: "local", command: nodeBin, args: [serverBin], tools: ["*"] };
335
344
  await promises_1.default.mkdir(node_path_1.default.dirname(copilotCliConfigPath), { recursive: true });
336
345
  await promises_1.default.writeFile(copilotCliConfigPath, JSON.stringify(copilotCliConfig, null, 2) + "\n", "utf8");
337
346
  copilotCliRegistered = true;
347
+ details.push("Copilot CLI MCP registered");
338
348
  }
339
- catch {
340
- // Copilot CLI not installed — skip silently
341
- }
349
+ catch { /* Copilot CLI not installed — skip */ }
342
350
  // Register in OpenAI Codex CLI (~/.codex/config.toml in TOML format)
343
351
  let codexCliRegistered = false;
344
352
  try {
@@ -353,99 +361,107 @@ async function run() {
353
361
  await promises_1.default.writeFile(codexConfigPath, tomlContent + entry, "utf8");
354
362
  }
355
363
  codexCliRegistered = true;
364
+ details.push("Codex CLI MCP registered");
356
365
  }
357
- catch {
358
- // Codex CLI not installed skip silently
359
- }
366
+ catch { /* Codex CLI not installed — skip */ }
367
+ // Suppress unused-variable warnings when variables are only used in console.log branches
368
+ void vscodeRegistered;
369
+ void copilotCliRegistered;
370
+ void codexCliRegistered;
360
371
  // Create .docuflow/ directory structure
361
- const projectDir = process.cwd();
362
372
  const docuflowDir = node_path_1.default.join(projectDir, ".docuflow");
363
373
  const specsDir = node_path_1.default.join(docuflowDir, "specs");
364
374
  const wikiDir = node_path_1.default.join(docuflowDir, "wiki");
365
375
  const sourcesDir = node_path_1.default.join(docuflowDir, "sources");
366
- const entitiesDir = node_path_1.default.join(wikiDir, "entities");
367
- const conceptsDir = node_path_1.default.join(wikiDir, "concepts");
368
- const timelinesDir = node_path_1.default.join(wikiDir, "timelines");
369
- const synthesesDir = node_path_1.default.join(wikiDir, "syntheses");
370
376
  await promises_1.default.mkdir(specsDir, { recursive: true });
371
- await promises_1.default.mkdir(entitiesDir, { recursive: true });
372
- await promises_1.default.mkdir(conceptsDir, { recursive: true });
373
- await promises_1.default.mkdir(timelinesDir, { recursive: true });
374
- await promises_1.default.mkdir(synthesesDir, { recursive: true });
377
+ await promises_1.default.mkdir(node_path_1.default.join(wikiDir, "entities"), { recursive: true });
378
+ await promises_1.default.mkdir(node_path_1.default.join(wikiDir, "concepts"), { recursive: true });
379
+ await promises_1.default.mkdir(node_path_1.default.join(wikiDir, "timelines"), { recursive: true });
380
+ await promises_1.default.mkdir(node_path_1.default.join(wikiDir, "syntheses"), { recursive: true });
375
381
  await promises_1.default.mkdir(sourcesDir, { recursive: true });
382
+ details.push("Created .docuflow/ directory structure");
376
383
  // Copy or create template files
377
384
  await copyTemplateFile("schema.md", node_path_1.default.join(docuflowDir, "schema.md"));
378
385
  await copyTemplateFile("index.md", node_path_1.default.join(docuflowDir, "index.md"));
379
386
  await copyTemplateFile("log.md", node_path_1.default.join(docuflowDir, "log.md"));
380
- // Generate CLAUDE.md so Claude Code picks up DocuFlow automatically
387
+ details.push("Wrote schema.md, index.md, log.md");
388
+ // Generate CLAUDE.md
381
389
  await writeClaudeMd(projectDir);
382
- // Generate AGENTS.md so OpenAI Codex picks up DocuFlow automatically
390
+ details.push("Wrote CLAUDE.md");
391
+ // Generate AGENTS.md
383
392
  await writeAgentsMd(projectDir);
384
- // Write .vscode/mcp.json for project-level workspace MCP config (shareable via git)
385
- // Uses npx so it works on any machine — safe to commit
386
- const vscodeDirPath = node_path_1.default.join(projectDir, ".vscode");
387
- const vscodeWorkspaceMcpPath = node_path_1.default.join(vscodeDirPath, "mcp.json");
388
- let workspaceMcpConfig = {};
393
+ details.push("Wrote AGENTS.md");
394
+ // Write .vscode/mcp.json for project-level workspace MCP config
389
395
  try {
390
- const raw = await promises_1.default.readFile(vscodeWorkspaceMcpPath, "utf8");
391
- workspaceMcpConfig = JSON.parse(raw);
392
- }
393
- catch {
394
- // File doesn't exist yet
396
+ const vscodeDirPath = node_path_1.default.join(projectDir, ".vscode");
397
+ const vscodeWorkspaceMcpPath = node_path_1.default.join(vscodeDirPath, "mcp.json");
398
+ let workspaceMcpConfig = {};
399
+ try {
400
+ const raw = await promises_1.default.readFile(vscodeWorkspaceMcpPath, "utf8");
401
+ workspaceMcpConfig = JSON.parse(raw);
402
+ }
403
+ catch { /* File doesn't exist yet */ }
404
+ if (!workspaceMcpConfig.servers)
405
+ workspaceMcpConfig.servers = {};
406
+ workspaceMcpConfig.servers.docuflow = {
407
+ command: "npx",
408
+ args: ["-y", "-p", "@doquflow/server", "docuflow-server"],
409
+ type: "stdio",
410
+ };
411
+ await promises_1.default.mkdir(vscodeDirPath, { recursive: true });
412
+ await promises_1.default.writeFile(vscodeWorkspaceMcpPath, JSON.stringify(workspaceMcpConfig, null, 2) + "\n", "utf8");
413
+ details.push("Wrote .vscode/mcp.json");
395
414
  }
396
- if (!workspaceMcpConfig.servers)
397
- workspaceMcpConfig.servers = {};
398
- workspaceMcpConfig.servers.docuflow = {
399
- command: "npx",
400
- args: ["-y", "-p", "@doquflow/server", "docuflow-server"],
401
- type: "stdio",
402
- };
403
- await promises_1.default.mkdir(vscodeDirPath, { recursive: true });
404
- await promises_1.default.writeFile(vscodeWorkspaceMcpPath, JSON.stringify(workspaceMcpConfig, null, 2) + "\n", "utf8");
415
+ catch { /* non-fatal */ }
405
416
  // Add .docuflow/ to .gitignore if present and not already listed
406
- const gitignorePath = node_path_1.default.join(process.cwd(), ".gitignore");
407
- if (node_fs_1.default.existsSync(gitignorePath)) {
408
- const gitignore = await promises_1.default.readFile(gitignorePath, "utf8");
409
- if (!gitignore.includes(".docuflow/") && !gitignore.includes(".docuflow")) {
410
- await promises_1.default.appendFile(gitignorePath, "\n# Docuflow\n.docuflow/\n");
417
+ try {
418
+ const gitignorePath = node_path_1.default.join(projectDir, ".gitignore");
419
+ if (node_fs_1.default.existsSync(gitignorePath)) {
420
+ const gitignore = await promises_1.default.readFile(gitignorePath, "utf8");
421
+ if (!gitignore.includes(".docuflow/") && !gitignore.includes(".docuflow")) {
422
+ await promises_1.default.appendFile(gitignorePath, "\n# Docuflow\n.docuflow/\n");
423
+ details.push("Added .docuflow/ to .gitignore");
424
+ }
411
425
  }
412
426
  }
413
- // Install git post-commit hook (auto-sync on every commit)
427
+ catch { /* non-fatal */ }
428
+ // Install git post-commit hook
414
429
  await installGitHook(projectDir);
415
- // Register in global project registry so `docuflow ui` always finds this project
430
+ details.push("Installed git post-commit hook");
431
+ // Register in global project registry
416
432
  await registerInGlobalRegistry(projectDir);
417
- console.log("\u2713 DocuFlow initialised successfully.");
433
+ details.push("Registered in global project registry (~/.docuflow/projects.json)");
434
+ return { ok: true, path: projectDir, details };
435
+ }
436
+ async function run() {
437
+ const result = await runInit(process.cwd());
438
+ const docuflowDir = node_path_1.default.join(result.path, ".docuflow");
439
+ console.log("✓ DocuFlow initialised successfully.");
418
440
  console.log("");
419
- console.log("\ud83d\udcc1 Structure created:");
441
+ console.log("📁 Structure created:");
420
442
  console.log(` ${docuflowDir}/`);
421
- console.log(` \u251c\u2500\u2500 specs/ (code specs written by the agent)`);
422
- console.log(` \u251c\u2500\u2500 wiki/ (LLM-generated wiki pages)`);
423
- console.log(` \u2502 \u251c\u2500\u2500 entities/`);
424
- console.log(` \u2502 \u251c\u2500\u2500 concepts/`);
425
- console.log(` \u2502 \u251c\u2500\u2500 timelines/`);
426
- console.log(` \u2502 \u2514\u2500\u2500 syntheses/`);
427
- console.log(` \u251c\u2500\u2500 sources/ (raw markdown documents to ingest)`);
428
- console.log(` \u251c\u2500\u2500 schema.md (wiki configuration)`);
429
- console.log(` \u251c\u2500\u2500 index.md (auto-maintained catalog)`);
430
- console.log(` \u2514\u2500\u2500 log.md (operation log)`);
443
+ console.log(` ├── specs/ (code specs written by the agent)`);
444
+ console.log(` ├── wiki/ (LLM-generated wiki pages)`);
445
+ console.log(` ├── entities/`);
446
+ console.log(` ├── concepts/`);
447
+ console.log(` ├── timelines/`);
448
+ console.log(` └── syntheses/`);
449
+ console.log(` ├── sources/ (raw markdown documents to ingest)`);
450
+ console.log(` ├── schema.md (wiki configuration)`);
451
+ console.log(` ├── index.md (auto-maintained catalog)`);
452
+ console.log(` └── log.md (operation log)`);
431
453
  console.log("");
432
- console.log("\ud83d\udcdd Instruction files:");
433
- console.log(` CLAUDE.md ✓ ${node_path_1.default.join(projectDir, "CLAUDE.md")}`);
434
- console.log(` AGENTS.md ✓ ${node_path_1.default.join(projectDir, "AGENTS.md")}`);
435
- console.log("");
436
- console.log("\ud83d\udd27 MCP Registration:");
437
- console.log(` Claude Desktop: \u2713 registered`);
438
- console.log(` VS Code Copilot: ${vscodeRegistered ? "\u2713 registered (user-level)" : "\u2014 not detected"}`);
439
- console.log(` Copilot CLI: ${copilotCliRegistered ? "\u2713 registered (~/.copilot/mcp-config.json)" : "\u2014 not detected"}`);
440
- console.log(` Codex CLI: ${codexCliRegistered ? "\u2713 registered (~/.codex/config.toml)" : "\u2014 not detected"}`);
441
- console.log(` Workspace: \u2713 .vscode/mcp.json written (commit to share with team)`);
454
+ console.log("📝 Steps completed:");
455
+ for (const line of result.details) {
456
+ console.log(` ✓ ${line}`);
457
+ }
442
458
  console.log("");
443
- console.log("\ud83d\udcd6 Next steps:");
459
+ console.log("📖 Next steps:");
444
460
  console.log(" 1. Edit .docuflow/schema.md to customize your wiki domain");
445
461
  console.log(" 2. Add markdown docs to .docuflow/sources/ then ingest them");
446
462
  console.log(" 3. Restart Claude Desktop / reload VS Code / restart Copilot CLI");
447
463
  console.log("");
448
- console.log("\u26a1 Auto-sync options:");
464
+ console.log(" Auto-sync options:");
449
465
  console.log(" docuflow watch # background daemon (watches for file changes)");
450
466
  console.log(" docuflow watch --ai # + Claude/Codex documents code changes automatically");
451
467
  console.log(" docuflow sync # one-shot sync (good for CI/CD)");
@@ -0,0 +1,276 @@
1
+ "use strict";
2
+ /**
3
+ * docuflow recent
4
+ *
5
+ * Aggregates recent work by scanning .devloop/specs/TASK-*.md files,
6
+ * correlating git commits by task ID, reading .docuflow/log.md for wiki
7
+ * activity, and rendering a formatted dashboard in the terminal.
8
+ */
9
+ var __importDefault = (this && this.__importDefault) || function (mod) {
10
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.run = run;
14
+ const promises_1 = __importDefault(require("node:fs/promises"));
15
+ const node_path_1 = __importDefault(require("node:path"));
16
+ const node_child_process_1 = require("node:child_process");
17
+ // ── ANSI helpers ────────────────────────────────────────────────────────────
18
+ const c = {
19
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
20
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
21
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
22
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
23
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
24
+ };
25
+ function colourStatus(status, text) {
26
+ if (status === "approved")
27
+ return c.green(text);
28
+ if (status === "needs-work")
29
+ return c.yellow(text);
30
+ if (status === "rejected")
31
+ return c.red(text);
32
+ return c.dim(text);
33
+ }
34
+ function normaliseStatus(raw) {
35
+ // Strip emojis (✅ ⚠️ ⏳ ❌) and other non-printable/non-ASCII characters
36
+ const stripped = raw.replace(/[^\x20-\x7E]/g, " ").trim().toLowerCase();
37
+ if (stripped.includes("approved"))
38
+ return "approved";
39
+ if (stripped.includes("needs-work") || stripped.includes("needs_work"))
40
+ return "needs-work";
41
+ if (stripped.includes("rejected"))
42
+ return "rejected";
43
+ if (stripped.includes("pending"))
44
+ return "pending";
45
+ return stripped.split(/\s+/)[0] || "pending";
46
+ }
47
+ function truncate(s, max) {
48
+ return s.length > max ? s.slice(0, max - 1) + "…" : s;
49
+ }
50
+ function formatDateYMD(d) {
51
+ return d.toISOString().slice(0, 10);
52
+ }
53
+ // ── Git helper ───────────────────────────────────────────────────────────────
54
+ function gitLog(taskId) {
55
+ try {
56
+ const out = (0, node_child_process_1.execSync)(`git log --oneline --all --grep="${taskId}"`, { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] });
57
+ return out.trim().split("\n").filter(Boolean);
58
+ }
59
+ catch {
60
+ return [];
61
+ }
62
+ }
63
+ // ── Spec parser ───────────────────────────────────────────────────────────────
64
+ async function parseSpec(specPath) {
65
+ let content;
66
+ try {
67
+ content = await promises_1.default.readFile(specPath, "utf8");
68
+ }
69
+ catch {
70
+ return {};
71
+ }
72
+ let title = "";
73
+ let feature = "";
74
+ let status = "pending";
75
+ for (const line of content.split("\n")) {
76
+ // First H1 heading: "# TASK-YYYYMMDD-HHMMSS: <title>"
77
+ if (!title && line.startsWith("# TASK-")) {
78
+ const colonIdx = line.indexOf(":");
79
+ if (colonIdx !== -1) {
80
+ title = line.slice(colonIdx + 1).trim();
81
+ }
82
+ }
83
+ // **Feature**: value
84
+ if (!feature && line.startsWith("**Feature**:")) {
85
+ feature = line.slice("**Feature**:".length).trim().slice(0, 120).trimEnd();
86
+ }
87
+ // **Status**: value
88
+ if (line.startsWith("**Status**:")) {
89
+ const raw = line.slice("**Status**:".length).trim();
90
+ status = normaliseStatus(raw);
91
+ }
92
+ }
93
+ return { title, feature, status };
94
+ }
95
+ // ── Review parser ────────────────────────────────────────────────────────────
96
+ async function parseReview(reviewPath) {
97
+ let content;
98
+ try {
99
+ content = await promises_1.default.readFile(reviewPath, "utf8");
100
+ }
101
+ catch {
102
+ return { score: null, reviewVerdict: null };
103
+ }
104
+ let score = null;
105
+ let reviewVerdict = null;
106
+ const scoreMatch = content.match(/Score:\s*(\d+\/\d+)/);
107
+ if (scoreMatch)
108
+ score = scoreMatch[1];
109
+ const verdictMatch = content.match(/Verdict:\s*(APPROVED|NEEDS[\s_]WORK|REJECTED)/i);
110
+ if (verdictMatch) {
111
+ reviewVerdict = verdictMatch[1].toUpperCase().replace(/[\s-]+/g, "_");
112
+ }
113
+ return { score, reviewVerdict };
114
+ }
115
+ // ── Wiki log parser ──────────────────────────────────────────────────────────
116
+ // log.md format: ## [2026-05-01T13:49:38.141Z] operation | description
117
+ async function parseWikiLog(logPath, since) {
118
+ let content;
119
+ try {
120
+ content = await promises_1.default.readFile(logPath, "utf8");
121
+ }
122
+ catch {
123
+ return [];
124
+ }
125
+ const entries = [];
126
+ const headingRe = /^##\s+\[([^\]]+)\]\s+([^|]+)\|\s*(.+)$/;
127
+ for (const line of content.split("\n")) {
128
+ const m = line.match(headingRe);
129
+ if (!m)
130
+ continue;
131
+ const rawDate = m[1].trim();
132
+ const operation = m[2].trim();
133
+ const file = m[3].trim();
134
+ const dateVal = new Date(rawDate);
135
+ if (!isNaN(dateVal.getTime()) && dateVal >= since) {
136
+ entries.push({ date: rawDate.slice(0, 10), operation, file });
137
+ }
138
+ }
139
+ return entries;
140
+ }
141
+ // ── Renderers ────────────────────────────────────────────────────────────────
142
+ function renderTable(tasks, allCommits, wikiEntries, days, fromDate, toDate) {
143
+ const SEP = "─".repeat(72);
144
+ console.log(c.bold(`Recent Work — last ${days} days (${fromDate} → ${toDate})`));
145
+ console.log(SEP);
146
+ console.log(` ${"TASK".padEnd(24)} ${"TITLE".padEnd(40)} ${"STATUS".padEnd(12)} SCORE`);
147
+ console.log(SEP);
148
+ for (const t of tasks) {
149
+ const title = truncate(t.title || t.feature, 40).padEnd(40);
150
+ const status = t.status.padEnd(12);
151
+ const score = t.score ?? "—";
152
+ const row = ` ${t.id.padEnd(24)} ${title} ${status} ${score}`;
153
+ console.log(colourStatus(t.status, row));
154
+ }
155
+ console.log(SEP);
156
+ if (allCommits.length > 0) {
157
+ console.log("");
158
+ console.log(c.bold(`Git Commits (${allCommits.length} matching)`));
159
+ for (const line of allCommits) {
160
+ console.log(` ${line}`);
161
+ }
162
+ }
163
+ if (wikiEntries.length > 0) {
164
+ console.log("");
165
+ console.log(c.bold(`Wiki Activity (last ${days} days)`));
166
+ for (const e of wikiEntries) {
167
+ console.log(` ${e.date} ${e.operation} ${e.file}`);
168
+ }
169
+ }
170
+ }
171
+ function renderMarkdown(tasks, allCommits, wikiEntries, days, fromDate, toDate) {
172
+ console.log(`# Recent Work — last ${days} days (${fromDate} → ${toDate})`);
173
+ console.log("");
174
+ console.log("| TASK | TITLE | STATUS | SCORE |");
175
+ console.log("|------|-------|--------|-------|");
176
+ for (const t of tasks) {
177
+ const title = truncate(t.title || t.feature, 40);
178
+ const score = t.score ?? "—";
179
+ console.log(`| ${t.id} | ${title} | ${t.status} | ${score} |`);
180
+ }
181
+ if (allCommits.length > 0) {
182
+ console.log("");
183
+ console.log(`## Git Commits (${allCommits.length} matching)`);
184
+ console.log("");
185
+ for (const line of allCommits) {
186
+ console.log(`- ${line}`);
187
+ }
188
+ }
189
+ if (wikiEntries.length > 0) {
190
+ console.log("");
191
+ console.log(`## Wiki Activity (last ${days} days)`);
192
+ console.log("");
193
+ console.log("| Date | Operation | File |");
194
+ console.log("|------|-----------|------|");
195
+ for (const e of wikiEntries) {
196
+ console.log(`| ${e.date} | ${e.operation} | ${e.file} |`);
197
+ }
198
+ }
199
+ }
200
+ // ── Main entry point ─────────────────────────────────────────────────────────
201
+ async function run(opts = { days: 7, format: "table" }) {
202
+ const days = Math.max(1, isNaN(opts.days) ? 7 : opts.days);
203
+ const format = opts.format ?? "table";
204
+ const isMd = format === "md";
205
+ const cwd = process.cwd();
206
+ const specsDir = node_path_1.default.join(cwd, ".devloop", "specs");
207
+ const logPath = node_path_1.default.join(cwd, ".docuflow", "log.md");
208
+ const now = new Date();
209
+ const since = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
210
+ // Collect spec files
211
+ let specFiles = [];
212
+ try {
213
+ const entries = await promises_1.default.readdir(specsDir);
214
+ specFiles = entries
215
+ .filter(f => /^TASK-\d{8}-\d{6}\.md$/.test(f))
216
+ .map(f => node_path_1.default.join(specsDir, f));
217
+ }
218
+ catch {
219
+ console.log(`No tasks found in the last ${days} days.`);
220
+ return;
221
+ }
222
+ // Filter by mtime and parse each spec
223
+ const tasks = [];
224
+ for (const specPath of specFiles) {
225
+ let stat;
226
+ try {
227
+ stat = await promises_1.default.stat(specPath);
228
+ }
229
+ catch {
230
+ continue;
231
+ }
232
+ if (stat.mtime < since)
233
+ continue;
234
+ const stem = node_path_1.default.basename(specPath, ".md"); // TASK-YYYYMMDD-HHMMSS
235
+ const specFields = await parseSpec(specPath);
236
+ if (!specFields.title && !specFields.feature)
237
+ continue; // unreadable
238
+ const reviewPath = node_path_1.default.join(specsDir, `${stem}-review.md`);
239
+ const { score, reviewVerdict } = await parseReview(reviewPath);
240
+ // Override status from review verdict if present
241
+ let status = specFields.status ?? "pending";
242
+ if (reviewVerdict === "APPROVED")
243
+ status = "approved";
244
+ else if (reviewVerdict === "NEEDS_WORK")
245
+ status = "needs-work";
246
+ else if (reviewVerdict === "REJECTED")
247
+ status = "rejected";
248
+ const commits = gitLog(stem);
249
+ tasks.push({
250
+ id: stem,
251
+ title: specFields.title ?? "",
252
+ feature: specFields.feature ?? "",
253
+ status,
254
+ score,
255
+ reviewVerdict,
256
+ commits,
257
+ specMtime: stat.mtime,
258
+ });
259
+ }
260
+ // Sort newest first
261
+ tasks.sort((a, b) => b.specMtime.getTime() - a.specMtime.getTime());
262
+ if (tasks.length === 0) {
263
+ console.log(`No tasks found in the last ${days} days.`);
264
+ return;
265
+ }
266
+ const fromDate = formatDateYMD(since);
267
+ const toDate = formatDateYMD(now);
268
+ const allCommits = tasks.flatMap(t => t.commits);
269
+ const wikiEntries = await parseWikiLog(logPath, since);
270
+ if (isMd) {
271
+ renderMarkdown(tasks, allCommits, wikiEntries, days, fromDate, toDate);
272
+ }
273
+ else {
274
+ renderTable(tasks, allCommits, wikiEntries, days, fromDate, toDate);
275
+ }
276
+ }