@zalom/plastic 1.3.0 → 1.4.1

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 (71) hide show
  1. package/PLASTIC-reference.md +8 -6
  2. package/PLASTIC.md +52 -14
  3. package/hooks/hooks.json +5 -0
  4. package/hooks/links-gate +3 -0
  5. package/package.json +1 -1
  6. package/scripts/codex-hook +122 -8
  7. package/scripts/dashboard.rb +323 -71
  8. package/scripts/doctor.rb +393 -58
  9. package/scripts/end-intent +347 -43
  10. package/scripts/hook-links-gate +74 -0
  11. package/scripts/hook-lock-gate +8 -3
  12. package/scripts/install.rb +51 -6
  13. package/scripts/lib/bridge.rb +105 -27
  14. package/scripts/lib/config_asks.rb +110 -0
  15. package/scripts/lib/graph_rebuild.rb +30 -6
  16. package/scripts/lib/hook_registry.rb +34 -3
  17. package/scripts/lib/installer_core.rb +70 -13
  18. package/scripts/lib/intent_validator.rb +38 -10
  19. package/scripts/lib/links_gate.rb +140 -0
  20. package/scripts/lib/links_projection.rb +71 -12
  21. package/scripts/lib/lock.rb +186 -11
  22. package/scripts/lib/power_tools.rb +57 -14
  23. package/scripts/lib/project_validator.rb +113 -0
  24. package/scripts/lib/qmd_hook.rb +12 -8
  25. package/scripts/lib/restore_intent_v1.rb +154 -0
  26. package/scripts/lib/roadmap_queue.rb +1 -1
  27. package/scripts/lib/roadmap_savepoint.rb +38 -10
  28. package/scripts/lib/store_discovery.rb +77 -0
  29. package/scripts/lib/store_provisioning.rb +21 -12
  30. package/scripts/new-intent +10 -12
  31. package/scripts/plastic-lock +76 -9
  32. package/scripts/project-links +132 -35
  33. package/scripts/provision-project-store +18 -5
  34. package/scripts/read-config +1 -0
  35. package/scripts/rebuild-graph +42 -17
  36. package/scripts/restore-intent-v1 +288 -0
  37. package/scripts/roadmap-next +9 -2
  38. package/scripts/roadmap-savepoint +9 -1
  39. package/scripts/update.rb +50 -1
  40. package/scripts/validate-intent +3 -1
  41. package/scripts/validate-project +53 -0
  42. package/scripts/write-config +105 -0
  43. package/skills/auto/SKILL.md +45 -16
  44. package/skills/auto/references/agent-architecture.md +7 -0
  45. package/skills/auto/references/end-tail.md +27 -13
  46. package/skills/dashboard/SKILL.md +48 -25
  47. package/skills/dashboard/evals/evals.json +4 -4
  48. package/skills/dashboard/templates/dashboard-global.md +3 -5
  49. package/skills/dashboard/templates/dashboard-project.md +6 -18
  50. package/skills/install/SKILL.md +4 -4
  51. package/skills/intent-creating/SKILL.md +5 -0
  52. package/skills/intent-ending/SKILL.md +49 -36
  53. package/skills/intent-locking/SKILL.md +20 -2
  54. package/skills/intent-starting/SKILL.md +6 -4
  55. package/skills/project-continuing/SKILL.md +10 -0
  56. package/skills/project-continuing/evals/evals.json +3 -3
  57. package/skills/project-continuing/references/board-fill.md +13 -11
  58. package/skills/project-creating/SKILL.md +29 -1
  59. package/skills/releasing/SKILL.md +37 -19
  60. package/skills/roadmap/SKILL.md +9 -7
  61. package/skills/roadmap/references/file-format.md +14 -10
  62. package/skills/roadmap/references/operations.md +22 -18
  63. package/skills/roadmap-continuing/SKILL.md +5 -5
  64. package/skills/roadmap-continuing/evals/evals.json +3 -3
  65. package/skills/roadmap-continuing/references/liveness-ranking.md +6 -5
  66. package/skills/tutorial/SKILL.md +4 -4
  67. package/skills/tutorial/references/track-1-guided.md +2 -1
  68. package/skills/tutorial/references/track-2-auto.md +2 -1
  69. package/skills/tutorial/references/track-3-projects-and-roadmaps.md +12 -11
  70. package/skills/update/SKILL.md +30 -17
  71. package/templates/roadmap.md +8 -8
@@ -39,7 +39,7 @@ ruby ~/.plastic/scripts/roadmap-next --roadmaps-dir <tier>/roadmaps
39
39
 
40
40
  Branch on `state`:
41
41
  - `dispatchable`: work its `dispatchable_queue` in `rank` order (the head is the next batch
42
- entry). These are the current batch's `queued` intents, parallel-safe within the wave.
42
+ entry). These are the current batch's `queued` intents, parallel-safe within the batch.
43
43
  - `in_flight`: the frontier batch is still delivering. Report it and wait. Do NOT dispatch a
44
44
  later batch and do NOT fall through to the dashboard, the roadmap is live.
45
45
  - `none` or `exhausted`: no roadmap, or nothing left to dispatch. Fall back to the dashboard
@@ -93,13 +93,15 @@ edited before the plan exists (the gate applies to YOU, the orchestrator):
93
93
 
94
94
  ```bash
95
95
  ruby -r ~/.plastic/scripts/lib/bridge -e \
96
- 'Bridge.arm_auto(ENV["CLAUDE_CODE_SESSION_ID"], intent_id: "<ID>", intent_dir: "<STORE>/<dir>", store: "<STORE>", name: "<name>")'
96
+ 'codex=ENV["CODEX_THREAD_ID"].to_s.strip; claude=ENV["CLAUDE_CODE_SESSION_ID"].to_s.strip; harness=!codex.empty? ? "codex" : (!claude.empty? ? "claude" : nil); session=!codex.empty? ? codex : (!claude.empty? ? claude : nil); Bridge.arm_auto(session, intent_id: "<ID>", intent_dir: "<STORE>/<dir>", store: "<STORE>", name: "<name>", harness: harness, agent: "plastic-enforcer", thread: (!codex.empty? ? codex : nil))'
97
97
  ```
