@zalom/plastic 2.0.0-alpha.2 → 2.0.0-alpha.20

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.
Files changed (117) hide show
  1. package/agents/plastic-enforcer.md +3 -1
  2. package/agents/plastic-executor.md +9 -3
  3. package/hooks/hooks.json +12 -0
  4. package/hooks/message-display +134 -0
  5. package/hooks/statusline +4 -27
  6. package/package.json +2 -2
  7. package/scripts/agent-report +8 -2
  8. package/scripts/dashboard.rb +297 -18
  9. package/scripts/doctor.rb +652 -5
  10. package/scripts/end-intent +219 -0
  11. package/scripts/hook-capture +25 -113
  12. package/scripts/hook-message-display +81 -0
  13. package/scripts/hook-record +12 -4
  14. package/scripts/hook-session-start +23 -1
  15. package/scripts/index-projection +74 -0
  16. package/scripts/intent-screen +77 -0
  17. package/scripts/lib/action_graph_shim.rb +277 -0
  18. package/scripts/lib/arm.rb +26 -1
  19. package/scripts/lib/atomic_write.rb +31 -0
  20. package/scripts/lib/core_integrity.rb +71 -0
  21. package/scripts/lib/dashboard_screen.rb +40 -0
  22. package/scripts/lib/day_summary.rb +7 -2
  23. package/scripts/lib/doctor_core.rb +101 -5
  24. package/scripts/lib/doctor_session_ledger.rb +52 -0
  25. package/scripts/lib/graph_edges.rb +121 -0
  26. package/scripts/lib/graph_file.rb +246 -0
  27. package/scripts/lib/graph_tree.rb +98 -0
  28. package/scripts/lib/guarded_append.rb +155 -0
  29. package/scripts/lib/hook_registry.rb +14 -0
  30. package/scripts/lib/hook_replay.rb +211 -0
  31. package/scripts/lib/index_projection.rb +201 -0
  32. package/scripts/lib/installer_core.rb +131 -4
  33. package/scripts/lib/intent_screen.rb +309 -0
  34. package/scripts/lib/intent_screen_ansi.rb +262 -0
  35. package/scripts/lib/message_display.rb +586 -0
  36. package/scripts/lib/node_file.rb +214 -0
  37. package/scripts/lib/node_ids.rb +99 -0
  38. package/scripts/lib/node_ledger.rb +377 -0
  39. package/scripts/lib/node_packet.rb +886 -0
  40. package/scripts/lib/node_return.rb +199 -0
  41. package/scripts/lib/node_worktree.rb +337 -0
  42. package/scripts/lib/outcome_report.rb +440 -0
  43. package/scripts/lib/packet_wrapper.rb +132 -0
  44. package/scripts/lib/ready_set.rb +462 -0
  45. package/scripts/lib/release_guard.rb +16 -0
  46. package/scripts/lib/report_screen.rb +1967 -0
  47. package/scripts/lib/roadmap_graph.rb +210 -0
  48. package/scripts/lib/roadmap_migration.rb +95 -0
  49. package/scripts/lib/roadmap_queue.rb +155 -5
  50. package/scripts/lib/roadmap_render.rb +150 -0
  51. package/scripts/lib/roadmap_savepoint.rb +62 -12
  52. package/scripts/lib/runner_absorb.rb +620 -0
  53. package/scripts/lib/runner_answer.rb +206 -0
  54. package/scripts/lib/runner_core.rb +194 -0
  55. package/scripts/lib/runner_dispatch.rb +482 -0
  56. package/scripts/lib/runner_policy.rb +142 -0
  57. package/scripts/lib/runner_proposals.rb +254 -0
  58. package/scripts/lib/runner_rewind.rb +201 -0
  59. package/scripts/lib/runner_sweep.rb +231 -0
  60. package/scripts/lib/savepoint.rb +149 -12
  61. package/scripts/lib/screen_paint.rb +555 -0
  62. package/scripts/lib/screens/dashboard.rb +20 -0
  63. package/scripts/lib/screens/plan.rb +18 -0
  64. package/scripts/lib/screens/roadmap.rb +15 -0
  65. package/scripts/lib/session_git.rb +49 -18
  66. package/scripts/lib/session_ledger.rb +128 -0
  67. package/scripts/lib/verify_intent.rb +33 -0
  68. package/scripts/lib/work_graph_validator.rb +201 -0
  69. package/scripts/node-packet +92 -0
  70. package/scripts/node-transition +291 -0
  71. package/scripts/outcome-report +74 -0
  72. package/scripts/plastic-lock +8 -1
  73. package/scripts/ready-set +126 -0
  74. package/scripts/release-check +118 -0
  75. package/scripts/report-screen +281 -0
  76. package/scripts/roadmap-graph +119 -0
  77. package/scripts/roadmap-savepoint +7 -0
  78. package/scripts/runner +392 -0
  79. package/scripts/savepoint-note +69 -0
  80. package/scripts/spawn-preamble +9 -2
  81. package/scripts/validate-work-graph +39 -0
  82. package/scripts/verify-intent +2 -1
  83. package/skills/auto/SKILL.md +24 -17
  84. package/skills/auto/references/human-report-contract.md +136 -54
  85. package/skills/conventions/references/locks-and-worktrees.md +12 -0
  86. package/skills/dashboard/SKILL.md +13 -2
  87. package/skills/dashboard/templates/dashboard-global.md +1 -1
  88. package/skills/dashboard/templates/dashboard-project.md +2 -2
  89. package/skills/doctor/SKILL.md +10 -4
  90. package/skills/intent-continuing/SKILL.md +51 -41
  91. package/skills/intent-continuing/references/board-fill.md +9 -0
  92. package/skills/intent-continuing/references/boarding-matrix.md +6 -5
  93. package/skills/intent-continuing/references/context-management.md +1 -1
  94. package/skills/intent-ending/SKILL.md +36 -16
  95. package/skills/intent-executing/SKILL.md +17 -5
  96. package/skills/intent-executing/implementer-prompt.md +6 -1
  97. package/skills/intent-speccing/SKILL.md +7 -4
  98. package/skills/releasing/SKILL.md +39 -0
  99. package/skills/releasing/references/promotion-and-tagging.md +10 -6
  100. package/skills/releasing/references/release-lines.md +1 -1
  101. package/skills/roadmap/SKILL.md +26 -0
  102. package/skills/roadmap/references/file-format.md +10 -0
  103. package/templates/dashboard-screen.md +22 -0
  104. package/templates/display-fixture.md +21 -0
  105. package/templates/graph.md +16 -0
  106. package/templates/intent-screen.md +17 -0
  107. package/templates/node-decision.md +11 -0
  108. package/templates/node-research.md +11 -0
  109. package/templates/node-verify.md +13 -0
  110. package/templates/node-work.md +22 -0
  111. package/templates/outcome.md +19 -1
  112. package/templates/report-plan.md +15 -0
  113. package/templates/report-roadmap-delivered.md +10 -0
  114. package/templates/report-roadmap-plan.md +9 -0
  115. package/templates/report-roadmap-state.md +9 -0
  116. package/templates/report-state.md +11 -0
  117. package/templates/roadmap.md +13 -0
