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.
@@ -1,417 +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
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 candidates=(
75
- "$ROOT/.claude/skills/workflow-ledger/templates/WORKFLOW.md"
76
- "$REPO_ROOT/skills/workflow-ledger/templates/WORKFLOW.md"
77
- )
78
- local candidate
79
- for candidate in "${candidates[@]}"; do
80
- if [ -f "$candidate" ]; then
81
- printf '%s' "$candidate"
82
- return 0
83
- fi
84
- done
85
- return 1
86
- }
87
-
88
- cmd_init() {
89
- if ! mkdir -p "$ROOT/.claude" 2>/dev/null; then
90
- printf 'error: cannot create .claude directory\n' >&2
91
- return 1
92
- fi
93
-
94
- if [ -f "$LEDGER" ]; then
95
- printf 'kept existing .claude/WORKFLOW.md\n'
96
- else
97
- local template
98
- if ! template="$(find_template)"; then
99
- printf 'workflow-ledger skill template not found; run install.sh from the workflow-ledger checkout.\n' >&2
100
- return 0
101
- fi
102
- if ! cp "$template" "$LEDGER" 2>/dev/null; then
103
- printf 'error: cannot create .claude/WORKFLOW.md\n' >&2
104
- return 1
105
- fi
106
- printf 'created .claude/WORKFLOW.md\n'
107
- fi
108
-
109
- if [ ! -d "$ROOT/.claude/skills/workflow-ledger" ]; then
110
- printf 'note: .claude/skills/workflow-ledger is missing; run install.sh to install the skill.\n' >&2
111
- fi
112
- return 0
113
- }
114
-
115
- cmd_doctor() {
116
- error_count=0
117
- warning_count=0
118
-
119
- if [ ! -f "$LEDGER" ]; then
120
- say_error '.claude/WORKFLOW.md is missing.'
121
- return 1
122
- fi
123
- if [ ! -r "$LEDGER" ]; then
124
- say_error '.claude/WORKFLOW.md exists but cannot be read.'
125
- return 1
126
- fi
127
-
128
- section_exists 'Active' || say_error 'Missing ## Active section.'
129
- section_exists 'Backlog / Future' || say_error 'Missing ## Backlog / Future section.'
130
- section_exists 'Completed' || say_error 'Missing ## Completed section.'
131
-
132
- local active_count=0
133
- local backlog_items=0
134
- local completed_items=0
135
- local hook_status
136
- hook_status="$(hook_status_value)"
137
-
138
- local in_active=0 in_backlog=0 in_completed=0
139
- local task_title='' task_status='' task_level='' task_current=''
140
- 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
141
- local task_line_count=0
142
-
143
- finish_task() {
144
- if [ -z "$task_title" ]; then
145
- return 0
146
- fi
147
-
148
- if [ "$task_status" = 'In Progress' ]; then
149
- [ -n "$task_current" ] || say_error "In Progress task '$task_title' lacks Current phase."
150
- [ "$task_has_intent" -eq 1 ] || say_error "In Progress task '$task_title' lacks Intent."
151
- [ "$task_has_todo" -eq 1 ] || say_error "In Progress task '$task_title' lacks Current todo."
152
- [ "$task_has_resume" -eq 1 ] || say_error "In Progress task '$task_title' lacks Resume next."
153
- if [ "$task_level" = '2' ] || [ "$task_level" = '3' ]; then
154
- [ "$task_has_changes" -eq 1 ] || say_warning "Level $task_level task '$task_title' lacks Changes."
155
- [ "$task_has_prereq" -eq 1 ] || say_warning "Level $task_level task '$task_title' lacks Prerequisites."
156
- fi
157
- fi
158
-
159
- if [ "$task_status" = 'Blocked' ]; then
160
- [ "$task_has_blocked_by" -eq 1 ] || say_error "Blocked task '$task_title' lacks Blocked by."
161
- [ "$task_has_resume" -eq 1 ] || say_error "Blocked task '$task_title' lacks Resume next."
162
- fi
163
-
164
- if [ "$task_status" = 'Done' ] || [ "$task_status" = 'Completed' ]; then
165
- [ "$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."
166
- fi
167
-
168
- if [ "$task_line_count" -gt 80 ]; then
169
- say_warning "Task '$task_title' has more than 80 lines."
170
- fi
171
- }
172
-
173
- while IFS= read -r line || [ -n "$line" ]; do
174
- case "$line" in
175
- '## Active')
176
- finish_task
177
- in_active=1; in_backlog=0; in_completed=0
178
- task_title=''
179
- continue
180
- ;;
181
- '## Backlog / Future')
182
- finish_task
183
- in_active=0; in_backlog=1; in_completed=0
184
- task_title=''
185
- continue
186
- ;;
187
- '## Completed')
188
- finish_task
189
- in_active=0; in_backlog=0; in_completed=1
190
- task_title=''
191
- continue
192
- ;;
193
- '## '*)
194
- finish_task
195
- in_active=0; in_backlog=0; in_completed=0
196
- task_title=''
197
- ;;
198
- esac
199
-
200
- if [ "$in_backlog" -eq 1 ] && [[ "$line" =~ ^-[[:space:]]+(\[[[:space:]xX]\][[:space:]]+)? ]]; then
201
- backlog_items=$((backlog_items + 1))
202
- fi
203
- if [ "$in_completed" -eq 1 ] && [[ "$line" =~ ^###[[:space:]]+ ]]; then
204
- completed_items=$((completed_items + 1))
205
- fi
206
-
207
- if [ "$in_active" -eq 1 ]; then
208
- if [ -n "$task_title" ]; then
209
- task_line_count=$((task_line_count + 1))
210
- fi
211
- if [[ "$line" =~ ^###[[:space:]]+(.+) ]]; then
212
- finish_task
213
- active_count=$((active_count + 1))
214
- task_title="${BASH_REMATCH[1]}"
215
- task_status=''; task_level=''; task_current=''
216
- 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
217
- task_line_count=1
218
- continue
219
- fi
220
- [[ "$line" =~ ^Status:[[:space:]]*(.+) ]] && [ -z "$task_status" ] && task_status="${BASH_REMATCH[1]}"
221
- [[ "$line" =~ ^Level:[[:space:]]*([0-3]) ]] && task_level="${BASH_REMATCH[1]}"
222
- [[ "$line" =~ ^Current[[:space:]]phase:[[:space:]]*(.+) ]] && task_current="${BASH_REMATCH[1]}"
223
- [[ "$line" =~ ^Intent: ]] && task_has_intent=1
224
- [[ "$line" =~ ^Current[[:space:]]todo: ]] && task_has_todo=1
225
- [[ "$line" =~ ^Changes: ]] && task_has_changes=1
226
- [[ "$line" =~ ^Prerequisites: ]] && task_has_prereq=1
227
- [[ "$line" =~ ^Blocked[[:space:]]by: ]] && task_has_blocked_by=1
228
- [[ "$line" =~ ^Resume[[:space:]]next: ]] && task_has_resume=1
229
- [[ "$line" =~ ^Close[[:space:]]summary: ]] && task_has_close_summary=1
230
- fi
231
- done < "$LEDGER"
232
- finish_task
233
-
234
- if [ "$backlog_items" -gt 10 ]; then
235
- say_warning 'Backlog / Future contains more than 10 items.'
236
- fi
237
- if [ "$active_count" -gt 1 ]; then
238
- say_warning 'More than one Active task; include priority, blocker state, and Resume next if this is intentional.'
239
- fi
240
-
241
- say_info "Active tasks: $active_count"
242
- say_info "Backlog items: $backlog_items"
243
- say_info "Completed tasks: $completed_items"
244
- if ledger_mtime="$(stat -c %Y "$LEDGER" 2>/dev/null)"; then
245
- say_info "Ledger modified: $(date -d "@$ledger_mtime" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || printf '%s' "$ledger_mtime")"
246
- fi
247
- if command -v git >/dev/null 2>&1 && git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
248
- local commit_time
249
- if commit_time="$(git -C "$ROOT" log -1 --format=%ct 2>/dev/null)" && [ -n "$commit_time" ]; then
250
- say_info "Latest git commit: $(date -d "@$commit_time" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || printf '%s' "$commit_time")"
251
- if [ -n "${ledger_mtime:-}" ] && [ "$ledger_mtime" -lt "$commit_time" ]; then
252
- say_warning 'Ledger modified time is older than latest git commit.'
253
- fi
254
- fi
255
- fi
256
- say_info "Hooks: $hook_status"
257
-
258
- if [ "$error_count" -gt 0 ]; then
259
- printf 'doctor finished with %d error(s), %d warning(s).\n' "$error_count" "$warning_count"
260
- return 1
261
- fi
262
- printf 'doctor finished with 0 errors, %d warning(s).\n' "$warning_count"
263
- return 0
264
- }
265
-
266
- cmd_list() {
267
- if [ ! -f "$LEDGER" ]; then
268
- printf 'No .claude/WORKFLOW.md found; no tasks to list.\n' >&2
269
- return 0
270
- fi
271
- if [ ! -r "$LEDGER" ]; then
272
- printf 'error: .claude/WORKFLOW.md exists but cannot be read.\n' >&2
273
- return 1
274
- fi
275
-
276
- local in_active=0 in_backlog=0 in_completed=0 in_resume=0
277
- local backlog_items=0 completed_items=0
278
- local current_task='' status='' level='' current_phase='' resume_next=''
279
-
280
- print_task() {
281
- if [ -n "$current_task" ]; then
282
- local meta=''
283
- [ -n "$level" ] && meta="[Level $level]"
284
- [ -n "$status" ] && meta="$meta $status"
285
- printf -- '- %s %s\n' "$current_task" "$meta"
286
- [ -n "$current_phase" ] && printf ' Current phase: %s\n' "$current_phase"
287
- [ -n "$resume_next" ] && printf ' Resume next: %s\n' "$resume_next"
288
- fi
289
- }
290
-
291
- printf 'Active:\n'
292
- while IFS= read -r line || [ -n "$line" ]; do
293
- case "$line" in
294
- '## Active') in_active=1; in_backlog=0; in_completed=0; in_resume=0; continue ;;
295
- '## Backlog / Future') print_task; current_task=''; in_active=0; in_backlog=1; in_completed=0; in_resume=0; continue ;;
296
- '## Completed') print_task; current_task=''; in_active=0; in_backlog=0; in_completed=1; in_resume=0; continue ;;
297
- '## '*) print_task; current_task=''; in_active=0; in_backlog=0; in_completed=0; in_resume=0 ;;
298
- esac
299
- if [ "$in_active" -eq 1 ]; then
300
- if [[ "$line" =~ ^###[[:space:]]+(.+) ]]; then
301
- print_task
302
- current_task="${BASH_REMATCH[1]}"; status=''; level=''; current_phase=''; resume_next=''; in_resume=0
303
- elif [[ "$line" =~ ^Status:[[:space:]]*(.+) ]] && [ -z "$status" ]; then
304
- status="${BASH_REMATCH[1]}"
305
- in_resume=0
306
- elif [[ "$line" =~ ^Level:[[:space:]]*([0-3]) ]]; then
307
- level="${BASH_REMATCH[1]}"
308
- in_resume=0
309
- elif [[ "$line" =~ ^Current[[:space:]]phase:[[:space:]]*(.+) ]]; then
310
- current_phase="${BASH_REMATCH[1]}"
311
- in_resume=0
312
- elif [[ "$line" =~ ^Resume[[:space:]]next: ]]; then
313
- in_resume=1
314
- elif [ "$in_resume" -eq 1 ] && [[ "$line" =~ ^-[[:space:]]+(.+) ]]; then
315
- [ -z "$resume_next" ] && resume_next="${BASH_REMATCH[1]}"
316
- elif [[ -n "$line" && ! "$line" =~ ^[[:space:]]*$ ]]; then
317
- in_resume=0
318
- fi
319
- elif [ "$in_backlog" -eq 1 ] && [[ "$line" =~ ^-[[:space:]]+(\[[[:space:]xX]\][[:space:]]+)? ]]; then
320
- backlog_items=$((backlog_items + 1))
321
- elif [ "$in_completed" -eq 1 ] && [[ "$line" =~ ^###[[:space:]]+ ]]; then
322
- completed_items=$((completed_items + 1))
323
- fi
324
- done < "$LEDGER"
325
- print_task
326
- printf '\nBacklog / Future:\n- %d items\n\nCompleted:\n- %d items\n' "$backlog_items" "$completed_items"
327
- }
328
-
329
- cmd_hooks_status() {
330
- local hooks_json="$ROOT/.claude/hooks/hooks.json"
331
- local hook_script="$ROOT/.claude/hooks/session-start"
332
- printf 'hooks: %s\n' "$(hook_status_value)"
333
- printf 'hooks.json: %s\n' "$hooks_json"
334
- printf 'session-start: %s\n' "$hook_script"
335
- return 0
336
- }
337
-
338
- cmd_hooks_install() {
339
- local target_dir="$ROOT/.claude/hooks"
340
- if ! mkdir -p "$target_dir" 2>/dev/null; then
341
- printf 'error: cannot create .claude/hooks directory.\n' >&2
342
- return 1
343
- fi
344
- if [ -e "$target_dir/hooks.json" ]; then
345
- printf 'kept existing .claude/hooks/hooks.json\n'
346
- else
347
- cat > "$target_dir/hooks.json" <<'HOOKS_JSON'
348
- {
349
- "hooks": {
350
- "SessionStart": [
351
- {
352
- "matcher": "",
353
- "command": ".claude/hooks/session-start"
354
- }
355
- ]
356
- }
357
- }
358
- HOOKS_JSON
359
- printf 'installed .claude/hooks/hooks.json\n'
360
- fi
361
- if [ -e "$target_dir/session-start" ]; then
362
- printf 'kept existing .claude/hooks/session-start\n'
363
- else
364
- cat > "$target_dir/session-start" <<'HOOK_SCRIPT'
365
- #!/usr/bin/env bash
366
- set -u
367
-
368
- ledger=".claude/WORKFLOW.md"
369
- cli=".claude/bin/workflow-ledger"
370
-
371
- if [ ! -f "$ledger" ]; then
372
- exit 0
373
- fi
374
-
375
- if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
376
- cat <<'PLUGIN_JSON'
377
- {"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."}}
378
- PLUGIN_JSON
379
- exit 0
380
- fi
381
-
382
- printf 'Workflow Ledger detected.\n'
383
- printf -- '- Read .claude/WORKFLOW.md before resuming tracked work.\n'
384
- printf -- '- Check Active tasks, Current phase/current focus, Current todo, and Resume next.\n'
385
-
386
- if [ -x "$cli" ]; then
387
- printf -- '- Run .claude/bin/workflow-ledger doctor if state may be stale.\n'
388
- else
389
- printf -- '- Run workflow-ledger doctor if the project CLI is available.\n'
390
- fi
391
-
392
- exit 0
393
- HOOK_SCRIPT
394
- chmod +x "$target_dir/session-start" || return 1
395
- printf 'installed .claude/hooks/session-start\n'
396
- fi
397
- }
398
- main() {
399
- local cmd="${1:-help}"
400
- case "$cmd" in
401
- help|-h|--help) print_help; return 0 ;;
402
- init) shift; cmd_init "$@" ;;
403
- doctor) shift; cmd_doctor "$@" ;;
404
- list) shift; cmd_list "$@" ;;
405
- hooks)
406
- local sub="${2:-status}"
407
- case "$sub" in
408
- status) cmd_hooks_status ;;
409
- install) cmd_hooks_install ;;
410
- *) printf 'error: unknown hooks command: %s\n' "$sub" >&2; return 1 ;;
411
- esac
412
- ;;
413
- *) printf 'error: unknown command: %s\n\n' "$cmd" >&2; print_help; return 1 ;;
414
- esac
415
- }
416
-
417
- main "$@"