98
98
 
99
99
  Replace `<ID>`, `<STORE>` (e.g. `~/.plastic/projects/<slug>/store` or `~/.plastic/store`),
100
100
  `<dir>` (the `ID--slug` directory), and `<name>`. The first argument is the session id you
101
- want the bridge keyed by: pass the hook stdin `session_id` when you have it, otherwise
102
- `ENV["CLAUDE_CODE_SESSION_ID"]`, otherwise `nil`. Arming always succeeds and acquires the
101
+ want the bridge keyed by: pass the hook stdin `session_id` when you have it. The executable
102
+ snippet trusts a nonblank `CODEX_THREAD_ID` as Codex, otherwise a nonblank
103
+ `CLAUDE_CODE_SESSION_ID` as Claude, otherwise leaves harness and thread unknown. Never guess
104
+ identity from an absent runtime variable. Arming always succeeds and acquires the
103
105
  durable `delivery.lock` in the intent dir. For the `resolve_session` fallback chain
104
106
  (why arming never needs a non-empty session env var, and what the lock ownership model
105
107
  implies for later tool calls) read `references/end-tail.md`.
@@ -148,12 +150,33 @@ The enforcer's session owns the delivery lock. Per-stage specialists run in
148
150
  their own sessions and would be denied by the lock gate, so register each one
149
151
  as a delegate before (or when) it needs to write into the intent dir:
150
152
 
151
- 1. Instruct each spawned specialist to report its session id
152
- (`CLAUDE_CODE_SESSION_ID`) in its first message.
153
+ 1. Instruct each spawned specialist to report its session id and runtime identity in its first
154
+ message: `CODEX_THREAD_ID` for Codex, or `CLAUDE_CODE_SESSION_ID` for Claude. Use the
155
+ specialist/hook identity when known; never infer a harness or model from missing context.
153
156
  2. As the lock owner, run:
154
- `ruby ~/.plastic/scripts/plastic-lock delegate --delegate <specialist-session-id>`
157
+ `ruby ~/.plastic/scripts/plastic-lock delegate --intent-dir <intent-dir> --delegate <specialist-session-id> --harness <specialist-harness-when-known> --agent <role> --model <resolved-model-when-known> --thread <reported-CODEX_THREAD_ID-when-Codex>`
158
+ Omit `--harness`, `--model`, or `--thread` when that value is unknown; `--agent <role>` is
159
+ always known from the dispatch roster.
155
160
  3. If a specialist hits a lock-gate deny, the deny message names this exact
156
161
  command; run it and have the specialist retry.
162
+ 4. Immediately after the specialist returns, and before validating or dispatching
163
+ the next handoff, classify the return and record its activity status as the owner:
164
+ - `finished` means the specialist returned a usable completion report, whether
165
+ agent-authored or synthesized through `scripts/agent-report`.
166
+ - `failed` means the specialist returned blocked, errored, or without a usable
167
+ completion report that can be synthesized.
168
+ 5. Record the classification with exactly one of:
169
+ ```bash
170
+ ruby ~/.plastic/scripts/plastic-lock delegate --intent-dir <intent-dir> \
171
+ --delegate <specialist-session-id> --status finished --harness <same-specialist-harness-when-known> \
172
+ --agent <same-role> --model <same-resolved-model-when-known> --thread <same-CODEX_THREAD_ID-when-Codex>
173
+ ruby ~/.plastic/scripts/plastic-lock delegate --intent-dir <intent-dir> \
174
+ --delegate <specialist-session-id> --status failed --harness <same-specialist-harness-when-known> \
175
+ --agent <same-role> --model <same-resolved-model-when-known> --thread <same-CODEX_THREAD_ID-when-Codex>
176
+ ```
177
+ Apply the same omission rule to unknown values on terminal status commands.
178
+ A failed specialist stops that handoff under the normal blocker/error procedure;
179
+ never dispatch the next specialist first.
157
180
 
158
181
  Only the owner can delegate. Delegates cannot re-delegate or release.
159
182
 
@@ -305,19 +328,25 @@ During initial project creation, all decisions are non-destructive by definition
305
328
  - Update `chain` in the current intent's frontmatter
306
329
  6. Run the mechanical close through `plastic-intent-ending`: it owns steps 1-6 of the Done
307
330
  procedure (outcome/INDEX/savepoint/commit, disarm, and the QMD reindex last) as ONE
308
- delegation, not five separate one-liners restated here. Run its backing script for the
309
- outcome/INDEX/savepoint/commit core, passing `--index-note` with a rich Completed/
310
- Abandoned entry description (mode/tier, what shipped or why abandoned, suite result):
331
+ delegation, not five separate one-liners restated here. `scripts/end-intent` now performs
332
+ steps 1-5 itself, INCLUDING disarm (worktree release plus clearing `delivery.lock`): a
333
+ single call closes the intent AND clears its lock, so exit 0 means both are done. Pass
334
+ `--session` (this session's id, or rely on the `CLAUDE_CODE_SESSION_ID` fallback) so
335
+ disarm resolves the right bridge, and `--index-note` with a rich Completed/Abandoned entry
336
+ description (mode/tier, what shipped or why abandoned, suite result):
311
337
  ```bash
312
338
  ruby ~/.plastic/scripts/end-intent --store <store_path> --id <ID> --disposition delivered \
339
+ --session "$CLAUDE_CODE_SESSION_ID" \
313
340
  --index-note "<mode, tier>; <what shipped>; <suite result>"
314
341
  ```
315
- (Use `--disposition abandoned` when the intent is being moved to `## Abandoned`.) Then
316
- follow `plastic-intent-ending`'s Step 5 (disarm: `Bridge.disarm_auto` on this auto/curator
317
- path, the plain-remove branch) and Step 6 (QMD reindex, async, last) exactly as that skill
318
- states them. Never leave an orphaned worktree; run `git worktree prune` on a stale
319
- reference. If any of this ever needs to change, change `plastic-intent-ending`, not this
320
- skill.
342
+ (Use `--disposition abandoned` when the intent is being moved to `## Abandoned`.) A
343
+ non-zero exit needs attention before moving on: 4 means a live foreign session holds the
344
+ lock (back off), 5 means the code worktree is dirty (commit/stash first, or pass
345
+ `--discard-worktree-changes` deliberately), 3 means disarm ran but the lock is still
346
+ present (run `/plastic-doctor check the lock status`). Only Step 6 (QMD reindex, async,
347
+ last) remains a separate action after this call succeeds. Never leave an orphaned
348
+ worktree; run `git worktree prune` on a stale reference. If any of this ever needs to
349
+ change, change `plastic-intent-ending`, not this skill.
321
350
  7. Notify user (Done briefing): brief per `references/human-report-contract.md`