@@ -31,7 +31,9 @@ deliberately; the auto pipeline never dispatches them.
31
31
  `plastic-intent-executing`'s `plan-reviewer-prompt.md`; fold every finding into the spec, the
32
32
  matrix, and the tests.
33
33
  4. **Dispatch one executor, tests first** - the executor commits the matrix's tests red, then
34
- builds, then drives the full suite green; you verify the checklist against the diff.
34
+ builds, then drives the full suite green; you verify tick-versus-diff at the
35
+ post-execution review and again before the merge. A mismatch is a review finding, not a
36
+ cleanup you perform silently.
35
37
  5. **Review by risk** - dispatch the post-execution reviewer only when the auto skill's risk
36
38
  rule fires; otherwise the green suite is the review.
37
39
  6. **Close** - `outcome.md`, then `plastic-intent-ending`, which releases the worktree, clears
@@ -20,7 +20,11 @@ valid lifecycle artifacts. Honor it as your live state; do not re-derive or cont
20
20
  any other change.
21
21
  2. **Implement the actions** - make the code changes for each action in order, inside the
22
22
  worktree the preamble names.
23
- 3. **Track progress** - check off `checklist.md` items as they complete.
23
+ 3. **Tick with the commit** - commit after each logical unit of work, and in the same step
24
+ tick the checklist item that unit lands: mark its box `[x]` and move the line to
25
+ `## Completed`, then append the savepoint `Commit` line
26
+ (`scripts/savepoint-note <intent_dir> --kind Commit --text "<sha> <what it proves>"`). A
27
+ commit without its tick is incomplete.
24
28
  4. **Record insights** - capture durable discoveries and report them in the `insights:` field;
