workflow-ledger 0.3.5 → 0.3.8

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.
@@ -2,10 +2,21 @@
2
2
  const fs = require('fs');
3
3
  const os = require('os');
4
4
  const path = require('path');
5
+ const readline = require('readline/promises');
6
+ const { stdin: input, stdout: output } = require('process');
5
7
  const { spawnSync } = require('child_process');
6
8
 
7
9
  const repoRoot = path.resolve(__dirname, '..');
8
- const targetRoot = path.resolve(process.env.WORKFLOW_LEDGER_ROOT || process.cwd());
10
+
11
+ function defaultTargetRoot() {
12
+ if (process.env.WORKFLOW_LEDGER_ROOT) return path.resolve(process.env.WORKFLOW_LEDGER_ROOT);
13
+ if (path.basename(__dirname) === 'bin' && path.basename(path.dirname(__dirname)) === '.claude') {
14
+ return path.resolve(__dirname, '..', '..');
15
+ }
16
+ return process.cwd();
17
+ }
18
+
19
+ const targetRoot = defaultTargetRoot();
9
20
 
10
21
  const toolAliases = new Map([
11
22
  ['cc', 'claude-code'],
@@ -15,12 +26,23 @@ const toolAliases = new Map([
15
26
  ['all', 'all'],
16
27
  ]);
17
28
 
29
+ const languageAliases = new Map([
30
+ ['en', 'en'],
31
+ ['english', 'en'],
32
+ ['zh', 'zh-CN'],
33
+ ['zh-cn', 'zh-CN'],
34
+ ['zh_CN', 'zh-CN'],
35
+ ['cn', 'zh-CN'],
36
+ ['chinese', 'zh-CN'],
37
+ ['中文', 'zh-CN'],
38
+ ]);
39
+
18
40
  function printHelp() {
19
41
  console.log(`workflow-ledger — lightweight workflow guardrails for AI coding agents
20
42
 
21
43
  Usage:
22
44
  workflow-ledger setup [--tool claude-code|codex|all]
23
- workflow-ledger init [--tool claude-code|codex|all] [--root PATH]
45
+ workflow-ledger init [--tool claude-code|codex|all] [--lang en|zh-CN] [--root PATH]
24
46
  workflow-ledger help
25
47
  workflow-ledger doctor
26
48
  workflow-ledger list
@@ -31,14 +53,26 @@ setup installs global tool integrations. init creates project-local ledger files
31
53
  }
32
54
 
33
55
  function parseArgs(argv) {
34
- const args = { command: argv[0] || 'help', tool: 'claude-code', root: targetRoot };
56
+ const args = { command: argv[0] || 'help', tool: 'claude-code', language: '', root: targetRoot, interactiveLanguage: argv[0] === 'init' };
35
57
  for (let i = 1; i < argv.length; i += 1) {
36
58
  const arg = argv[i];
37
59
  if (arg === '--tool') {
38
60
  args.tool = argv[i + 1] || '';
61
+ args.interactiveLanguage = false;
39
62
  i += 1;
40
63
  } else if (arg.startsWith('--tool=')) {
41
64
  args.tool = arg.slice('--tool='.length);
65
+ args.interactiveLanguage = false;
66
+ } else if (arg === '--lang' || arg === '--language') {
67
+ args.language = argv[i + 1] || '';
68
+ args.interactiveLanguage = false;
69
+ i += 1;
70
+ } else if (arg.startsWith('--lang=')) {
71
+ args.language = arg.slice('--lang='.length);
72
+ args.interactiveLanguage = false;
73
+ } else if (arg.startsWith('--language=')) {
74
+ args.language = arg.slice('--language='.length);
75
+ args.interactiveLanguage = false;
42
76
  } else if (arg === '--root') {
43
77
  args.root = path.resolve(argv[i + 1] || '.');
44
78
  i += 1;
@@ -47,6 +81,7 @@ function parseArgs(argv) {
47
81
  }
48
82
  }
49
83
  args.tool = toolAliases.get(args.tool) || args.tool;
84
+ args.language = args.language ? languageAliases.get(args.language) || args.language : 'en';
50
85
  return args;
51
86
  }
52
87
 
@@ -78,6 +113,12 @@ function copyDir(src, dest) {
78
113
  fs.cpSync(src, dest, { recursive: true });
79
114
  }
80
115
 
116
+ function copyCli(dest) {
117
+ ensureDir(path.dirname(dest));
118
+ fs.copyFileSync(__filename, dest);
119
+ fs.chmodSync(dest, 0o755);
120
+ }
121
+
81
122
  function appendSnippet(marker, snippetPath, targetPath, updatedMessage, keptMessage, result) {
82
123
  let current = '';
83
124
  if (fs.existsSync(targetPath)) {
@@ -103,6 +144,32 @@ function validateTool(tool) {
103
144
  return true;
104
145
  }
105
146
 
147
+ function validateLanguage(language) {
148
+ if (!['en', 'zh-CN'].includes(language)) {
149
+ console.error(`error: unknown language '${language}'. Expected en or zh-CN.`);
150
+ process.exitCode = 1;
151
+ return false;
152
+ }
153
+ return true;
154
+ }
155
+
156
+ async function chooseLanguage() {
157
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return 'en';
158
+ const rl = readline.createInterface({ input, output });
159
+ try {
160
+ const answer = await rl.question('Choose language / 选择语言 [1] English [2] 简体中文: ');
161
+ const normalized = answer.trim().toLowerCase();
162
+ if (['2', 'zh', 'zh-cn', 'cn', '中文'].includes(normalized)) return 'zh-CN';
163
+ return 'en';
164
+ } finally {
165
+ rl.close();
166
+ }
167
+ }
168
+
169
+ function localizedPath(...segments) {
170
+ return path.join(repoRoot, ...segments);
171
+ }
172
+
106
173
  function createResult() {
107
174
  return { configured: [], skipped: [], errors: [] };
108
175
  }
@@ -134,9 +201,7 @@ function setupClaudeCodeGlobal(result) {
134
201
  const binDir = path.join(claudeDir, 'bin');
135
202
  try {
136
203
  copyDir(path.join(repoRoot, 'skills', 'workflow-ledger'), path.join(skillsDir, 'workflow-ledger'));
137
- ensureDir(binDir);
138
- fs.copyFileSync(path.join(repoRoot, 'bin', 'workflow-ledger'), path.join(binDir, 'workflow-ledger'));
139
- fs.chmodSync(path.join(binDir, 'workflow-ledger'), 0o755);
204
+ copyCli(path.join(binDir, 'workflow-ledger'));
140
205
  result.configured.push('Claude Code skill → ~/.claude/skills/workflow-ledger');
141
206
  result.configured.push('Claude Code local CLI → ~/.claude/bin/workflow-ledger');
142
207
  } catch (error) {
@@ -169,11 +234,19 @@ function setup(args) {
169
234
  console.log('\nNext: run workflow-ledger init in a project.');
170
235
  }
171
236
 
172
- function initClaudeCodeProject(root, result) {
237
+ function templateFile(language, englishPath, chinesePath) {
238
+ return language === 'zh-CN' ? localizedPath(...chinesePath) : localizedPath(...englishPath);
239
+ }
240
+
241
+ function initClaudeCodeProject(root, language, result) {
173
242
  const claudeDir = path.join(root, '.claude');
174
243
  ensureDir(claudeDir);
175
244
  copyFileIfMissing(
176
- path.join(repoRoot, 'skills', 'workflow-ledger', 'templates', 'WORKFLOW.md'),
245
+ templateFile(
246
+ language,
247
+ ['skills', 'workflow-ledger', 'templates', 'WORKFLOW.md'],
248
+ ['skills', 'workflow-ledger', 'templates', 'WORKFLOW.zh-CN.md']
249
+ ),
177
250
  path.join(claudeDir, 'WORKFLOW.md'),
178
251
  'created .claude/WORKFLOW.md',
179
252
  'kept existing .claude/WORKFLOW.md',
@@ -181,7 +254,11 @@ function initClaudeCodeProject(root, result) {
181
254
  );
182
255
  appendSnippet(
183
256
  '## Workflow Ledger',
184
- path.join(repoRoot, 'examples', 'claude-project', 'CLAUDE.md.snippet'),
257
+ templateFile(
258
+ language,
259
+ ['examples', 'claude-project', 'CLAUDE.md.snippet'],
260
+ ['examples', 'claude-project', 'CLAUDE.zh-CN.md.snippet']
261
+ ),
185
262
  path.join(root, 'CLAUDE.md'),
186
263
  'updated CLAUDE.md',
187
264
  'kept existing Workflow Ledger section in CLAUDE.md',
@@ -189,11 +266,15 @@ function initClaudeCodeProject(root, result) {
189
266
  );
190
267
  }
191
268
 
192
- function initCodexProject(root, result) {
269
+ function initCodexProject(root, language, result) {
193
270
  const ledgerDir = path.join(root, '.workflow-ledger');
194
271
  ensureDir(ledgerDir);
195
272
  copyFileIfMissing(
196
- path.join(repoRoot, 'templates', 'WORKFLOW.md'),
273
+ templateFile(
274
+ language,
275
+ ['templates', 'WORKFLOW.md'],
276
+ ['templates', 'WORKFLOW.zh-CN.md']
277
+ ),
197
278
  path.join(ledgerDir, 'WORKFLOW.md'),
198
279
  'created .workflow-ledger/WORKFLOW.md',
199
280
  'kept existing .workflow-ledger/WORKFLOW.md',
@@ -201,7 +282,11 @@ function initCodexProject(root, result) {
201
282
  );
202
283
  appendSnippet(
203
284
  '# Workflow Ledger',
204
- path.join(repoRoot, 'examples', 'codex-project', 'AGENTS.md.snippet'),
285
+ templateFile(
286
+ language,
287
+ ['examples', 'codex-project', 'AGENTS.md.snippet'],
288
+ ['examples', 'codex-project', 'AGENTS.zh-CN.md.snippet']
289
+ ),
205
290
  path.join(root, 'AGENTS.md'),
206
291
  'updated AGENTS.md',
207
292
  'kept existing Workflow Ledger section in AGENTS.md',
@@ -209,31 +294,463 @@ function initCodexProject(root, result) {
209
294
  );
210
295
  }
211
296
 
212
- function initProject(args) {
297
+ async function initProject(args) {
213
298
  if (!validateTool(args.tool)) return;
299
+ if (!validateLanguage(args.language)) return;
300
+ const language = args.interactiveLanguage ? await chooseLanguage() : args.language;
214
301
  const result = createResult();
215
302
  ensureDir(args.root);
216
- if (args.tool === 'claude-code' || args.tool === 'all') initClaudeCodeProject(args.root, result);
217
- if (args.tool === 'codex' || args.tool === 'all') initCodexProject(args.root, result);
303
+ if (args.tool === 'claude-code' || args.tool === 'all') initClaudeCodeProject(args.root, language, result);
304
+ if (args.tool === 'codex' || args.tool === 'all') initCodexProject(args.root, language, result);
218
305
  printResult('Workflow Ledger Init', result);
219
306
  }
220
307
 
221
- function delegateToBash(argv) {
222
- const script = path.join(repoRoot, 'bin', 'workflow-ledger');
223
- const result = spawnSync(script, argv, {
224
- stdio: 'inherit',
225
- env: { ...process.env, WORKFLOW_LEDGER_ROOT: process.env.WORKFLOW_LEDGER_ROOT || targetRoot },
226
- });
227
- process.exitCode = result.status ?? 1;
308
+ function ledgerPath(root = targetRoot) {
309
+ return path.join(root, '.claude', 'WORKFLOW.md');
310
+ }
311
+
312
+ function hooksJsonPath(root = targetRoot) {
313
+ return path.join(root, '.claude', 'hooks', 'hooks.json');
314
+ }
315
+
316
+ function hookScriptPath(root = targetRoot) {
317
+ return path.join(root, '.claude', 'hooks', 'session-start');
318
+ }
319
+
320
+ function isExecutable(filePath) {
321
+ try {
322
+ fs.accessSync(filePath, fs.constants.X_OK);
323
+ return true;
324
+ } catch {
325
+ return false;
326
+ }
327
+ }
328
+
329
+ function hookStatusValue(root = targetRoot) {
330
+ const hooksJson = hooksJsonPath(root);
331
+ const hookScript = hookScriptPath(root);
332
+ if (!fs.existsSync(hooksJson) || !fs.existsSync(hookScript)) return 'not installed';
333
+ const hooksText = fs.readFileSync(hooksJson, 'utf8');
334
+ if (hooksText.includes('SessionStart') && hooksText.includes('.claude/hooks/session-start') && isExecutable(hookScript)) {
335
+ return 'installed';
336
+ }
337
+ return 'incomplete';
338
+ }
339
+
340
+ function hasSection(lines, name) {
341
+ return lines.some((line) => new RegExp(`^##\\s+${name}\\s*$`).test(line));
342
+ }
343
+
344
+ function formatLocalTimestamp(epochSeconds) {
345
+ const date = new Date(epochSeconds * 1000);
346
+ const pad = (value) => String(value).padStart(2, '0');
347
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
348
+ }
349
+
350
+ function cmdDoctor() {
351
+ const ledger = ledgerPath();
352
+ let errorCount = 0;
353
+ let warningCount = 0;
354
+ const out = [];
355
+
356
+ const sayError = (message) => {
357
+ errorCount += 1;
358
+ out.push(`ERROR: ${message}`);
359
+ };
360
+ const sayWarning = (message) => {
361
+ warningCount += 1;
362
+ out.push(`WARNING: ${message}`);
363
+ };
364
+ const sayInfo = (message) => out.push(`INFO: ${message}`);
365
+
366
+ if (!fs.existsSync(ledger)) {
367
+ sayError('.claude/WORKFLOW.md is missing.');
368
+ console.log(out.join('\n'));
369
+ process.exitCode = 1;
370
+ return;
371
+ }
372
+
373
+ let text = '';
374
+ try {
375
+ text = fs.readFileSync(ledger, 'utf8');
376
+ } catch {
377
+ sayError('.claude/WORKFLOW.md exists but cannot be read.');
378
+ console.log(out.join('\n'));
379
+ process.exitCode = 1;
380
+ return;
381
+ }
382
+
383
+ const lines = text.split(/\r?\n/);
384
+ if (!hasSection(lines, 'Active')) sayError('Missing ## Active section.');
385
+ if (!hasSection(lines, 'Backlog / Future')) sayError('Missing ## Backlog / Future section.');
386
+ if (!hasSection(lines, 'Completed')) sayError('Missing ## Completed section.');
387
+
388
+ let activeCount = 0;
389
+ let backlogItems = 0;
390
+ let completedItems = 0;
391
+ let inActive = false;
392
+ let inBacklog = false;
393
+ let inCompleted = false;
394
+ let task = null;
395
+
396
+ const finishTask = () => {
397
+ if (!task) return;
398
+ if (task.status === 'In Progress') {
399
+ if (!task.currentPhase) sayError(`In Progress task '${task.title}' lacks Current phase.`);
400
+ if (!task.hasIntent) sayError(`In Progress task '${task.title}' lacks Intent.`);
401
+ if (!task.hasTodo) sayError(`In Progress task '${task.title}' lacks Current todo.`);
402
+ if (!task.hasResume) sayError(`In Progress task '${task.title}' lacks Resume next.`);
403
+ if (task.level === '2' || task.level === '3') {
404
+ if (!task.hasChanges) sayWarning(`Level ${task.level} task '${task.title}' lacks Changes.`);
405
+ if (!task.hasPrerequisites) sayWarning(`Level ${task.level} task '${task.title}' lacks Prerequisites.`);
406
+ }
407
+ }
408
+ if (task.status === 'Blocked') {
409
+ if (!task.hasBlockedBy) sayError(`Blocked task '${task.title}' lacks Blocked by.`);
410
+ if (!task.hasResume) sayError(`Blocked task '${task.title}' lacks Resume next.`);
411
+ }
412
+ if (task.status === 'Done' || task.status === 'Completed') {
413
+ if (!task.hasCloseSummary) sayWarning(`Completed task '${task.title}' is still under Active and lacks Close summary. Move it to ## Completed when closing.`);
414
+ }
415
+ if (task.lineCount > 80) sayWarning(`Task '${task.title}' has more than 80 lines.`);
416
+ };
417
+
418
+ for (const line of lines) {
419
+ if (line === '## Active') {
420
+ finishTask();
421
+ inActive = true;
422
+ inBacklog = false;
423
+ inCompleted = false;
424
+ task = null;
425
+ continue;
426
+ }
427
+ if (line === '## Backlog / Future') {
428
+ finishTask();
429
+ inActive = false;
430
+ inBacklog = true;
431
+ inCompleted = false;
432
+ task = null;
433
+ continue;
434
+ }
435
+ if (line === '## Completed') {
436
+ finishTask();
437
+ inActive = false;
438
+ inBacklog = false;
439
+ inCompleted = true;
440
+ task = null;
441
+ continue;
442
+ }
443
+ if (line.startsWith('## ')) {
444
+ finishTask();
445
+ inActive = false;
446
+ inBacklog = false;
447
+ inCompleted = false;
448
+ task = null;
449
+ }
450
+
451
+ if (inBacklog && /^-\s+(\[[ xX]\]\s+)?/.test(line)) backlogItems += 1;
452
+ if (inCompleted && /^###\s+/.test(line)) completedItems += 1;
453
+
454
+ if (!inActive) continue;
455
+ if (task) task.lineCount += 1;
456
+ const titleMatch = line.match(/^###\s+(.+)/);
457
+ if (titleMatch) {
458
+ finishTask();
459
+ activeCount += 1;
460
+ task = {
461
+ title: titleMatch[1],
462
+ status: '',
463
+ level: '',
464
+ currentPhase: '',
465
+ hasIntent: false,
466
+ hasTodo: false,
467
+ hasChanges: false,
468
+ hasPrerequisites: false,
469
+ hasResume: false,
470
+ hasBlockedBy: false,
471
+ hasCloseSummary: false,
472
+ lineCount: 1,
473
+ };
474
+ continue;
475
+ }
476
+ if (!task) continue;
477
+ const statusMatch = line.match(/^Status:\s*(.+)/);
478
+ if (statusMatch && !task.status) task.status = statusMatch[1];
479
+ const levelMatch = line.match(/^Level:\s*([0-3])/);
480
+ if (levelMatch) task.level = levelMatch[1];
481
+ const currentMatch = line.match(/^Current\s+phase:\s*(.+)/);
482
+ if (currentMatch) task.currentPhase = currentMatch[1];
483
+ if (/^Intent:/.test(line)) task.hasIntent = true;
484
+ if (/^Current\s+todo:/.test(line)) task.hasTodo = true;
485
+ if (/^Changes:/.test(line)) task.hasChanges = true;
486
+ if (/^Prerequisites:/.test(line)) task.hasPrerequisites = true;
487
+ if (/^Blocked\s+by:/.test(line)) task.hasBlockedBy = true;
488
+ if (/^Resume\s+next:/.test(line)) task.hasResume = true;
489
+ if (/^Close\s+summary:/.test(line)) task.hasCloseSummary = true;
490
+ }
491
+ finishTask();
492
+
493
+ if (backlogItems > 10) sayWarning('Backlog / Future contains more than 10 items.');
494
+ if (activeCount > 1) sayWarning('More than one Active task; include priority, blocker state, and Resume next if this is intentional.');
495
+
496
+ sayInfo(`Active tasks: ${activeCount}`);
497
+ sayInfo(`Backlog items: ${backlogItems}`);
498
+ sayInfo(`Completed tasks: ${completedItems}`);
499
+
500
+ let ledgerMtime = 0;
501
+ try {
502
+ ledgerMtime = Math.floor(fs.statSync(ledger).mtimeMs / 1000);
503
+ sayInfo(`Ledger modified: ${formatLocalTimestamp(ledgerMtime)}`);
504
+ } catch {
505
+ ledgerMtime = 0;
506
+ }
507
+
508
+ const inGit = spawnSync('git', ['-C', targetRoot, 'rev-parse', '--is-inside-work-tree'], { encoding: 'utf8' });
509
+ if (inGit.status === 0) {
510
+ const latest = spawnSync('git', ['-C', targetRoot, 'log', '-1', '--format=%ct'], { encoding: 'utf8' });
511
+ const commitTime = Number(latest.stdout.trim());
512
+ if (latest.status === 0 && Number.isFinite(commitTime) && commitTime > 0) {
513
+ sayInfo(`Latest git commit: ${formatLocalTimestamp(commitTime)}`);
514
+ if (ledgerMtime > 0 && ledgerMtime < commitTime) sayWarning('Ledger modified time is older than latest git commit.');
515
+ }
516
+ }
517
+
518
+ sayInfo(`Hooks: ${hookStatusValue()}`);
519
+ if (errorCount > 0) {
520
+ out.push(`doctor finished with ${errorCount} error(s), ${warningCount} warning(s).`);
521
+ process.exitCode = 1;
522
+ } else {
523
+ out.push(`doctor finished with 0 errors, ${warningCount} warning(s).`);
524
+ }
525
+ console.log(out.join('\n'));
526
+ }
527
+
528
+ function cmdList() {
529
+ const ledger = ledgerPath();
530
+ if (!fs.existsSync(ledger)) {
531
+ console.error('No .claude/WORKFLOW.md found; no tasks to list.');
532
+ return;
533
+ }
534
+
535
+ let text = '';
536
+ try {
537
+ text = fs.readFileSync(ledger, 'utf8');
538
+ } catch {
539
+ console.error('error: .claude/WORKFLOW.md exists but cannot be read.');
540
+ process.exitCode = 1;
541
+ return;
542
+ }
543
+
544
+ const lines = text.split(/\r?\n/);
545
+ const out = ['Active:'];
546
+ let inActive = false;
547
+ let inBacklog = false;
548
+ let inCompleted = false;
549
+ let inResume = false;
550
+ let backlogItems = 0;
551
+ let completedItems = 0;
552
+ let currentTask = '';
553
+ let status = '';
554
+ let level = '';
555
+ let currentPhase = '';
556
+ let resumeNext = '';
557
+
558
+ const printTask = () => {
559
+ if (!currentTask) return;
560
+ let meta = '';
561
+ if (level) meta = `[Level ${level}]`;
562
+ if (status) meta = `${meta} ${status}`;
563
+ out.push(`- ${currentTask} ${meta}`);
564
+ if (currentPhase) out.push(` Current phase: ${currentPhase}`);
565
+ if (resumeNext) out.push(` Resume next: ${resumeNext}`);
566
+ };
567
+
568
+ for (const line of lines) {
569
+ if (line === '## Active') {
570
+ inActive = true;
571
+ inBacklog = false;
572
+ inCompleted = false;
573
+ inResume = false;
574
+ continue;
575
+ }
576
+ if (line === '## Backlog / Future') {
577
+ printTask();
578
+ currentTask = '';
579
+ inActive = false;
580
+ inBacklog = true;
581
+ inCompleted = false;
582
+ inResume = false;
583
+ continue;
584
+ }
585
+ if (line === '## Completed') {
586
+ printTask();
587
+ currentTask = '';
588
+ inActive = false;
589
+ inBacklog = false;
590
+ inCompleted = true;
591
+ inResume = false;
592
+ continue;
593
+ }
594
+ if (line.startsWith('## ')) {
595
+ printTask();
596
+ currentTask = '';
597
+ inActive = false;
598
+ inBacklog = false;
599
+ inCompleted = false;
600
+ inResume = false;
601
+ }
602
+
603
+ if (inActive) {
604
+ const titleMatch = line.match(/^###\s+(.+)/);
605
+ if (titleMatch) {
606
+ printTask();
607
+ currentTask = titleMatch[1];
608
+ status = '';
609
+ level = '';
610
+ currentPhase = '';
611
+ resumeNext = '';
612
+ inResume = false;
613
+ } else {
614
+ const statusMatch = line.match(/^Status:\s*(.+)/);
615
+ const levelMatch = line.match(/^Level:\s*([0-3])/);
616
+ const currentMatch = line.match(/^Current\s+phase:\s*(.+)/);
617
+ if (statusMatch && !status) {
618
+ status = statusMatch[1];
619
+ inResume = false;
620
+ } else if (levelMatch) {
621
+ level = levelMatch[1];
622
+ inResume = false;
623
+ } else if (currentMatch) {
624
+ currentPhase = currentMatch[1];
625
+ inResume = false;
626
+ } else if (/^Resume\s+next:/.test(line)) {
627
+ inResume = true;
628
+ } else if (inResume && /^-\s+(.+)/.test(line)) {
629
+ if (!resumeNext) resumeNext = line.replace(/^-\s+/, '');
630
+ } else if (line.trim()) {
631
+ inResume = false;
632
+ }
633
+ }
634
+ } else if (inBacklog && /^-\s+(\[[ xX]\]\s+)?/.test(line)) {
635
+ backlogItems += 1;
636
+ } else if (inCompleted && /^###\s+/.test(line)) {
637
+ completedItems += 1;
638
+ }
639
+ }
640
+ printTask();
641
+ out.push('', 'Backlog / Future:', `- ${backlogItems} items`, '', 'Completed:', `- ${completedItems} items`);
642
+ console.log(out.join('\n'));
643
+ }
644
+
645
+ function cmdHooksStatus() {
646
+ console.log(`hooks: ${hookStatusValue()}`);
647
+ console.log(`hooks.json: ${hooksJsonPath()}`);
648
+ console.log(`session-start: ${hookScriptPath()}`);
649
+ }
650
+
651
+ const DEFAULT_HOOKS_JSON = `{
652
+ "hooks": {
653
+ "SessionStart": [
654
+ {
655
+ "matcher": "",
656
+ "command": ".claude/hooks/session-start"
657
+ }
658
+ ]
659
+ }
660
+ }
661
+ `;
662
+
663
+ const DEFAULT_SESSION_START_HOOK = `#!/usr/bin/env bash
664
+ set -u
665
+
666
+ ledger=".claude/WORKFLOW.md"
667
+ cli=".claude/bin/workflow-ledger"
668
+
669
+ if [ ! -f "$ledger" ]; then
670
+ exit 0
671
+ fi
672
+
673
+ if [ -n "\${CLAUDE_PLUGIN_ROOT:-}" ]; then
674
+ cat <<'PLUGIN_JSON'
675
+ {"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"Workflow Ledger detected.\\n- Read .claude/WORKFLOW.md before resuming tracked work.\\n- Check Active tasks, Current phase/current focus, Current todo, and Resume next.\\n- Run workflow-ledger doctor if state may be stale."}}
676
+ PLUGIN_JSON
677
+ exit 0
678
+ fi
679
+
680
+ printf 'Workflow Ledger detected.\\n'
681
+ printf -- '- Read .claude/WORKFLOW.md before resuming tracked work.\\n'
682
+ printf -- '- Check Active tasks, Current phase/current focus, Current todo, and Resume next.\\n'
683
+
684
+ if [ -x "$cli" ]; then
685
+ printf -- '- Run .claude/bin/workflow-ledger doctor if state may be stale.\\n'
686
+ else
687
+ printf -- '- Run workflow-ledger doctor if the project CLI is available.\\n'
688
+ fi
689
+
690
+ exit 0
691
+ `;
692
+
693
+ function cmdHooksInstall() {
694
+ const targetDir = path.join(targetRoot, '.claude', 'hooks');
695
+ try {
696
+ ensureDir(targetDir);
697
+ } catch {
698
+ console.error('error: cannot create .claude/hooks directory.');
699
+ process.exitCode = 1;
700
+ return;
701
+ }
702
+
703
+ const hooksJson = hooksJsonPath();
704
+ const hookScript = hookScriptPath();
705
+ if (fs.existsSync(hooksJson)) {
706
+ console.log('kept existing .claude/hooks/hooks.json');
707
+ } else {
708
+ fs.writeFileSync(hooksJson, DEFAULT_HOOKS_JSON);
709
+ console.log('installed .claude/hooks/hooks.json');
710
+ }
711
+
712
+ if (fs.existsSync(hookScript)) {
713
+ console.log('kept existing .claude/hooks/session-start');
714
+ } else {
715
+ fs.writeFileSync(hookScript, DEFAULT_SESSION_START_HOOK, { mode: 0o755 });
716
+ fs.chmodSync(hookScript, 0o755);
717
+ console.log('installed .claude/hooks/session-start');
718
+ }
719
+ }
720
+
721
+ function runCommand(argv) {
722
+ const command = argv[0] || 'help';
723
+ if (command === 'help' || command === '-h' || command === '--help') {
724
+ printHelp();
725
+ } else if (command === 'setup') {
726
+ setup(args);
727
+ } else if (command === 'init') {
728
+ return initProject(args);
729
+ } else if (command === 'doctor') {
730
+ cmdDoctor();
731
+ } else if (command === 'list') {
732
+ cmdList();
733
+ } else if (command === 'hooks') {
734
+ const subcommand = argv[1] || 'status';
735
+ if (subcommand === 'status') cmdHooksStatus();
736
+ else if (subcommand === 'install') cmdHooksInstall();
737
+ else {
738
+ console.error(`error: unknown hooks command: ${subcommand}`);
739
+ process.exitCode = 1;
740
+ }
741
+ } else {
742
+ console.error(`error: unknown command: ${command}\n`);
743
+ printHelp();
744
+ process.exitCode = 1;
745
+ }
228
746
  }
229
747
 
230
748
  const args = parseArgs(process.argv.slice(2));
231
- if (args.command === 'help' || args.command === '-h' || args.command === '--help') {
232
- printHelp();
233
- } else if (args.command === 'setup') {
234
- setup(args);
235
- } else if (args.command === 'init') {
236
- initProject(args);
237
- } else {
238
- delegateToBash(process.argv.slice(2));
239
- }
749
+ async function main() {
750
+ await runCommand(process.argv.slice(2));
751
+ }
752
+
753
+ main().catch((error) => {
754
+ console.error(error.message);
755
+ process.exitCode = 1;
756
+ });