322
351
  (State: the delivered impact; Risk: residual risk; Call: the decision left to you, merge,
323
352
  release, or accept). See `outcome.md` for details.
@@ -81,6 +81,13 @@ account therefore always exists: agent-authored when present, deterministically
81
81
  otherwise. This structures the finish notification only; in-flight observations stay in
82
82
  `## Insights`, no progress chatter is added.
83
83
 
84
+ Immediately after a specialist returns and before the next handoff, the enforcer records the
85
+ delegate's activity through `plastic-lock delegate --intent-dir <intent-dir> --delegate <id>
86
+ --status finished|failed`. `finished` requires a usable agent-authored or synthesized completion
87
+ report. A blocked or errored return, or one with no report that can be synthesized, is `failed`
88
+ and stops the handoff under the normal error procedure. Activity status is descriptive and does
89
+ not revoke the registered delegate's authorization.
90
+
84
91
  ### Gate Ownership
85
92
 
86
93
  The enforcer arms and verifies the lifecycle gate, then gates every stage transition.
@@ -33,19 +33,33 @@ purge-eligible. Disarming also purges stale bridge files from the temp directory
33
33
  automatically (it keeps the current bridge, any live run, and any bridge whose intent
34
34
  still holds a delivery lock), so no manual `/tmp` cleanup is needed.
35
35
 
36
- **Worktree cleanup (mandatory, intent 73c3).** Disarming performs the worktree release:
37
- `disarm_auto` calls `Worktree.release`, which removes both per-intent worktrees (the code
38
- worktree under `<repo>/.claude/worktrees/{id}--{slug}` and the paired store worktree under
39
- `<plastic_home>/.worktrees/{id}--{slug}`), prunes both repos, and clears the worktree block
40
- from the bridge. This is the plain remove path: the disarm route does NOT merge, so use it
41
- only when no release merges the branch (the branch survives and can be reclaimed).
42
-
43
- When the work is being shipped through a release, do NOT rely on this plain remove. The
44
- release path (Completion step 4, via `plastic-releasing`) is responsible for merging the
45
- intent's code branch (`plastic/{id}--{slug}`) back to the repo's default branch BEFORE the
46
- worktree is removed, so the integrated work is not lost. It does this with
47
- `Worktree.finish(bridge_data, merge: true)` (merge-then-remove). Never leave an orphaned
48
- worktree, and run `git worktree prune` if you hit a stale reference.
36
+ **Mechanized since intent 188.** `scripts/end-intent` performs this disarm itself, as its
37
+ own step 5, after steps 1-4 (outcome/INDEX/savepoint/commit) commit. No agent needs to run
38
+ a separate `Bridge.disarm_auto` one-liner any more on the auto mode / curator path: the
39
+ single `end-intent` call in `SKILL.md`'s Completion section already does it. A pre-flight
40
+ lock guard (before anything is written) refuses on a live foreign session (exit 4) and
41
+ reclaims a stale foreign lock automatically (audited to savepoint.md); a dirty code
42
+ worktree refuses before removal (exit 5, `--discard-worktree-changes` overrides
43
+ deliberately); and the durable lock file is checked again after disarm, never merely
44
+ trusted (exit 3 if it is somehow still present).
45
+
46
+ **Worktree cleanup (mandatory, intent 73c3).** `end-intent`'s step 5 calls
47
+ `Bridge.disarm_auto` by default, which calls `Worktree.release`, which removes both
48
+ per-intent worktrees (the code worktree under `<repo>/.claude/worktrees/{id}--{slug}` and
49
+ the paired store worktree under `<plastic_home>/.worktrees/{id}--{slug}`), prunes both
50
+ repos, and clears the worktree block from the bridge. This is the plain remove path: the
51
+ disarm route does NOT merge, so use it only when no release merges the branch (the branch
52
+ survives and can be reclaimed).
53
+
54
+ When the work is being shipped through a release, do NOT rely on this plain remove.
55
+ `skills/releasing/SKILL.md` reorders its own two steps for exactly this reason (intent 188,
56
+ D7): its worktree-merge step now runs BEFORE its `end-intent` call, merging the intent's
57
+ code branch (`plastic/{id}--{slug}`) back to the repo's default branch BEFORE the worktree
58
+ is removed, via `Worktree.finish(bridge_data, merge: true)` (merge-then-remove), so the
59
+ integrated work is not lost. By the time `end-intent`'s own step 5 runs afterward, the
60
+ worktree is already gone (a harmless no-op) and only the delivery lock is left to clear,
61
+ correctly, for the first time on that path. Never leave an orphaned worktree, and run
62
+ `git worktree prune` if you hit a stale reference.
49
63
 
50
64
  ## QMD reindex ordering rationale
51
65
 
@@ -33,14 +33,19 @@ ruby ~/.plastic/scripts/dashboard.rb [continue|project <slug>] --data
33
33
  - `continue` (default) → the **global** board payload (`mode: "global"`).
34
34
  - `project <slug>` → that **project** board payload (`mode: "project"`).
35
35
 
36
- The payload is read-only JSON. Global-board fields: `date`, `store_health`, `recently_worked`,
37
- `next_work`, `counts`, `projects`, `project_totals`. Project-board fields: `slug`, `store_health`,
38
- `description`, `recently_worked`, `next_work`, `counts`, `active`, `future`. Each list carries
39
- cell-ready fields for its table: `next_work` rows are
36
+ The payload is read-only JSON. Global-board fields: `date`, `store_health`, `summary`,
37
+ `next_work`, `next_total`, `next_shown`, `counts`, `projects`, `project_totals`, `footer`.
38
+ Project-board fields: `slug`, `store_health`, `description`, `summary`, `counts`, `active`,
39
+ `active_total`, `active_shown`, `next_work`, `next_total`, `next_shown`, `footer`. `summary`
40
+ and `footer` are finished prose strings (2-3 sentences and one line respectively), built in
41
+ `dashboard.rb` and substituted verbatim, exactly like `{{date}}`/`{{description}}` already
42
+ are - never re-worded or re-derived by the skill. Each list carries cell-ready fields for
43
+ its table: `next_work` rows are
40
44
  `{id, intent, scope, lifecycle, value, disposition, flags, what, flags_label, line}`;
41
- `recently_worked` rows carry `{id, status, glyph, last_accessed_at, what, state, scope, line}`;
42
- `active`/`future` rows carry `{id, intent, created, bullet, scope, what, stage, line}`. The `what`,
43
- `scope`, and `flags_label` cell fields arrive pipe-escaped and whitespace-normalized.
45
+ `active` rows carry
46
+ `{id, intent, created, bullet, scope, what, stage, worker, activity, line}`. The `what`,
47
+ `scope`, `worker`, `activity`, and `flags_label` cell fields arrive pipe-escaped and
48
+ whitespace-normalized.
44
49
 
45
50
  Each board load runs the scoped store check (`doctor --store <scope>`): the global board runs
46
51
  `--store global` and a project board runs `--store <slug>`. The result rides in the payload as
@@ -57,24 +62,42 @@ Templates live in this skill's `templates/` directory:
57
62
 
58
63
  Fill mechanically, no rewriting, no re-sorting:
59
64
  - `{{a.b.count}}` → the integer (e.g. `counts.active` = that count).
60
- - `{{<list>.rows}}` → the four intent lists (`recently_worked`, `next_work`, `active`, `future`)
61
- render as **Markdown table rows**. The template hard-codes each table's header and separator;
62
- this placeholder becomes one data row per list entry, joined with real newlines, in that table's
63
- fixed column order (below). Drop each cell from the named payload field **verbatim**: cells
64
- arrive pre-escaped and whitespace-normalized from the script (pipes escaped as `\|`), so never
65
- re-escape, re-truncate, or reword them. Never emit `<br>`.
66
- - `recently_worked` (global) → `| {id} | {what} | {state} | {scope} |`
67
- - `recently_worked` (project) → `| {id} | {what} | {state} |`
65
+ - `{{<list>.rows}}` → the two intent lists (`active`, `next_work`) render as **Markdown table
66
+ rows**. The template hard-codes each table's header and separator; this placeholder becomes
67
+ one data row per list entry, joined with real newlines, in that table's fixed column order
68
+ (below). Drop each cell from the named payload field **verbatim**: cells arrive pre-escaped
69
+ and whitespace-normalized from the script (pipes escaped as `\|`), so never re-escape,
70
+ re-truncate, or reword them. Never emit `<br>`.
68
71
  - `next_work` → `| {id} | {what} | {value} | {disposition} | {flags_label} |`
69
- - `active` → `| {id} | {what} | {stage} |`
70
- - `future` `| {id} | {what} |`
71
- Overflow entry (empty `id`, `what` = `+N more`) one row with `+N more` in the Id column and
72
- every other cell blank. Empty list one full-width row with `_(none)_` in the Id column and
73
- every other cell blank, matching that table's column count (e.g. `| _(none)_ | | | | |` for
74
- the 5-column next_work table, `| _(none)_ | |` for the 2-column future table).
75
- - `{{projects.lines}}` the project rollup stays **prose**, one line per project (not a table):
72
+ - `active` → `| {id} | {what} | {stage} | {worker} | {activity} |`
73
+ Empty list → one full-width row with `_(none)_` in the Id column and every other cell blank,
74
+ matching that table's column count (e.g. `| _(none)_ | | | | |` for the 5-column next_work
75
+ table, `| _(none)_ | | | |` for the 5-column active table). Neither list carries an overflow
76
+ "+N more" row anymore (D5, intent 202): the true pool size rides on the payload as
77
+ `active_total`/`next_total` (shown counts as `active_shown`/`next_shown`), and `{{footer}}`
78
+ states it in prose instead.
79
+ - `{{projects.lines}}` (global board only) → the project rollup stays **prose**, one line per
80
+ project (not a table):
76
81
  `- **{slug}**: {description}, active {active}, done {done}, future {future}, last accessed {last_accessed_at[0,10]}`.
77
- - Scalars (`{{date}}`, `{{slug}}`, `{{description}}`) → substitute verbatim.
82
+ - Scalars (`{{date}}`, `{{slug}}`, `{{summary}}`, `{{footer}}`) → substitute verbatim. `summary`
83
+ and `footer` are finished prose built in `dashboard.rb`; do not rewrite, shorten, or
84
+ re-derive them from the counts - that is exactly the non-determinism D5 rules out.
85
+
86
+ ### Paging (conversational, D4)
87
+
88
+ The board shows a short page by default (Active capped at 3, Next-work at 5). When the
89
+ user asks for "more" or "all", re-invoke Step 1 with an explicit flag and re-fill the
90
+ template with the new payload - nothing is persisted to disk, the offset lives only in the
91
+ chat turn:
92
+ - "all" → add `--all` (lifts both caps to unbounded; the footer then shows equal shown/total).
93
+ - "more" → add `--limit-active N`/`--limit-next N` with a larger `N` for whichever list the
94
+ user is paging.
95
+
96
+ For a real, own-terminal pager instead, point the owner at `--plain`:
97
+ `ruby ~/.plastic/scripts/dashboard.rb project <slug> --plain | less` (or `continue --plain`
98
+ for the global board). `--plain` prints the full, uncapped board as plain text with no
99
+ Markdown table syntax; it is a separate CLI mode from `--data`, not something this skill
100
+ fills a template from.
78
101
 
79
102
  ### Step 3 — Present it (mandatory, every invocation)
80
103
 
@@ -139,8 +162,8 @@ intentional change means the skill is broken.
139
162
 
140
163
  ## Notes
141
164
 
142
- - The four intent lists (recently worked, active, future, next work) render as Markdown tables;
143
- the narrative wrappers, counts, and the project rollup stay prose. No Value x Effort grid
165
+ - The two intent lists (active, next work) render as Markdown tables; the prose summary,
166
+ footer, counts, and the project rollup stay prose (intent 202). No Value x Effort grid
144
167
  returns. Never emit `<br>`.
145
168
  - Clusters (Zettelkasten grouping in INDEX.md) are intentionally not rendered.
146
169
  - Additive: changes no core lifecycle, gate, or cycle logic.
@@ -22,14 +22,14 @@
22
22
  "id": 2,
23
23
  "scope": "behavior",
24
24
  "set": "validation",
25
- "prompt": "How do the four intent lists render on the Markdown boards?",
26
- "expected_output": "recently_worked, next_work, active, and future render as Markdown tables (fixed columns: next_work Id|What|Value|Disposition|Flags; active Id|What|Stage; future Id|What; recently_worked Id|What|State, plus a Scope column on the global board). The narrative wrappers, counts, and the projects rollup stay prose; no Value x Effort grid. Cells arrive pre-escaped from the payload and are dropped verbatim.",
25
+ "prompt": "How does the project board render after intent 202 (short by default, paged on request)?",
26
+ "expected_output": "The project board shows, in order, and nothing else: a 2-3 sentence prose summary of what was delivered most recently (built in dashboard.rb from completed/completed_on data, not the 24h-windowed recently_worked), an Active table capped at 3 rows (Id|What|Stage, ordered lifecycle-stage descending with a savepoint tie-break), a Next-work table capped at 5 rows (Id|What|Value|Disposition|Flags), and a one-line footer stating true totals (e.g. '3 of 12 active, 5 of 47 next work') plus how to see everything. The raw Future table and its {{future.rows}} placeholder, and the separate Recently-worked table, are both gone from dashboard-project.md. dashboard-global.md carries the equivalent fix (D6): a prose summary in place of its recently-worked table, the same honest footer, and the same --limit-active/--limit-next/--all/--plain mechanics.",
27
27
  "files": ["skills/dashboard/templates/dashboard-global.md", "skills/dashboard/templates/dashboard-project.md", "skills/dashboard/SKILL.md"],
28
28
  "assertions": [
29
29
  {
30
30
  "type": "convention",
31
- "check": "the four intent lists render as Markdown tables with the fixed columns; wrappers and the projects rollup stay prose; no grid",
32
- "observed": "templates hard-code table headers plus {{list.rows}} for recently_worked/next_work/active/future; SKILL.md Step 2 documents the fixed column order and verbatim pre-escaped cell fill; projects stays {{projects.lines}} prose",
31
+ "check": "project board template has summary+active+next_work+footer only (no future, no recently_worked table); global template has summary+footer added; SKILL.md documents the new payload fields and CLI flags",
32
+ "observed": "templates carry {{summary}}, {{active.rows}} (Id|What|Stage), {{next_work.rows}} (Id|What|Value|Disposition|Flags), {{footer}}; {{future.rows}} and the recently-worked table are removed from both; SKILL.md Step 1/2 document summary/footer/active_total/active_shown/next_total/next_shown and --limit-active/--limit-next/--all/--plain",
33
33
  "result": "pass"
34
34
  }
35
35
  ]
@@ -1,10 +1,6 @@
1
1
  # 🧩 Plastic · Global Board, {{date}}
2
2
 
3
- **Recently worked** (last 24h)
4
-
5
- | Id | What | State | Scope |
6
- | --- | --- | --- | --- |
7
- {{recently_worked.rows}}
3
+ {{summary}}
8
4
 
9
5
  ## Where we are
10
6
 
@@ -19,4 +15,6 @@
19
15
  | --- | --- | --- | --- | --- |
20
16
  {{next_work.rows}}
21
17
 
18
+ {{footer}}
19
+
22
20
  **What would you like to work on next?** (type an **intent id**, a **project name**, or anything **new** you'd like to start)
@@ -1,31 +1,19 @@
1
1
  # 📦 {{slug}} · Project Board, {{date}}
2
2
 
3
- {{description}}
4
-
5
- **Recently worked** (last active work, last 24h)
6
-
7
- | Id | What | State |
8
- | --- | --- | --- |
9
- {{recently_worked.rows}}
10
-
11
- ## Intents, active {{counts.active}}, done {{counts.done}}, future {{counts.future}}
3
+ {{summary}}
12
4
 
13
5
  **Active**
14
6
 
15
- | Id | What | Stage |
16
- | --- | --- | --- |
7
+ | Id | What | Stage | Worker | Activity |
8
+ | --- | --- | --- | --- | --- |
17
9
  {{active.rows}}
18
10
 
19
- **Future**
20
-
21
- | Id | What |
22
- | --- | --- |
23
- {{future.rows}}
24
-
25
- ## Most-valuable next work
11
+ **Most-valuable next work**
26
12
 
27
13
  | Id | What | Value | Disposition | Flags |
28
14
  | --- | --- | --- | --- | --- |
29
15
  {{next_work.rows}}
30
16
 
17
+ {{footer}}
18
+
31
19
  **What would you like to work on next?** (type an **intent id**, or **global** to go back)
@@ -103,10 +103,10 @@ Non-interactive sessions (no tty) skip the question entirely: the install ships
103
103
  shipped default, advisor enabled with no `--advisor` flag (the `plastic-agent-advisor`
104
104
  skill's own routing falls back to `plastic-faux-advisor` at consult time).
105
105
 
106
- Update flow: if `advisor.enabled` or `advisor.claude.default` are already set in
107
- `~/.plastic/config.yml`, do not re-ask; the existing value is respected, the same
108
- ask-once-when-unset rule the statusline choice below already follows. Either flag can
109
- also be passed again on `install --reinstall` to change a prior answer.
106
+ Update flow: pending config questions, including this one, are now announced
107
+ generically by `plastic-update`'s Step 2, sourced from `config_asks.yml` - not
108
+ duplicated here. A value already set by either path is never re-asked by the
109
+ other.
110
110
 
111
111
  **Statusline**
112
112
 
@@ -69,6 +69,11 @@ Having a "parent" in mind does NOT automatically mean branch. Choose by meaning:
69
69
  `--sources` when it was genuinely created from / emerged from that intent's lifecycle.
70
70
  Topic similarity alone is not a `sources` edge.
71
71
 
72
+ When a branch intent exists because a late ruling arrived AFTER its parent was already
73
+ completed (the owner's late-ruling rule), the parent is restored to v1 via
74
+ `scripts/restore-intent-v1`, never by a hand-run `git checkout`/revert. See `PLASTIC.md` >
75
+ Terminal immutability for the rule and the tool.
76
+
72
77
  `## Links` is a DERIVED view of `sources`/`chain`. Never hand-write a `## Links` line; add the