25
29
  persist each to `## Insights` via the `insight-append` helper
26
30
  (`scripts/insight-append <intent_dir> <text> --stage Exec --author "plastic-executor (autonomous)"`),
@@ -33,7 +37,8 @@ valid lifecycle artifacts. Honor it as your live state; do not re-derive or cont
33
37
  real `ACTION_N.md` with its failure-mode matrix, pasted in by the lead. Execute the action
34
38
  files in order.
35
39
  2. Write the matrix's tests; commit red.
36
- 3. Work one action at a time, preferring safe, non-destructive routes.
40
+ 3. Work one action at a time, ticking its checklist item in the same commit that lands it
41
+ (box marked, line moved); prefer safe, non-destructive routes.
37
42
  4. Run the full suite, iterate to zero failures and zero errors; commit green.
38
43
  5. Produce (output handoff): the code changes, a checked-off `checklist.md`, and `## Insights`.
39
44
  6. Report (see `## Completion Report`); the lead applies the risk rule and may dispatch a
@@ -46,7 +51,8 @@ preamble's `REPORT_CONTRACT` and `skills/auto/references/agent-report-contract.m
46
51
  finish silently. Carry the common envelope (role, intent id, stage, status, artifacts written,
47
52
  verification, checklist deltas, deviations, blockers, insights) plus the executor payload:
48
53
 
49
- - Actions implemented this turn, mapped to checklist items checked off (checked / total)
54
+ - Actions implemented this turn, mapped to checklist items checked off (checked / total);
55
+ checked / total must equal the items whose commits exist
50
56
  - A summary of the code changed (files and the shape of the change)
51
57
  - Test result: the red commit's failing count, then the full-suite command and its pass / fail
52
58
  counts
package/hooks/hooks.json CHANGED
@@ -64,6 +64,18 @@
64
64
  }
65
65
  ]
66
66
  }
67
+ ],
68
+ "MessageDisplay": [
69
+ {
70
+ "matcher": "",
71
+ "hooks": [
72
+ {
73
+ "type": "command",
74
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\" message-display",
75
+ "statusMessage": ""
76
+ }
77
+ ]
78
+ }
67
79
  ]
68
80
  }
69
81
  }
