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.
@@ -1,467 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -u
3
-
4
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
- if [ -n "${WORKFLOW_LEDGER_ROOT:-}" ]; then
6
- ROOT="$WORKFLOW_LEDGER_ROOT"
7
- elif [ "$(basename "$SCRIPT_DIR")" = "bin" ] && [ "$(basename "$(dirname "$SCRIPT_DIR")")" = ".claude" ]; then
8
- ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
9
- else
10
- ROOT="$PWD"
11
- fi
12
- LEDGER="$ROOT/.claude/WORKFLOW.md"
13
- REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
14
-
15
- error_count=0
16
- warning_count=0
17
-
18
- print_help() {
19
- cat <<'HELP'
20
- workflow-ledger — lightweight project-local workflow guardrails
21
-
22
- Usage:
23
- workflow-ledger help
24
- workflow-ledger init [--lang en|zh-CN]
25
- workflow-ledger doctor
26
- workflow-ledger list
27
- workflow-ledger hooks status
28
- workflow-ledger hooks install
29
-
30
- Exit codes:
31
- help: always 0
32
- init: 0 on created/already exists/guidance, 1 on filesystem errors
33
- doctor: 0 when no errors, 1 when errors exist
34
- list: 0 for readable output or missing ledger, 1 when ledger exists but cannot be read
35
- hooks status: 0 when status is reported, 1 on filesystem errors
36
- hooks install: 0 when installed or no-op, 1 on filesystem errors
37
- HELP
38
- }
39
-
40
- say_error() {
41
- error_count=$((error_count + 1))
42
- printf 'ERROR: %s\n' "$1"
43
- }
44
-
45
- say_warning() {
46
- warning_count=$((warning_count + 1))
47
- printf 'WARNING: %s\n' "$1"
48
- }
49
-
50
- say_info() {
51
- printf 'INFO: %s\n' "$1"
52
- }
53
-
54
- section_exists() {
55
- local name="$1"
56
- grep -Eq "^##[[:space:]]+$name[[:space:]]*$" "$LEDGER"
57
- }
58
-
59
- hook_status_value() {
60
- local hooks_json="$ROOT/.claude/hooks/hooks.json"
61
- local hook_script="$ROOT/.claude/hooks/session-start"
62
- if [ ! -f "$hooks_json" ] || [ ! -f "$hook_script" ]; then
63
- printf 'not installed'
64
- return 0
65
- fi
66
- if grep -Fq 'SessionStart' "$hooks_json" && grep -Fq '.claude/hooks/session-start' "$hooks_json" && [ -x "$hook_script" ]; then
67
- printf 'installed'
68
- else
69
- printf 'incomplete'
70
- fi
71
- }
72
-
73
- find_template() {
74
- local language="$1"
75
- local template_name='WORKFLOW.md'
76
- if [ "$language" = 'zh-CN' ]; then
77
- template_name='WORKFLOW.zh-CN.md'
78
- fi
79
- local candidates=(
80
- "$ROOT/.claude/skills/workflow-ledger/templates/$template_name"
81
- "$REPO_ROOT/skills/workflow-ledger/templates/$template_name"
82
- )
83
- local candidate
84
- for candidate in "${candidates[@]}"; do
85
- if [ -f "$candidate" ]; then
86
- printf '%s' "$candidate"
87
- return 0
88
- fi
89
- done
90
- return 1
91
- }
92
-
93
- normalize_language() {
94
- case "$1" in
95
- en|english) printf 'en' ;;
96
- zh|zh-CN|zh-cn|zh_CN|cn|chinese|中文) printf 'zh-CN' ;;
97
- *) return 1 ;;
98
- esac
99
- }
100
-
101
- choose_language() {
102
- local answer=''
103
- if [ -t 0 ] && [ -t 1 ]; then
104
- printf 'Choose language / 选择语言 [1] English [2] 简体中文: '
105
- IFS= read -r answer || answer=''
106
- case "$answer" in
107
- 2|zh|zh-CN|zh-cn|cn|中文) printf 'zh-CN'; return 0 ;;
108
- esac
109
- fi
110
- printf 'en'
111
- }
112
-
113
- cmd_init() {
114
- local language=''
115
- while [ "$#" -gt 0 ]; do
116
- case "$1" in
117
- --lang|--language)
118
- language="${2:-}"
119
- shift 2
120
- ;;
121
- --lang=*|--language=*)
122
- language="${1#*=}"
123
- shift
124
- ;;
125
- *)
126
- shift
127
- ;;
128
- esac
129
- done
130
- if [ -n "$language" ]; then
131
- if ! language="$(normalize_language "$language")"; then
132
- printf 'error: unknown language. Expected en or zh-CN.\n' >&2
133
- return 1
134
- fi
135
- else
136
- language="$(choose_language)"
137
- fi
138
-
139
- if ! mkdir -p "$ROOT/.claude" 2>/dev/null; then
140
- printf 'error: cannot create .claude directory\n' >&2
141
- return 1
142
- fi
143
-
144
- if [ -f "$LEDGER" ]; then
145
- printf 'kept existing .claude/WORKFLOW.md\n'
146
- else
147
- local template
148
- if ! template="$(find_template "$language")"; then
149
- printf 'workflow-ledger skill template not found; run install.sh from the workflow-ledger checkout.\n' >&2
150
- return 0
151
- fi
152
- if ! cp "$template" "$LEDGER" 2>/dev/null; then
153
- printf 'error: cannot create .claude/WORKFLOW.md\n' >&2
154
- return 1
155
- fi
156
- printf 'created .claude/WORKFLOW.md\n'
157
- fi
158
-
159
- if [ ! -d "$ROOT/.claude/skills/workflow-ledger" ]; then
160
- printf 'note: .claude/skills/workflow-ledger is missing; run install.sh to install the skill.\n' >&2
161
- fi
162
- return 0
163
- }
164
-
165
- cmd_doctor() {
166
- error_count=0
167
- warning_count=0
168
-
169
- if [ ! -f "$LEDGER" ]; then
170
- say_error '.claude/WORKFLOW.md is missing.'
171
- return 1
172
- fi
173
- if [ ! -r "$LEDGER" ]; then
174
- say_error '.claude/WORKFLOW.md exists but cannot be read.'
175
- return 1
176
- fi
177
-
178
- section_exists 'Active' || say_error 'Missing ## Active section.'
179
- section_exists 'Backlog / Future' || say_error 'Missing ## Backlog / Future section.'
180
- section_exists 'Completed' || say_error 'Missing ## Completed section.'
181
-
182
- local active_count=0
183
- local backlog_items=0
184
- local completed_items=0
185
- local hook_status
186
- hook_status="$(hook_status_value)"
187
-
188
- local in_active=0 in_backlog=0 in_completed=0
189
- local task_title='' task_status='' task_level='' task_current=''
190
- local task_has_intent=0 task_has_todo=0 task_has_changes=0 task_has_prereq=0 task_has_resume=0 task_has_blocked_by=0 task_has_close_summary=0
191
- local task_line_count=0
192
-
193
- finish_task() {
194
- if [ -z "$task_title" ]; then
195
- return 0
196
- fi
197
-
198
- if [ "$task_status" = 'In Progress' ]; then
199
- [ -n "$task_current" ] || say_error "In Progress task '$task_title' lacks Current phase."
200
- [ "$task_has_intent" -eq 1 ] || say_error "In Progress task '$task_title' lacks Intent."
201
- [ "$task_has_todo" -eq 1 ] || say_error "In Progress task '$task_title' lacks Current todo."
202
- [ "$task_has_resume" -eq 1 ] || say_error "In Progress task '$task_title' lacks Resume next."
203
- if [ "$task_level" = '2' ] || [ "$task_level" = '3' ]; then
204
- [ "$task_has_changes" -eq 1 ] || say_warning "Level $task_level task '$task_title' lacks Changes."
205
- [ "$task_has_prereq" -eq 1 ] || say_warning "Level $task_level task '$task_title' lacks Prerequisites."
206
- fi
207
- fi
208
-
209
- if [ "$task_status" = 'Blocked' ]; then
210
- [ "$task_has_blocked_by" -eq 1 ] || say_error "Blocked task '$task_title' lacks Blocked by."
211
- [ "$task_has_resume" -eq 1 ] || say_error "Blocked task '$task_title' lacks Resume next."
212
- fi
213
-
214
- if [ "$task_status" = 'Done' ] || [ "$task_status" = 'Completed' ]; then
215
- [ "$task_has_close_summary" -eq 1 ] || say_warning "Completed task '$task_title' is still under Active and lacks Close summary. Move it to ## Completed when closing."
216
- fi
217
-
218
- if [ "$task_line_count" -gt 80 ]; then
219
- say_warning "Task '$task_title' has more than 80 lines."
220
- fi
221
- }
222
-
223
- while IFS= read -r line || [ -n "$line" ]; do
224
- case "$line" in
225
- '## Active')
226
- finish_task
227
- in_active=1; in_backlog=0; in_completed=0
228
- task_title=''
229
- continue
230
- ;;
231
- '## Backlog / Future')
232
- finish_task
233
- in_active=0; in_backlog=1; in_completed=0
234
- task_title=''
235
- continue
236
- ;;
237
- '## Completed')
238
- finish_task
239
- in_active=0; in_backlog=0; in_completed=1
240
- task_title=''
241
- continue
242
- ;;
243
- '## '*)
244
- finish_task
245
- in_active=0; in_backlog=0; in_completed=0
246
- task_title=''
247
- ;;
248
- esac
249
-
250
- if [ "$in_backlog" -eq 1 ] && [[ "$line" =~ ^-[[:space:]]+(\[[[:space:]xX]\][[:space:]]+)? ]]; then
251
- backlog_items=$((backlog_items + 1))
252
- fi
253
- if [ "$in_completed" -eq 1 ] && [[ "$line" =~ ^###[[:space:]]+ ]]; then
254
- completed_items=$((completed_items + 1))
255
- fi
256
-
257
- if [ "$in_active" -eq 1 ]; then
258
- if [ -n "$task_title" ]; then
259
- task_line_count=$((task_line_count + 1))
260
- fi
261
- if [[ "$line" =~ ^###[[:space:]]+(.+) ]]; then
262
- finish_task
263
- active_count=$((active_count + 1))
264
- task_title="${BASH_REMATCH[1]}"
265
- task_status=''; task_level=''; task_current=''
266
- task_has_intent=0; task_has_todo=0; task_has_changes=0; task_has_prereq=0; task_has_resume=0; task_has_blocked_by=0; task_has_close_summary=0
267
- task_line_count=1
268
- continue
269
- fi
270
- [[ "$line" =~ ^Status:[[:space:]]*(.+) ]] && [ -z "$task_status" ] && task_status="${BASH_REMATCH[1]}"
271
- [[ "$line" =~ ^Level:[[:space:]]*([0-3]) ]] && task_level="${BASH_REMATCH[1]}"
272
- [[ "$line" =~ ^Current[[:space:]]phase:[[:space:]]*(.+) ]] && task_current="${BASH_REMATCH[1]}"
273
- [[ "$line" =~ ^Intent: ]] && task_has_intent=1
274
- [[ "$line" =~ ^Current[[:space:]]todo: ]] && task_has_todo=1
275
- [[ "$line" =~ ^Changes: ]] && task_has_changes=1
276
- [[ "$line" =~ ^Prerequisites: ]] && task_has_prereq=1
277
- [[ "$line" =~ ^Blocked[[:space:]]by: ]] && task_has_blocked_by=1
278
- [[ "$line" =~ ^Resume[[:space:]]next: ]] && task_has_resume=1
279
- [[ "$line" =~ ^Close[[:space:]]summary: ]] && task_has_close_summary=1
280
- fi
281
- done < "$LEDGER"
282
- finish_task
283
-
284
- if [ "$backlog_items" -gt 10 ]; then
285
- say_warning 'Backlog / Future contains more than 10 items.'
286
- fi
287
- if [ "$active_count" -gt 1 ]; then
288
- say_warning 'More than one Active task; include priority, blocker state, and Resume next if this is intentional.'
289
- fi
290
-
291
- say_info "Active tasks: $active_count"
292
- say_info "Backlog items: $backlog_items"
293
- say_info "Completed tasks: $completed_items"
294
- if ledger_mtime="$(stat -c %Y "$LEDGER" 2>/dev/null)"; then
295
- say_info "Ledger modified: $(date -d "@$ledger_mtime" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || printf '%s' "$ledger_mtime")"
296
- fi
297
- if command -v git >/dev/null 2>&1 && git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
298
- local commit_time
299
- if commit_time="$(git -C "$ROOT" log -1 --format=%ct 2>/dev/null)" && [ -n "$commit_time" ]; then
300
- say_info "Latest git commit: $(date -d "@$commit_time" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || printf '%s' "$commit_time")"
301
- if [ -n "${ledger_mtime:-}" ] && [ "$ledger_mtime" -lt "$commit_time" ]; then
302
- say_warning 'Ledger modified time is older than latest git commit.'
303
- fi
304
- fi
305
- fi
306
- say_info "Hooks: $hook_status"
307
-
308
- if [ "$error_count" -gt 0 ]; then
309
- printf 'doctor finished with %d error(s), %d warning(s).\n' "$error_count" "$warning_count"
310
- return 1
311
- fi
312
- printf 'doctor finished with 0 errors, %d warning(s).\n' "$warning_count"
313
- return 0
314
- }
315
-
316
- cmd_list() {
317
- if [ ! -f "$LEDGER" ]; then
318
- printf 'No .claude/WORKFLOW.md found; no tasks to list.\n' >&2
319
- return 0
320
- fi
321
- if [ ! -r "$LEDGER" ]; then
322
- printf 'error: .claude/WORKFLOW.md exists but cannot be read.\n' >&2
323
- return 1
324
- fi
325
-
326
- local in_active=0 in_backlog=0 in_completed=0 in_resume=0
327
- local backlog_items=0 completed_items=0
328
- local current_task='' status='' level='' current_phase='' resume_next=''
329
-
330
- print_task() {
331
- if [ -n "$current_task" ]; then
332
- local meta=''
333
- [ -n "$level" ] && meta="[Level $level]"
334
- [ -n "$status" ] && meta="$meta $status"
335
- printf -- '- %s %s\n' "$current_task" "$meta"
336
- [ -n "$current_phase" ] && printf ' Current phase: %s\n' "$current_phase"
337
- [ -n "$resume_next" ] && printf ' Resume next: %s\n' "$resume_next"
338
- fi
339
- }
340
-
341
- printf 'Active:\n'
342
- while IFS= read -r line || [ -n "$line" ]; do
343
- case "$line" in
344
- '## Active') in_active=1; in_backlog=0; in_completed=0; in_resume=0; continue ;;
345
- '## Backlog / Future') print_task; current_task=''; in_active=0; in_backlog=1; in_completed=0; in_resume=0; continue ;;
346
- '## Completed') print_task; current_task=''; in_active=0; in_backlog=0; in_completed=1; in_resume=0; continue ;;
347
- '## '*) print_task; current_task=''; in_active=0; in_backlog=0; in_completed=0; in_resume=0 ;;
348
- esac
349
- if [ "$in_active" -eq 1 ]; then
350
- if [[ "$line" =~ ^###[[:space:]]+(.+) ]]; then
351
- print_task
352
- current_task="${BASH_REMATCH[1]}"; status=''; level=''; current_phase=''; resume_next=''; in_resume=0
353
- elif [[ "$line" =~ ^Status:[[:space:]]*(.+) ]] && [ -z "$status" ]; then
354
- status="${BASH_REMATCH[1]}"
355
- in_resume=0
356
- elif [[ "$line" =~ ^Level:[[:space:]]*([0-3]) ]]; then
357
- level="${BASH_REMATCH[1]}"
358
- in_resume=0
359
- elif [[ "$line" =~ ^Current[[:space:]]phase:[[:space:]]*(.+) ]]; then
360
- current_phase="${BASH_REMATCH[1]}"
361
- in_resume=0
362
- elif [[ "$line" =~ ^Resume[[:space:]]next: ]]; then
363
- in_resume=1
364
- elif [ "$in_resume" -eq 1 ] && [[ "$line" =~ ^-[[:space:]]+(.+) ]]; then
365
- [ -z "$resume_next" ] && resume_next="${BASH_REMATCH[1]}"
366
- elif [[ -n "$line" && ! "$line" =~ ^[[:space:]]*$ ]]; then
367
- in_resume=0
368
- fi
369
- elif [ "$in_backlog" -eq 1 ] && [[ "$line" =~ ^-[[:space:]]+(\[[[:space:]xX]\][[:space:]]+)? ]]; then
370
- backlog_items=$((backlog_items + 1))
371
- elif [ "$in_completed" -eq 1 ] && [[ "$line" =~ ^###[[:space:]]+ ]]; then
372
- completed_items=$((completed_items + 1))
373
- fi
374
- done < "$LEDGER"
375
- print_task
376
- printf '\nBacklog / Future:\n- %d items\n\nCompleted:\n- %d items\n' "$backlog_items" "$completed_items"
377
- }
378
-
379
- cmd_hooks_status() {
380
- local hooks_json="$ROOT/.claude/hooks/hooks.json"
381
- local hook_script="$ROOT/.claude/hooks/session-start"
382
- printf 'hooks: %s\n' "$(hook_status_value)"
383
- printf 'hooks.json: %s\n' "$hooks_json"
384
- printf 'session-start: %s\n' "$hook_script"
385
- return 0
386
- }
387
-
388
- cmd_hooks_install() {
389
- local target_dir="$ROOT/.claude/hooks"
390
- if ! mkdir -p "$target_dir" 2>/dev/null; then
391
- printf 'error: cannot create .claude/hooks directory.\n' >&2
392
- return 1
393
- fi
394
- if [ -e "$target_dir/hooks.json" ]; then
395
- printf 'kept existing .claude/hooks/hooks.json\n'
396
- else
397
- cat > "$target_dir/hooks.json" <<'HOOKS_JSON'
398
- {
399
- "hooks": {
400
- "SessionStart": [
401
- {
402
- "matcher": "",
403
- "command": ".claude/hooks/session-start"
404
- }
405
- ]
406
- }
407
- }
408
- HOOKS_JSON
409
- printf 'installed .claude/hooks/hooks.json\n'
410
- fi
411
- if [ -e "$target_dir/session-start" ]; then
412
- printf 'kept existing .claude/hooks/session-start\n'
413
- else
414
- cat > "$target_dir/session-start" <<'HOOK_SCRIPT'
415
- #!/usr/bin/env bash
416
- set -u
417
-
418
- ledger=".claude/WORKFLOW.md"
419
- cli=".claude/bin/workflow-ledger"
420
-
421
- if [ ! -f "$ledger" ]; then
422
- exit 0
423
- fi
424
-
425
- if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
426
- cat <<'PLUGIN_JSON'
427
- {"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."}}
428
- PLUGIN_JSON
429
- exit 0
430
- fi
431
-
432
- printf 'Workflow Ledger detected.\n'
433
- printf -- '- Read .claude/WORKFLOW.md before resuming tracked work.\n'
434
- printf -- '- Check Active tasks, Current phase/current focus, Current todo, and Resume next.\n'
435
-
436
- if [ -x "$cli" ]; then
437
- printf -- '- Run .claude/bin/workflow-ledger doctor if state may be stale.\n'
438
- else
439
- printf -- '- Run workflow-ledger doctor if the project CLI is available.\n'
440
- fi
441
-
442
- exit 0
443
- HOOK_SCRIPT
444
- chmod +x "$target_dir/session-start" || return 1
445
- printf 'installed .claude/hooks/session-start\n'
446
- fi
447
- }
448
- main() {
449
- local cmd="${1:-help}"
450
- case "$cmd" in
451
- help|-h|--help) print_help; return 0 ;;
452
- init) shift; cmd_init "$@" ;;
453
- doctor) shift; cmd_doctor "$@" ;;
454
- list) shift; cmd_list "$@" ;;
455
- hooks)
456
- local sub="${2:-status}"
457
- case "$sub" in
458
- status) cmd_hooks_status ;;
459
- install) cmd_hooks_install ;;
460
- *) printf 'error: unknown hooks command: %s\n' "$sub" >&2; return 1 ;;
461
- esac
462
- ;;
463
- *) printf 'error: unknown command: %s\n\n' "$cmd" >&2; print_help; return 1 ;;
464
- esac
465
- }
466
-
467
- main "$@"