73
78
  frontmatter edge and reproject. Links are decided by context influence (a `chain` edge has a high
74
79
  bar: the candidate's context must materially help deliver this intent), not by shared files or a
@@ -24,15 +24,16 @@ failure branch: only outcome.md content and the INDEX section differ.
24
24
  | 2 | INDEX.md terminal move (Active -> Completed/Abandoned) | `scripts/end-intent` |
25
25
  | 3 | savepoint `Done` bookend | `scripts/end-intent` |
26
26
  | 4 | store auto-commit | `scripts/end-intent` |
27
- | 5 | disarm (worktree + lock) | You, after end-intent exits 0 |
27
+ | 5 | disarm (worktree + lock) | `scripts/end-intent` (intent 188) |
28
28
  | 6 | QMD reindex, async, LAST | You |
29
29
  | 7 | EM-to-CTO report | You |
30
30
 
31
- Steps 1-4 are ONE callable script, not four separate one-liners: this is
32
- exactly what the failure mode this intent fixes looked like (releasing hand
31
+ Steps 1-5 are ONE callable script call, not several separate one-liners: this
32
+ is exactly what the failure mode this intent fixes looked like (releasing hand
33
33
  authored the close in prose and dropped the savepoint bookend for two real
34
- deliveries). Never restate outcome/INDEX/savepoint prose inline again; call
35
- `scripts/end-intent`.
34
+ deliveries; separately, one session delivered four intents back to back and
35
+ never ran the old step-5 one-liner at all, intent 188). Never restate
36
+ outcome/INDEX/savepoint/disarm prose inline again; call `scripts/end-intent`.
36
37
 
37
38
  ### Step 0. Precondition (the gate is section-blind, not selective)
38
39
 
@@ -55,7 +56,7 @@ for orchestrator-owned or completion-tracking items.
55
56
  (tick it if the described work is actually done, or do the remaining
56
57
  work); do not attempt outcome.md and fight the gate's deny.
57
58
 
58
- ### Step 1-4. Run `scripts/end-intent`
59
+ ### Step 1-5. Run `scripts/end-intent`
59
60
 
60
61
  First author outcome.md for real (never leave the scaffold placeholder in
61
62
  place): copy `templates/outcome.md`, set the frontmatter to
@@ -72,40 +73,52 @@ Then call the script once:
72
73
  ```bash
73
74
  ruby ~/.plastic/scripts/end-intent \
74
75
  --store <store_path> --id <intent_id> --disposition delivered|abandoned \
76
+ --session "$CLAUDE_CODE_SESSION_ID" \
75
77
  --outcome-summary "<one-line ## Outcome summary for the intent file>" \
76
78
  --index-note "<rich Completed/Abandoned entry description>"
77
79
  ```
78
80
 
79
- This does all of steps 1-4 in order: guards outcome.md (refuses a missing,
81
+ This does all of steps 1-5 in order: guards outcome.md (refuses a missing,
80
82
  still-placeholder, or wrong-disposition file with exit 2 and authors
81
83
  nothing), stamps the intent file's `## Outcome` section, moves the INDEX.md
82
84
  line from `## Active` to `## Completed` or `## Abandoned` (dated today,
83
- idempotent) with the `--index-note` text appended after the date so the
84
- entry stays rich, appends the savepoint `Done` bookend, and commits the
85
- store repo. Omit `--index-note` for a thin id+date entry, add `--no-commit`
86
- when a separate commit step already covers the store, and `--dry-run` to
87
- preview with no writes. Exit 0 is success; exit 1 is a usage or resolution
88
- failure; exit 2 is the outcome.md guard refusing (fix outcome.md and re-run,
89
- nothing was written).
90
-
91
- ### Step 5. Disarm (worktree + lock)
92
-
93
- Two branches, decided by how this intent ships:
94
-
95
- - **Shipped through a release** (the `plastic-releasing` flow reached this
96
- close): merge the code branch back BEFORE removing the worktrees, via
97
- `Worktree.finish(bridge_data, merge: true)`. Releasing's own workflow
98
- already drives this; this skill's job here is only the outcome/INDEX/
99
- savepoint/commit core above.
100
- - **Auto mode or the curator path** (no release involved): plain remove,
101
- branch survives for reclaim.
102
- ```bash
103
- ruby -r ~/.plastic/scripts/lib/bridge -e \
104
- 'Bridge.disarm_auto(ENV["CLAUDE_CODE_SESSION_ID"], intent_id: "<ID>")'
105
- ```
106
- `disarm_auto` releases both worktrees, clears the `delivery.lock`, and only
107
- then makes the bridge purge-eligible, in that order. Never leave an
108
- orphaned worktree; run `git worktree prune` on a stale reference.
85
+ idempotent, accepting either a real em dash or a plain hyphen as the id/
86
+ title separator on read while always emitting the real em dash on write)
87
+ with the `--index-note` text appended after the date so the entry stays
88
+ rich, appends the savepoint `Done` bookend, commits the store repo, and
89
+ disarms (releases the code worktree and clears `delivery.lock`, verified
90
+ against the durable lock file on disk, never merely trusted). Omit
91
+ `--index-note` for a thin id+date entry, add `--no-commit` when a separate
92
+ commit step already covers the store (this never skips disarm), and
93
+ `--dry-run` to preview steps 1-5 with no writes.
94
+
95
+ A pre-flight lock guard runs before anything is written: it resolves the
96
+ calling session (`--session`, else `CLAUDE_CODE_SESSION_ID`, else the
97
+ existing lock's own recorded owner, else a no-op) and checks it against any
98
+ existing `delivery.lock`. A live foreign session refuses the whole run
99
+ (exit 4, nothing written); a stale foreign lock is reclaimed automatically
100
+ (audited to savepoint.md) and the run proceeds as the new owner. Before
101
+ removing the worktree, step 5 also refuses on an unexpectedly dirty code
102
+ worktree (exit 5, naming the worktree path) rather than force-discarding
103
+ uncommitted changes; pass `--discard-worktree-changes` only when you mean
104
+ to override that deliberately.
105
+
106
+ On the auto mode / curator path (no release), this single call performs the
107
+ FULL disarm (plain worktree remove, since the branch survives for later
108
+ reclaim). On a release-shipped path, `skills/releasing/SKILL.md` merges and
109
+ removes the worktree FIRST (its own step 8, merge-then-remove) before ever
110
+ calling this script, so by the time this call's step 5 runs, the worktree is
111
+ already gone (a harmless no-op) and only the lock is left to clear,
112
+ correctly, for the first time on that path (D7).
113
+
114
+ Exit codes: 0 success (the intent is closed AND its delivery lock is gone);
115
+ 1 a usage or resolution failure, OR an INDEX id that resolves to neither
116
+ `## Active` nor the terminal section; 2 the outcome.md guard refusing (fix
117
+ outcome.md and re-run, nothing was written); 3 steps 1-4 already committed
118
+ but disarm could not verify the lock is gone afterward (run `/plastic-doctor
119
+ check the lock status`); 4 a live foreign session holds the lock (back off);
120
+ 5 the code worktree is dirty (commit/stash first, or pass
121
+ `--discard-worktree-changes` deliberately).
109
122
 
110
123
  ### Step 6. QMD reindex, LAST
111
124
 
@@ -146,6 +159,6 @@ historical record of what was planned.
146
159
 
147
160
  `plastic-releasing`, `plastic-auto`, the curator agent, `store-curating`, and
148
161
  `store-indexing` all delegate their mechanical close to this skill (or call
149
- `scripts/end-intent` directly for steps 1-4). None of them restate the
150
- outcome/INDEX/savepoint prose inline any more; if you find one that does,
151
- that surface has drifted and should route here instead.
162
+ `scripts/end-intent` directly for steps 1-5). None of them restate the
163
+ outcome/INDEX/savepoint/disarm prose inline any more; if you find one that
164
+ does, that surface has drifted and should route here instead.
@@ -19,23 +19,41 @@ Run from the project (the intent resolves from this session's bridge), or pass
19
19
 
20
20
  | Verb | What it does | When |
21
21
  |---|---|---|
22
+ | `who` | Print a compact owner, heartbeat, claim, and delegate view from durable files only | Safe human inspection; requires `--intent-dir` |
22
23
  | `status` | Report the lock file, bridge cache, freshness, agreement | Always safe; run first |
23
24
  | `fix` | Idempotent repair: rebuild lock + bridge from disk truth for THIS session. Never touches a fresh foreign lock | Interrupted work, corrupted state, /tmp wiped, legacy pid locks |
24
25
  | `release` | Owner clears the lock | Ending or abandoning a boarding |
25
26
  | `reclaim` | Explicit takeover of a STALE lock; appends an audit line to savepoint.md | The owner is gone and the lease expired |
26
- | `delegate` | Owner registers a subagent session under the lock (`--delegate <session-id>`) | Auto-mode orchestration |
27
+ | `delegate` | Owner registers a subagent session and optional provenance, or marks it `finished`/`failed` | Auto-mode orchestration |
27
28
 
28
29
  ```
29
30
  ruby ~/.plastic/scripts/plastic-lock status
31
+ ruby ~/.plastic/scripts/plastic-lock who --intent-dir <store>/<id>--<slug>
30
32
  ruby ~/.plastic/scripts/plastic-lock fix --intent-dir <store>/<id>--<slug>
31
33
  ruby ~/.plastic/scripts/plastic-lock reclaim --intent-dir <store>/<id>--<slug>
32
- ruby ~/.plastic/scripts/plastic-lock delegate --delegate <subagent-session-id>
34
+ ruby ~/.plastic/scripts/plastic-lock delegate --delegate <subagent-session-id> \
35
+ --harness codex --agent plastic-executor --model <model> --thread <thread-id>
36
+ ruby ~/.plastic/scripts/plastic-lock delegate --delegate <subagent-session-id> --status finished
33
37
  ```
34
38
 
39
+ When the current controller knows its provenance, `fix` and `reclaim` accept
40
+ `--harness`, `--agent`, `--model`, `--thread`, and `--mode auto|guided`.
41
+ Provenance is descriptive; the session remains the authorization identity.
42
+
35
43
  ## Rules
36
44
 
37
45
  - `fix` exits non-zero when another session holds a FRESH lock: back off, do
38
46
  not retry in a loop. `status` shows the owner.
47
+ - `who` is strictly read-only. It reads `delivery.lock`, its mtime, and claim
48
+ files; it never reads or repairs a bridge, searches transcripts, heartbeats,
49
+ or writes. Missing legacy provenance displays as `Unknown` rather than being
50
+ inferred.
51
+ - The `delivery.lock` file mtime is the sole heartbeat and freshness truth.
52
+ Provenance timestamps do not replace it.
53
+ - Only the lock owner may register delegates or mark them `finished` or
54
+ `failed`. Terminal status is observational and does not remove the delegate
55
+ session from the authorization list. Finished and failed activity history is
56
+ bounded to the 20 most recent terminal entries.
39
57
  - `reclaim` refuses a fresh lock. There is no silent reclaim anywhere; every
40
58
  takeover is audited in the intent's savepoint.md.
41
59
  - Acquiring a lock for new work is NOT this skill's job: board through
@@ -47,10 +47,10 @@ enforces it: without a held lock, mutating writes to this active intent's dir ar
47
47
  ```bash
48
48
  # guided (lock only):
49
49
  ruby -r ~/.plastic/scripts/lib/bridge -e \
50
- 'Bridge.arm_guided(ENV["CLAUDE_CODE_SESSION_ID"], intent_id: "<ID>", intent_dir: "<STORE>/<dir>", store: "<STORE>", name: "<name>")'
50
+ 'codex=ENV["CODEX_THREAD_ID"].to_s.strip; claude=ENV["CLAUDE_CODE_SESSION_ID"].to_s.strip; harness=!codex.empty? ? "codex" : (!claude.empty? ? "claude" : nil); session=!codex.empty? ? codex : (!claude.empty? ? claude : nil); Bridge.arm_guided(session, intent_id: "<ID>", intent_dir: "<STORE>/<dir>", store: "<STORE>", name: "<name>", harness: harness, agent: "plastic-enforcer", thread: (!codex.empty? ? codex : nil))'
51
51
  # auto (lock + auto), then hand to plastic-auto:
52
52
  ruby -r ~/.plastic/scripts/lib/bridge -e \
53
- 'Bridge.arm_auto(ENV["CLAUDE_CODE_SESSION_ID"], intent_id: "<ID>", intent_dir: "<STORE>/<dir>", store: "<STORE>", name: "<name>")'
53
+ 'codex=ENV["CODEX_THREAD_ID"].to_s.strip; claude=ENV["CLAUDE_CODE_SESSION_ID"].to_s.strip; harness=!codex.empty? ? "codex" : (!claude.empty? ? "claude" : nil); session=!codex.empty? ? codex : (!claude.empty? ? claude : nil); Bridge.arm_auto(session, intent_id: "<ID>", intent_dir: "<STORE>/<dir>", store: "<STORE>", name: "<name>", harness: harness, agent: "plastic-enforcer", thread: (!codex.empty? ? codex : nil))'
54
54
  ```
55
55
  Replace `<ID>`, `<STORE>` (`~/.plastic/projects/<slug>/store` or `~/.plastic/store`),
56
56
  `<dir>` (the `ID--slug` directory), and `<name>`.
@@ -67,8 +67,10 @@ enforces it: without a held lock, mutating writes to this active intent's dir ar
67
67
  discovery yields nothing, proceed to Why normally.
68
68
 
69
69
  **Session id resolution (verbatim from `plastic-auto`).** The first argument is the session
70
- id the bridge is keyed by: pass the hook stdin `session_id` when you have it, otherwise
71
- `ENV["CLAUDE_CODE_SESSION_ID"]`, otherwise `nil`. Both arms call `resolve_session`, which
70
+ id the bridge is keyed by: pass the hook stdin `session_id` when you have it; in the executable
71
+ snippets, a nonblank `CODEX_THREAD_ID` identifies Codex, otherwise a nonblank
72
+ `CLAUDE_CODE_SESSION_ID` identifies Claude, otherwise identity remains unknown. Never infer a
73
+ harness from an absent variable. Both arms call `resolve_session`, which
72
74
  picks the first non-empty of: the explicit id you pass → `CLAUDE_CODE_SESSION_ID` → a
73
75
  deterministic derived key (a hash of the store and intent id). It never returns nil, so the
74
76
  lock is taken even when every session env var is empty; arming prints a one-line stderr
@@ -104,6 +104,16 @@ state and stops, asking nothing (unchanged): it does not itself dispatch, re-ran
104
104
  roadmap. The global store and any project with no roadmap report `none`, so this board stays the
105
105
  default route for them.
106
106
 
107
+ Intent 202 has landed on top of 149/149a: the project board is short by default. The
108
+ Recently-worked table and the raw Future table are both gone, replaced by a 2-3 sentence
109
+ prose summary (built in `dashboard.rb`, not by this skill) plus a one-line footer stating
110
+ true totals. Active is capped at 3 (ordered lifecycle-stage descending, a later savepoint
111
+ breaking a tie - D2), Next-work at 5. Conversational paging ("more"/"all") re-invokes
112
+ `dashboard.rb ... --data` with `--limit-active`/`--limit-next`/`--all`, carrying no state on
113
+ disk; `--plain` prints the full uncapped board as plain text for a real pager. The rule-name
114
+ citations and the `dashboard.rb project <slug> --data` -> `dashboard-project.md` path still
115
+ resolve.
116
+
107
117
  ## References
108
118
 
109
119
  - `references/board-fill.md` - the template-fill mechanics and store-health surfacing detail.