workflow-ledger 0.3.6 → 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.
@@ -8,7 +8,7 @@
8
8
  {
9
9
  "name": "workflow-ledger",
10
10
  "description": "Lightweight, recoverable workflow ledger for Claude Code projects",
11
- "version": "0.3.6",
11
+ "version": "0.3.8",
12
12
  "source": "./",
13
13
  "author": {
14
14
  "name": "MorseWayne"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "workflow-ledger",
3
3
  "description": "Lightweight, recoverable workflow ledger for Claude Code projects",
4
- "version": "0.3.6",
4
+ "version": "0.3.8",
5
5
  "author": {
6
6
  "name": "MorseWayne"
7
7
  },
package/README.md CHANGED
@@ -94,6 +94,8 @@ npx workflow-ledger setup --tool codex
94
94
  npx workflow-ledger setup --tool all
95
95
  ```
96
96
 
97
+ `setup` only installs the integration so supported agents can find it. It does not activate Workflow Ledger in every project.
98
+
97
99
  Then initialize a project ledger from the target project root. Bare `init` asks you to choose a language first; automation can pass `--lang en` or `--lang zh-CN` to skip the prompt:
98
100
 
99
101
  ```bash
@@ -104,9 +106,9 @@ npx workflow-ledger init --tool codex --lang en
104
106
  npx workflow-ledger init --tool all --lang en
105
107
  ```
106
108
 
107
- `setup` installs global tool integrations. `init` creates project-local ledger files and short instruction snippets. `claude-code` uses `.claude/WORKFLOW.md`; `codex` uses `.workflow-ledger/WORKFLOW.md` plus `AGENTS.md`. The language choice controls newly created ledger templates and tool instruction snippets; existing files are not overwritten.
109
+ `init` is the activation step. It creates project-local ledger files and short instruction snippets. Without `init`, the skill stays dormant for ordinary development work. `claude-code` uses `.claude/WORKFLOW.md`; `codex` uses `.workflow-ledger/WORKFLOW.md` plus `AGENTS.md`. The language choice controls newly created ledger templates and tool instruction snippets; existing files are not overwritten.
108
110
 
109
- The Bash installer remains available for Claude Code project initialization:
111
+ The Bash installer remains available for Claude Code project initialization. It uses the Node.js CLI internally, so Node.js 18 or newer is required:
110
112
 
111
113
  ```bash
112
114
  curl -fsSL https://raw.githubusercontent.com/MorseWayne/workflow-ledger/main/install.sh | bash
@@ -132,11 +134,11 @@ mkdir -p ~/.claude/skills
132
134
  cp -R skills/workflow-ledger ~/.claude/skills/workflow-ledger
133
135
  ```
134
136
 
135
- The installer also copies a project-local CLI to `.claude/bin/workflow-ledger`:
137
+ The CLI can check and summarize the ledger:
136
138
 
137
139
  ```bash
138
- .claude/bin/workflow-ledger doctor
139
- .claude/bin/workflow-ledger list
140
+ npx workflow-ledger doctor
141
+ npx workflow-ledger list
140
142
  ```
141
143
 
142
144
  See [docs/cli.md](docs/cli.md) for command details. The CLI is an optional guardrail; it does not replace the skill workflow.
@@ -154,7 +156,7 @@ Then invoke it in Claude Code:
154
156
  A project needs two layers:
155
157
 
156
158
  1. Global tool setup with `workflow-ledger setup` so supported agents can find the workflow instructions.
157
- 2. Project initialization with `workflow-ledger init` so the repository has a ledger file and a short tool-specific reminder.
159
+ 2. Project initialization with `workflow-ledger init` so the repository has a ledger file and a short tool-specific reminder. This is what makes Workflow Ledger active for that project.
158
160
 
159
161
  For Claude Code, `init` adds the snippet from [examples/claude-project/CLAUDE.md.snippet](examples/claude-project/CLAUDE.md.snippet) to your project's `CLAUDE.md` and creates [skills/workflow-ledger/templates/WORKFLOW.md](skills/workflow-ledger/templates/WORKFLOW.md) at `.claude/WORKFLOW.md`.
160
162
 
@@ -191,7 +193,7 @@ Resume next:
191
193
 
192
194
  | Level | Use for | Ledger? |
193
195
  |---|---|---|
194
- | Level 0 | Q&A, read-only explanation | No |
196
+ | Level 0 | Q&A, read-only explanation, tagging or release-version publishing | No |
195
197
  | Level 1 | typo, docs tweak, tiny config, no behavior change | Optional |
196
198
  | Level 2 | standard code work, tests, single-module behavior changes | Yes |
197
199
  | Level 3 | new features, cross-module work, public APIs, unclear or high-risk changes | Yes, attachments optional |
@@ -224,4 +226,4 @@ Skip it for:
224
226
 
225
227
  ## Repository status
226
228
 
227
- This version supports both npm setup and the legacy Bash installer. The CLI can install Workflow Ledger for Claude Code, Codex, or both, while keeping the ledger format lightweight and tool-readable.
229
+ This version uses the Node.js CLI as the single implementation for setup, init, doctor, list, and hooks. The Bash installer remains as a small bootstrap wrapper for Claude Code project initialization.
package/README.zh-CN.md CHANGED
@@ -92,6 +92,8 @@ npx workflow-ledger setup --tool codex
92
92
  npx workflow-ledger setup --tool all
93
93
  ```
94
94
 
95
+ `setup` 只安装工具接入,让支持的 agent 能找到它;它不会让 Workflow Ledger 在所有项目里自动生效。
96
+
95
97
  然后在目标项目根目录初始化 ledger。裸 `init` 会先交互式选择语言;自动化脚本可传 `--lang en` 或 `--lang zh-CN` 跳过交互:
96
98
 
97
99
  ```bash
@@ -102,9 +104,9 @@ npx workflow-ledger init --tool codex --lang zh-CN
102
104
  npx workflow-ledger init --tool all --lang zh-CN
103
105
  ```
104
106
 
105
- `setup` 安装全局工具接入;`init` 创建项目本地 ledger 和短指令片段。`claude-code` 使用 `.claude/WORKFLOW.md`;`codex` 使用 `.workflow-ledger/WORKFLOW.md` 和 `AGENTS.md`。语言选择会影响新创建的 ledger 模板和工具指令片段;已有文件不会被覆盖。
107
+ `init` 是启用步骤。它创建项目本地 ledger 和短指令片段。没有 `init` 时,本 skill 对普通开发任务保持 dormant。`claude-code` 使用 `.claude/WORKFLOW.md`;`codex` 使用 `.workflow-ledger/WORKFLOW.md` 和 `AGENTS.md`。语言选择会影响新创建的 ledger 模板和工具指令片段;已有文件不会被覆盖。
106
108
 
107
- Bash 安装器仍可用于 Claude Code 项目初始化:
109
+ Bash 安装器仍可用于 Claude Code 项目初始化。它内部使用 Node.js CLI,因此需要 Node.js 18 或更高版本:
108
110
 
109
111
  ```bash
110
112
  curl -fsSL https://raw.githubusercontent.com/MorseWayne/workflow-ledger/main/install.sh | bash
@@ -130,11 +132,11 @@ mkdir -p ~/.claude/skills
130
132
  cp -R skills/workflow-ledger ~/.claude/skills/workflow-ledger
131
133
  ```
132
134
 
133
- 安装器也会把项目本地 CLI 复制到 `.claude/bin/workflow-ledger`:
135
+ CLI 可以检查和汇总 ledger
134
136
 
135
137
  ```bash
136
- .claude/bin/workflow-ledger doctor
137
- .claude/bin/workflow-ledger list
138
+ npx workflow-ledger doctor
139
+ npx workflow-ledger list
138
140
  ```
139
141
 
140
142
  详见 [docs/cli.md](docs/cli.md)。CLI 是可选保护栏,不替代 skill 工作流。
@@ -152,7 +154,7 @@ cp -R skills/workflow-ledger ~/.claude/skills/workflow-ledger
152
154
  项目接入分两层:
153
155
 
154
156
  1. 先用 `workflow-ledger setup` 做全局工具接入,让支持的 agent 能找到工作流指令。
155
- 2. 再用 `workflow-ledger init` 初始化项目,让仓库里有 ledger 文件和短工具提醒。
157
+ 2. 再用 `workflow-ledger init` 初始化项目,让仓库里有 ledger 文件和短工具提醒。这一步才会让 Workflow Ledger 在该项目生效。
156
158
 
157
159
  对 Claude Code,`init` 会把 [examples/claude-project/CLAUDE.md.snippet](examples/claude-project/CLAUDE.md.snippet) 加入项目 `CLAUDE.md`,并把 [skills/workflow-ledger/templates/WORKFLOW.md](skills/workflow-ledger/templates/WORKFLOW.md) 创建到 `.claude/WORKFLOW.md`。
158
160
 
@@ -189,7 +191,7 @@ Resume next:
189
191
 
190
192
  | Level | 适用场景 | 是否写 ledger |
191
193
  |---|---|---|
192
- | Level 0 | 问答、只读解释 | 不需要 |
194
+ | Level 0 | 问答、只读解释、新增 tag 或发布版本 | 不需要 |
193
195
  | Level 1 | typo、文档小改、小配置、无行为变化 | 可选 |
194
196
  | Level 2 | 标准代码修改、测试、单模块行为变更 | 需要 |
195
197
  | Level 3 | 新功能、跨模块、公共 API、不明确或高风险变更 | 需要,可选附件 |
@@ -222,4 +224,4 @@ Resume next:
222
224
 
223
225
  ## 当前状态
224
226
 
225
- 当前版本同时支持 npm setup 和旧的 Bash installer。CLI 可以为 Claude Code、Codex 或两者安装 Workflow Ledger,同时保持 ledger 格式轻量、可被多个工具读取。
227
+ 当前版本以 Node.js CLI 作为 setup、init、doctor、list hooks 的唯一实现。Bash 安装器保留为 Claude Code 项目初始化的轻量 bootstrap wrapper。
@@ -7,7 +7,16 @@ const { stdin: input, stdout: output } = require('process');
7
7
  const { spawnSync } = require('child_process');
8
8
 
9
9
  const repoRoot = path.resolve(__dirname, '..');
10
- 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();
11
20
 
12
21
  const toolAliases = new Map([
13
22
  ['cc', 'claude-code'],
@@ -104,6 +113,12 @@ function copyDir(src, dest) {
104
113
  fs.cpSync(src, dest, { recursive: true });
105
114
  }
106
115
 
116
+ function copyCli(dest) {
117
+ ensureDir(path.dirname(dest));
118
+ fs.copyFileSync(__filename, dest);
119
+ fs.chmodSync(dest, 0o755);
120
+ }
121
+
107
122
  function appendSnippet(marker, snippetPath, targetPath, updatedMessage, keptMessage, result) {
108
123
  let current = '';
109
124
  if (fs.existsSync(targetPath)) {
@@ -186,9 +201,7 @@ function setupClaudeCodeGlobal(result) {
186
201
  const binDir = path.join(claudeDir, 'bin');
187
202
  try {
188
203
  copyDir(path.join(repoRoot, 'skills', 'workflow-ledger'), path.join(skillsDir, 'workflow-ledger'));
189
- ensureDir(binDir);
190
- fs.copyFileSync(path.join(repoRoot, 'bin', 'workflow-ledger'), path.join(binDir, 'workflow-ledger'));
191
- fs.chmodSync(path.join(binDir, 'workflow-ledger'), 0o755);
204
+ copyCli(path.join(binDir, 'workflow-ledger'));
192
205
  result.configured.push('Claude Code skill → ~/.claude/skills/workflow-ledger');
193
206
  result.configured.push('Claude Code local CLI → ~/.claude/bin/workflow-ledger');
194
207
  } catch (error) {
@@ -292,29 +305,452 @@ async function initProject(args) {
292
305
  printResult('Workflow Ledger Init', result);
293
306
  }
294
307
 
295
- function delegateToBash(argv) {
296
- const script = path.join(repoRoot, 'bin', 'workflow-ledger');
297
- const result = spawnSync(script, argv, {
298
- stdio: 'inherit',
299
- env: { ...process.env, WORKFLOW_LEDGER_ROOT: process.env.WORKFLOW_LEDGER_ROOT || targetRoot },
300
- });
301
- process.exitCode = result.status ?? 1;
308
+ function ledgerPath(root = targetRoot) {
309
+ return path.join(root, '.claude', 'WORKFLOW.md');
302
310
  }
303
311
 
304
- const args = parseArgs(process.argv.slice(2));
305
- async function main() {
306
- if (args.command === 'help' || args.command === '-h' || args.command === '--help') {
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') {
307
724
  printHelp();
308
- } else if (args.command === 'setup') {
725
+ } else if (command === 'setup') {
309
726
  setup(args);
310
- } else if (args.command === 'init') {
311
- await initProject(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
+ }
312
741
  } else {
313
- delegateToBash(process.argv.slice(2));
742
+ console.error(`error: unknown command: ${command}\n`);
743
+ printHelp();
744
+ process.exitCode = 1;
314
745
  }
315
746
  }
316
747
 
748
+ const args = parseArgs(process.argv.slice(2));
749
+ async function main() {
750
+ await runCommand(process.argv.slice(2));
751
+ }
752
+
317
753
  main().catch((error) => {
318
754
  console.error(error.message);
319
755
  process.exitCode = 1;
320
- });
756
+ });