@@ -0,0 +1,134 @@
1
+ #!/bin/bash
2
+ # hooks/message-display (intent 316a, O6, round 3 concurrency fix): the
3
+ # MessageDisplay launcher. Fires on every streamed chunk of every assistant
4
+ # message (D11), so the common case — an ordinary chunk of an ordinary
5
+ # message — must decide with shell builtins alone and fork nothing. Only a
6
+ # candidate message hands off to Ruby (scripts/hook-message-display), which
7
+ # is the one place allowed to do real work.
8
+ #
9
+ # No command substitution, no backticks, no sed/jq/cat: case, [, parameter
10
+ # expansion and printf are all builtins. Deliberately does NOT copy hooks/
11
+ # capture's SCRIPT_DIR-via-subshell pattern (cd into dirname of $0, inside a
12
+ # command substitution, then pwd) — that forks a subshell on every single
13
+ # invocation, which is exactly the cost this hook cannot carry.
14
+ #
15
+ # A live run under a real pty found Claude Code fires these chunk processes
16
+ # CONCURRENTLY: a chunk with index > 0 can arrive, and be judged here,
17
+ # before chunk 0 ever runs. The OLD hand-off test — "does a buffer already
18
+ # exist for this message" — answered no in that race and silently dropped
19
+ # the chunk before Ruby ever saw it, no matter what MessageDisplay's own
20
+ # (correct) polling logic would have done. So a later chunk is now also
21
+ # handed off when its OWN delta looks like it could be part of a screen
22
+ # (leading "|" or "**Steps**", or blank — the same cheap test Ruby itself
23
+ # uses to decide whether a wait is worth paying for), and the final chunk is
24
+ # ALWAYS handed off, whatever it looks like, since it is the one that must
25
+ # not race. Ruby is the one place that actually waits (bounded, injectable
26
+ # for tests); this script only ever decides once, fast, and never sleeps.
27
+ #
28
+ # Claude adapter: Claude Code only; the core is harness-agnostic.
29
+ IFS= read -r -d '' INPUT # returns 1 at EOF: do NOT set -e
30
+ case $INPUT in *'"message_id"'*) ;; *) exit 0 ;; esac
31
+ rest=${INPUT#*\"message_id\"}; rest=${rest#*\"}; mid=${rest%%\"*}
32
+ rest=${INPUT#*\"session_id\"}; rest=${rest#*\"}; sid=${rest%%\"*}
33
+
34
+ SCRIPT_DIR=${0%/*}
35
+ TMP_ROOT=${PLASTIC_TMP:-${TMPDIR:-/tmp}}
36
+ export PLASTIC_TMP="$TMP_ROOT"
37
+ MSGDIR="$TMP_ROOT/plastic-message-display/$sid/$mid"
38
+
39
+ is_index_zero=0
40
+ case $INPUT in *'"index":0'*|*'"index": 0'*) is_index_zero=1 ;; esac
41
+
42
+ is_final=0
43
+ case $INPUT in *'"final":true'*|*'"final": true'*) is_final=1 ;; esac
44
+
45
+ handoff=0
46
+ if [ "$is_index_zero" = 1 ]; then
47
+ # Chunk 0 decides synchronously; a bare "#" first delta, in either JSON
48
+ # spacing, is the only shape that can possibly open a screen. Must be a
49
+ # single "#", not "##" — a real screen's own first delta can be as short
50
+ # as "## " — a "##" glob would filter out exactly the message this hook
51
+ # exists to recognize.
52
+ # 317a (B11): a screen can open with "## " (state, delivered), a bare "▶"
53
+ # (roster), or a bare "✔" (delay); the JSON may carry the glyph raw or
54
+ # \u-escaped depending on the encoder. All globs are literal bytes - case
55
+ # matching is byte-wise, so multibyte literals are safe under bash 3.2.
56
+ case $INPUT in
57
+ *'"delta":"#'*|*'"delta": "#'*) handoff=1 ;;
58
+ *'"delta":"▶'*|*'"delta": "▶'*) handoff=1 ;;
59
+ *'"delta":"✔'*|*'"delta": "✔'*) handoff=1 ;;
60
+ esac
61
+ # 331a review fix (M12e): chunk 0 is not exempt from D5 either. The
62
+ # intent's own flagship shape -- a lead-in sentence and the opener in the
63
+ # SAME first chunk -- never starts with "#", "▶", or "✔", so the arm
64
+ # above alone never sees it. Joins it with the identical opener-anywhere
65
+ # globs used for a later chunk (M12a) rather than replacing anything:
66
+ # same reasoning as the comment at that site applies verbatim here.
67
+ case $INPUT in
68
+ *'"delta":"'*'▶'*|*'"delta": "'*'▶'*) handoff=1 ;;
69
+ *'"delta":"'*'✔'*|*'"delta": "'*'✔'*) handoff=1 ;;
70
+ *'"delta":"'*'\u25b6'*|*'"delta": "'*'\u25b6'*) handoff=1 ;;
71
+ *'"delta":"'*'\u2714'*|*'"delta": "'*'\u2714'*) handoff=1 ;;
72
+ esac
73
+ else
74
+ # A later chunk: hand off when this message's directory already exists
75
+ # (chunk 0 already left a decision or a chunk file), when this chunk is
76
+ # final (it must always be checked, whatever it looks like), or when its
77
+ # own delta is shaped like part of a screen — a Markdown table row, the
78
+ # "**Steps**" heading, or a blank line, in either JSON spacing.
79
+ [ -d "$MSGDIR" ] && handoff=1
80
+ [ "$is_final" = 1 ] && handoff=1
81
+ case $INPUT in
82
+ *'"delta":"|'*|*'"delta": "|'*) handoff=1 ;;
83
+ *'"delta":"**'*|*'"delta": "**'*) handoff=1 ;;
84
+ *'"delta":""'*|*'"delta": ""'*) handoff=1 ;;
85
+ *'"delta":"\n"'*|*'"delta": "\n"'*) handoff=1 ;;
86
+ esac
87
+ # 331a (D5): the hook engages anywhere. An opener can now fall ANYWHERE
88
+ # inside a later chunk's own delta, not only at its start (the shape
89
+ # tests just above), so these globs search the delta value rather than
90
+ # anchor to its opening quote. Still anchored to the "delta":" key itself
91
+ # (never dropped, matrix M12d) so an ordinary field elsewhere in the
92
+ # payload can never be mistaken for the delta's own text; within that,
93
+ # every shipped opener (## ▶, ## ✔, ▶ In delivery, ✔ + digit) contains a
94
+ # bare ▶ or ✔, so matching the bare glyph anywhere covers all four at
95
+ # once. Prose that merely shapes like an opener still hands off this way
96
+ # (an accepted over-match: Ruby's own grammar is the final, stricter
97
+ # arbiter and fails open on anything that is not a real one). Raw glyph
98
+ # and \u-escaped forms both count, since the encoder's choice is not ours
99
+ # to predict.
100
+ case $INPUT in
101
+ *'"delta":"'*'▶'*|*'"delta": "'*'▶'*) handoff=1 ;;
102
+ *'"delta":"'*'✔'*|*'"delta": "'*'✔'*) handoff=1 ;;
103
+ *'"delta":"'*'\u25b6'*|*'"delta": "'*'\u25b6'*) handoff=1 ;;
104
+ *'"delta":"'*'\u2714'*|*'"delta": "'*'\u2714'*) handoff=1 ;;
105
+ esac
106
+ fi
107
+
108
+ [ "$handoff" = 1 ] || exit 0
109
+
110
+ # 331a1 (D1): chunk 0 is about to hand off to Ruby, which takes on the
111
+ # order of 150 ms to boot before it ever writes SCREEN or NOSCREEN. Claude
112
+ # Code fires the per-chunk hook PROCESSES CONCURRENTLY (see the header
113
+ # comment above), so a later chunk landing inside that window used to find
114
+ # no message directory at all, fail this script's own cheap shape test,
115
+ # and pass through as raw Markdown - measured at 15 to 21 chunks of every
116
+ # screen, against a stream-realistic 5 ms stagger. Staking a PENDING
117
+ # marker here, the instant the handoff gate above passes, closes that
118
+ # window down to microseconds: a later chunk's own "does the directory
119
+ # exist" check (below, unchanged) now finds it almost at once, and
120
+ # MessageDisplay polls for the real decision instead of judging the
121
+ # chunk's own shape. mkdir is the one non-builtin on this whole path - it
122
+ # forks, unlike everything else in this file - but it runs ONLY here,
123
+ # after the handoff gate, so the ordinary (non-candidate) chunk that is
124
+ # the overwhelming common case still forks nothing (matrix L8). A failed
125
+ # mkdir or printf (an unwritable tmp root, a race with another process) is
126
+ # silently swallowed: it must never change this script's own exit status
127
+ # or skip the Ruby handoff below - Ruby's own polling already fails open
128
+ # (D5) when no decision ever appears.
129
+ if [ "$is_index_zero" = 1 ]; then
130
+ mkdir -p "$MSGDIR" 2>/dev/null
131
+ printf '' > "$MSGDIR/PENDING" 2>/dev/null
132
+ fi
133
+
134
+ printf '%s' "$INPUT" | env -u RUBYOPT ruby "$SCRIPT_DIR/../scripts/hook-message-display"
package/hooks/statusline CHANGED
@@ -2,8 +2,8 @@
2
2
  # plastic-hook-version: 4.0.0
3
3
  # StatusLine hook - Plastic owns the full statusline (no chaining).
4
4
  # Renders: {model} - ctx {pct}% ({used}/{size}) - 5h {p}% - 7d {p}% - ${cost}
5
- # - claudish {n} rw / {tok} - Plastic {version}[ -> {next}] - {path}
6
- # Every segment but claudish and the Plastic version comes from the stdin payload.
5
+ # - Plastic {version}[ -> {next}] - {path}
6
+ # Every segment but the Plastic version comes from the stdin payload.
7
7
  # Pure bash (macOS 3.2 compatible). No ruby, no jq. Reads stdin once; small file reads
8
8
  # only. The stdin JSON is walked in a single awk pass (below); nothing else in this
9
9
  # script forks a second process per field, which is what keeps a render cheap.
@@ -17,7 +17,6 @@ C_OK='' # default - meter below 70%
17
17
  C_WARN=$'\033[33m' # yellow - meter at 70% and above
18
18
  C_CRIT=$'\033[38;2;231;76;60m' # red - meter at 90% and above
19
19
  C_COST=$'\033[37m' # white - session cost
20
- C_CLAUDISH=$'\033[38;2;249;140;30m' # orange - claudish rewrites
21
20
  C_VER=$'\033[37m' # white - "Plastic {version}" and update arrow
22
21
  C_NEXT=$'\033[33m' # yellow - next version
23
22
  C_PATH=$'\033[38;2;128;134;144m' # gray - path (matches dim UI text)
@@ -309,40 +308,18 @@ if [ -n "$COST_RAW" ]; then
309
308
  [ -n "$COST_FMT" ] && [ "$COST_FMT" != "0.00" ] && COST_SEG="${C_COST}\$${COST_FMT}${RST}"
310
309
  fi
311
310
 
312
- # --- claudish rewrites for THIS session (fork ledger, optional) ---
313
- # Tab separated, one call per line; column 11 is the session id. Rows written
314
- # before that column existed have 9 or 10 fields and never match, so an old
315
- # ledger renders no segment instead of a wrong one.
316
- CL_SEG=""
317
- CL_LOG="${CLAUDISH_LOCAL_DIR:-$HOME/.claude/claudish-local}/usage.log"
318
- if [ -n "$SID" ] && [ -f "$CL_LOG" ]; then
319
- CL=$(awk -F'\t' -v sid="$SID" '
320
- NF >= 11 && $11 == sid { n++; t += $5 + $6 }
321
- END {
322
- if (n > 0) {
323
- if (t >= 1000) {
324
- s = sprintf("%.1f", t / 1000); sub(/\.0$/, "", s); printf "%d %sk", n, s
325
- } else {
326
- printf "%d %d", n, t
327
- }
328
- }
329
- }' "$CL_LOG" 2>/dev/null)
330
- [ -n "$CL" ] && CL_SEG="${C_CLAUDISH}claudish ${CL%% *} rw / ${CL#* }${RST}"
331
- fi
332
-
333
311
  # --- Assemble segments in order, joined by " . " ---
334
312
  PARTS=""
335
313
  add() { [ -z "$1" ] && return; if [ -z "$PARTS" ]; then PARTS="$1"; else PARTS="$PARTS$SEP$1"; fi; }
336
314
 
337
- # Order: identity first (model), then this session's spend (context, meters, cost,
338
- # rewrites), then the volatile-length values last (version, path) so the line does
315
+ # Order: identity first (model), then this session's spend (context, meters, cost),
316
+ # then the volatile-length values last (version, path) so the line does
339
317
  # not visually jump as they change.
340
318
  [ -n "$MODEL" ] && add "${C_MODEL}${MODEL}${RST}"
341
319
  add "$CTX_SEG"
342
320
  add "$M5"
343
321
  add "$M7"
344
322
  add "$COST_SEG"
345
- add "$CL_SEG"
346
323
  if [ -n "$VERSION" ]; then
347
324
  vseg="${C_VER}Plastic ${VERSION}${RST}"
348
325
  [ -n "$NEXT" ] && vseg="${vseg}${C_VER} "$'\342\206\222'" ${RST}${C_NEXT}${NEXT}${RST}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalom/plastic",
3
- "version": "2.0.0-alpha.2",
3
+ "version": "2.0.0-alpha.20",
4
4
  "description": "Intent-driven idea development system for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,7 +27,7 @@
27
27
  "license": "MIT",
28
28
  "repository": {
29
29
  "type": "git",
30
- "url": "https://github.com/zalom/plastic"
30
+ "url": "git+https://github.com/zalom/plastic.git"
31
31
  },
32
32
  "homepage": "https://github.com/zalom/plastic",
33
33
  "files": [
@@ -22,6 +22,8 @@
22
22
  # Exit codes: 0 (report emitted), 2 (usage).
23
23
 
24
24
  require_relative "lib/savepoint"
25
+ require_relative "lib/intent_screen"
26
+
25
27
  def parse_args(argv)
26
28
  role = nil
27
29
  positional = []
@@ -67,11 +69,15 @@ STAGE_LABELS = {
67
69
  "exec" => "Exec", "done" => "Done"
68
70
  }.freeze
69
71
 
72
+ # Intent 317, D6: name the last LIFECYCLE line, never a trailing Lock/Review/
73
+ # Commit line (same guard as spawn-preamble; plan review finding B1).
70
74
  def current_stage(intent_dir)
71
75
  ledger = File.join(intent_dir, Savepoint::SAVEPOINT_FILE)
72
76
  if File.exist?(ledger)
73
- last = File.read(ledger).each_line.map(&:strip).reject(&:empty?).last
74
- return last if last
77
+ lines = File.read(ledger).each_line.map(&:strip).reject(&:empty?)
78
+ lifecycle = lines.select { |l| IntentScreen.lifecycle_line?(l) }
79
+ return lifecycle.last if lifecycle.any?
80
+ return lines.last if lines.any?
75
81
  end
76
82
  STAGE_LABELS.fetch(Savepoint.derive_stage(intent_dir), Savepoint.derive_stage(intent_dir))
77
83
  end