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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +10 -8
- package/README.zh-CN.md +10 -8
- package/bin/workflow-ledger.js +455 -19
- package/docs/cli.md +10 -10
- package/docs/design.md +1 -1
- package/docs/design.zh-CN.md +1 -1
- package/docs/usage.md +12 -9
- package/docs/usage.zh-CN.md +9 -6
- package/examples/claude-project/CLAUDE.md.snippet +3 -2
- package/examples/claude-project/CLAUDE.zh-CN.md.snippet +3 -2
- package/examples/codex-project/AGENTS.md.snippet +1 -0
- package/examples/codex-project/AGENTS.zh-CN.md.snippet +1 -0
- package/install.sh +7 -34
- package/package.json +1 -1
- package/skills/workflow-ledger/SKILL.md +11 -5
- package/bin/workflow-ledger +0 -467
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
|
-
`
|
|
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
|
|
137
|
+
The CLI can check and summarize the ledger:
|
|
136
138
|
|
|
137
139
|
```bash
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
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
|
-
`
|
|
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
|
-
|
|
135
|
+
CLI 可以检查和汇总 ledger:
|
|
134
136
|
|
|
135
137
|
```bash
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
227
|
+
当前版本以 Node.js CLI 作为 setup、init、doctor、list 和 hooks 的唯一实现。Bash 安装器保留为 Claude Code 项目初始化的轻量 bootstrap wrapper。
|
package/bin/workflow-ledger.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
296
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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 (
|
|
725
|
+
} else if (command === 'setup') {
|
|
309
726
|
setup(args);
|
|
310
|
-
} else if (
|
|
311
|
-
|
|
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
|
-
|
|
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
|
+
});
|