@zalom/plastic 1.4.0 → 1.5.0

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 (44) hide show
  1. package/PLASTIC-reference.md +2 -0
  2. package/PLASTIC.md +160 -42
  3. package/agents/plastic-intent-curator.md +10 -2
  4. package/package.json +1 -1
  5. package/scripts/codex-hook +122 -8
  6. package/scripts/dashboard.rb +323 -71
  7. package/scripts/doctor.rb +271 -15
  8. package/scripts/end-intent +32 -7
  9. package/scripts/hook-lock-gate +8 -3
  10. package/scripts/install.rb +51 -6
  11. package/scripts/lib/bridge.rb +79 -30
  12. package/scripts/lib/hook_registry.rb +44 -2
  13. package/scripts/lib/installer_core.rb +45 -6
  14. package/scripts/lib/lock.rb +186 -11
  15. package/scripts/lib/maintenance_git.rb +94 -0
  16. package/scripts/lib/revisions_writer.rb +69 -0
  17. package/scripts/lib/worktree.rb +14 -32
  18. package/scripts/lib/worktree_sweep.rb +129 -0
  19. package/scripts/maintenance-run +236 -0
  20. package/scripts/plastic-lock +76 -9
  21. package/scripts/project-links +127 -24
  22. package/scripts/rebuild-graph +37 -3
  23. package/scripts/restore-intent-v1 +37 -3
  24. package/scripts/sweep-store-worktrees +53 -0
  25. package/skills/auto/SKILL.md +29 -6
  26. package/skills/auto/references/agent-architecture.md +7 -0
  27. package/skills/auto/references/end-tail.md +8 -6
  28. package/skills/dashboard/SKILL.md +48 -25
  29. package/skills/dashboard/evals/evals.json +4 -4
  30. package/skills/dashboard/templates/dashboard-global.md +3 -5
  31. package/skills/dashboard/templates/dashboard-project.md +6 -18
  32. package/skills/doctor/SKILL.md +6 -0
  33. package/skills/intent-locking/SKILL.md +20 -2
  34. package/skills/intent-starting/SKILL.md +6 -4
  35. package/skills/project-continuing/SKILL.md +10 -0
  36. package/skills/project-continuing/evals/evals.json +3 -3
  37. package/skills/project-continuing/references/board-fill.md +13 -11
  38. package/skills/releasing/SKILL.md +3 -3
  39. package/skills/store-curating/SKILL.md +9 -0
  40. package/skills/store-curating/evals/evals.json +16 -0
  41. package/skills/tutorial/SKILL.md +4 -4
  42. package/skills/tutorial/references/track-1-guided.md +2 -1
  43. package/skills/tutorial/references/track-2-auto.md +2 -1
  44. package/skills/tutorial/references/track-3-projects-and-roadmaps.md +2 -1
@@ -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
 
@@ -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.
@@ -44,12 +44,14 @@ deliberately); and the durable lock file is checked again after disarm, never me
44
44
  trusted (exit 3 if it is somehow still present).
45
45
 
46
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).
47
+ `Bridge.disarm_auto` by default, which calls `Worktree.release`, which removes the
48
+ intent's code worktree under `<repo>/.claude/worktrees/{id}--{slug}`, prunes the repo, and
49
+ clears the worktree block from the bridge. (Plastic used to also provision a paired store
50
+ worktree under `<plastic_home>/.worktrees/{id}--{slug}`; intent 178 retired it, since
51
+ lifecycle-doc writes go straight to the main store checkout, and intent 197's
52
+ branch-from-main plus scoped commit already gives them their own write safety.) This is the
53
+ plain remove path: the disarm route does NOT merge, so use it only when no release merges
54
+ the branch (the branch survives and can be reclaimed).
53
55
 
54
56
  When the work is being shipped through a release, do NOT rely on this plain remove.
55
57
  `skills/releasing/SKILL.md` reorders its own two steps for exactly this reason (intent 188,
@@ -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)
@@ -106,6 +106,11 @@ If any checks have `fixable: true` AND status is not `pass`:
106
106
 
107
107
  If no fixable issues exist, skip this step.
108
108
 
109
+ This "Fix all / Select individually / Skip" prompt IS the router the spec calls
110
+ `doctor --fix-all` (intent 197): doctor itself never mutates anything (see Step 5's table and
111
+ "Important Notes" below); "Fix all" means "dispatch every fixable finding to the maintenance
112
+ tool or skill that owns that class of repair," one row per fix_hint pattern.
113
+
109
114
  ### Step 5: Apply fixes
110
115
 
111
116
  Use the `fix_hint` value to determine the correct action:
@@ -121,6 +126,7 @@ Use the `fix_hint` value to determine the correct action:
121
126
  | "Run: provision-project-store {slug}" | Run `provision-project-store <slug>` (or invoke the `plastic-store-provisioning` skill) to create the missing store |
122
127
  | "Re-run installer" | Run `npx -y @zalom/plastic@<channel> install --agent <agent>` (channel: -alpha->@alpha, -beta->@beta, else @latest) |
123
128
  | "Dispatch plastic-store-curating ... revisions.md ..." | Invoke the `plastic-store-curating` (or the agent) to relocate the flagged section or ref into the intent's `revisions.md` via move-and-record (one dated, `[rule: <tag>]`-tagged entry per item), per PLASTIC.md > Structural maintenance and revisions.md. For a missing required section, restore or reproject it instead. |
129
+ | "Run scripts/project-links ... PRESERVES ... --drop-unbacked-links" | Run `ruby ~/.plastic/scripts/maintenance-run --tool project-links --intent <id> --apply` for the one flagged id (never run bare `project-links` against a real store outside the rare owner-approved batch exception, D2) |
124
130
 
125
131
  For fixes the agent cannot handle automatically, explain what the user needs
126
132
  to do manually. The `revisions.md` remedy is curator-applied (a move-and-record
@@ -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.
@@ -71,11 +71,11 @@
71
71
  },
72
72
  {
73
73
  "id": 8, "scope": "behavior", "set": "validation",
74
- "prompt": "Does the skill carry the intent-149 coordination note?",
75
- "expected_output": "A 'Coordination' section records that intent 149 has landed (dashboard demoted to prose, no Value x Effort grid) and intent 149a has landed (the four intent lists render as Markdown tables), and confirms the rule-name citations and the dashboard.rb project <slug> --data -> dashboard-project.md path still resolve.",
74
+ "prompt": "Does the skill carry the intent-202 coordination note (project board short by default)?",
75
+ "expected_output": "A 'Coordination' section records that intent 202 has landed: the Recently-worked and raw Future tables are gone, replaced by a prose summary (built in dashboard.rb) and an honest-totals footer; Active is capped at 3 (lifecycle-stage descending, D2) and Next-work at 5; conversational paging re-invokes dashboard.rb --data with --limit-active/--limit-next/--all (no state on disk); --plain prints the full uncapped plain-text board. The rule-name citations and the dashboard.rb project <slug> --data -> dashboard-project.md path still resolve.",
76
76
  "files": ["skills/project-continuing/SKILL.md"],
77
77
  "assertions": [
78
- { "type": "convention", "check": "'149' present with the landed coordination note", "observed": "present: 'Intent 149 has landed' and 'Intent 149a has landed' (lists render as Markdown tables) with confirmed rule-name citations and dashboard.rb path", "result": "pass" }
78
+ { "type": "convention", "check": "'202' present with the landed coordination note describing the new shape, caps, paging flags, and --plain", "observed": "present: 'Intent 202 has landed' paragraph with the described content", "result": "pass" }
79
79
  ]
80
80
  },
81
81
  {
@@ -7,20 +7,22 @@ would otherwise bloat the SKILL.md body.
7
7
  ## Fill rules (owned by plastic-dashboard, summarized here for convenience)
8
8
 
9
9
  - `{{a.b.count}}` -> the integer (e.g. `counts.active` is that count).
10
- - `{{<list>.rows}}` -> the four intent lists (`recently_worked`, `next_work`, `active`, `future`)
11
- render as Markdown table rows. The template hard-codes each table's header and separator; the
12
- placeholder becomes one data row per entry, in that table's fixed column order, cells dropped
13
- verbatim from the payload (cells arrive pipe-escaped and whitespace-normalized; do not re-escape
14
- or re-truncate). Column order per table:
15
- - `recently_worked` -> `| id | what | state | scope |` (global), `| id | what | state |` (project)
10
+ - `{{<list>.rows}}` -> the two intent lists (`active`, `next_work`) render as Markdown table
11
+ rows. The template hard-codes each table's header and separator; the placeholder becomes one
12
+ data row per entry, in that table's fixed column order, cells dropped verbatim from the
13
+ payload (cells arrive pipe-escaped and whitespace-normalized; do not re-escape or
14
+ re-truncate). Column order per table:
16
15
  - `next_work` -> `| id | what | value | disposition | flags_label |`
17
- - `active` -> `| id | what | stage |`; `future` -> `| id | what |`
18
- Overflow entry (empty `id`, `what` = `+N more`) -> `+N more` in the Id column, other cells blank.
16
+ - `active` -> `| id | what | stage |`
19
17
  Empty list -> one full-width row with `_(none)_` in the Id column, other cells blank, matching
20
- that table's column count (e.g. `| _(none)_ | | | | |` for the 5-column next_work table,
21
- `| _(none)_ | |` for the 2-column future table). Never emit `<br>`.
18
+ that table's column count. Neither list carries an overflow "+N more" row (intent 202): the
19
+ true pool size rides on the payload (`active_total`/`next_total`, shown as
20
+ `active_shown`/`next_shown`), stated in prose by `{{footer}}` instead. Never emit `<br>`.
22
21
  - `{{projects.lines}}` (global board) -> the project rollup stays prose, one line per project.
23
- - Scalars (`{{date}}`, `{{slug}}`, `{{description}}`) -> substitute verbatim.
22
+ - Scalars (`{{date}}`, `{{slug}}`, `{{summary}}`, `{{footer}}`) -> substitute verbatim.
23
+ `summary` (the 2-3 sentence "what was delivered most recently") and `footer` (the
24
+ honest-totals + how-to-see-everything line) are finished prose built in `dashboard.rb`,
25
+ replacing the old recently-worked table and the raw future table respectively.
24
26
 
25
27
  No re-sorting, no re-summarizing, no hand-written prose replacing a line the payload already
26
28
  supplies. Same store state produces a byte-identical payload regardless of model.
@@ -247,11 +247,11 @@ lock correctly (G5): before intent 188 this path left the lock stranded, exactly
247
247
  of bug closed by the End-tail enforcement work.
248
248
 
249
249
  This is the release branch of `plastic-intent-ending`'s Step 5 disarm (`merge: true`), not a
250
- separate concern: a release is the merge-then-remove path for the intent's worktrees (intent
250
+ separate concern: a release is the merge-then-remove path for the intent's worktree (intent
251
251
  73c3), so the intent's code branch is merged back into the default branch BEFORE the worktree
252
252
  is removed. Drive it through `Worktree.finish` with `merge: true`, which merges the code
253
- branch, then removes both worktrees (code + paired store), prunes both repos, and clears the
254
- worktree block from the bridge:
253
+ branch, then removes the worktree, prunes the repo, and clears the worktree block from the
254
+ bridge:
255
255
 
256
256
  ```bash
257
257
  ruby -r ~/.plastic/scripts/lib/worktree -r ~/.plastic/scripts/lib/bridge -e \
@@ -45,3 +45,12 @@ When an intent reaches a terminal state, moved to Completed OR Abandoned, do the
45
45
  2. Call `plastic-intent-ending` for the terminal-transition close (INDEX move, savepoint `Done` bookend, store commit, disarm, and the QMD reindex last): `ruby ~/.plastic/scripts/end-intent --store <store> --id <id> --disposition delivered|abandoned`, then follow that skill's own disarm and reindex steps. Never restate those one-liners here.
46
46
 
47
47
  After the agent completes, report what changed.
48
+
49
+ ## Maintenance dispatch (intent 197)
50
+
51
+ When invoked to fix a structural finding on an intent OTHER than one currently being delivered
52
+ (for example, from `/plastic-doctor`'s fix-all routing), the `plastic-intent-curator` agent
53
+ follows its own step 7: it detects (never acquires) the target's delivery lock, requires a
54
+ clean working tree, and performs the fix on a fresh branch merged back to main as one closed
55
+ operation, with an append-only `revisions.md` receipt in the same pass as the edit. See
56
+ `agents/plastic-intent-curator.md` for the exact mechanics.
@@ -17,6 +17,22 @@
17
17
  "result": "pass"
18
18
  }
19
19
  ]
20
+ },
21
+ {
22
+ "id": 2,
23
+ "scope": "behavior",
24
+ "set": "validation",
25
+ "prompt": "The user says: intent 26 (Completed) has a stale ## Links comment that contradicts its own real chain edge; fix it. Intent 26 is NOT the intent currently being delivered by this session.",
26
+ "expected_output": "Before editing intent 26, checks its delivery lock freshness (plastic-lock status --intent-dir, reading lock_fresh) and defers if fresh; requires a clean store working tree; creates a fresh maintenance branch off main; makes the scoped edit AND appends a revisions.md receipt in the same pass, refusing the edit if the receipt cannot be written; stages only the changed paths (never git add -A); merges the branch back to main and deletes it before reporting done.",
27
+ "files": [],
28
+ "assertions": [
29
+ {
30
+ "type": "human",
31
+ "check": "agent file states detect-only lock check, clean-tree precheck, branch-and-merge-back, and refuse-without-receipt as an unconditional sequence for maintenance on a non-current intent",
32
+ "observed": "agents/plastic-intent-curator.md step 7 covers all five sub-steps (a-f) exactly as asked",
33
+ "result": "pass"
34
+ }
35
+ ]
20
36
  }
21
37
  ]
22
38
  }
@@ -59,7 +59,7 @@ remember where they left off.
59
59
 
60
60
  ## Before any track
61
61
 
62
- Every track opens with the same two checks: run `/plastic-update` first, so the walkthrough
63
- matches what is actually installed, and work in a sandbox (a throwaway repo, or a global-store
64
- intent) so nothing real is touched by mistake. Each reference restates this briefly; do not
65
- skip it even if the user seems experienced.
62
+ Every track opens with the same two checks: run `/plastic-update` first (`$plastic-update` on
63
+ Codex), so the walkthrough matches what is actually installed, and work in a sandbox (a
64
+ throwaway repo, or a global-store intent) so nothing real is touched by mistake. Each reference
65
+ restates this briefly; do not skip it even if the user seems experienced.
@@ -9,7 +9,8 @@ end to end: a piece of work moved through What, Why, How, and Exec, with a finis
9
9
 
10
10
  ## Before you start
11
11
 
12
- Run `/plastic-update` first, so the commands below match what is actually installed.
12
+ Run `/plastic-update` first (`$plastic-update` on Codex), so the commands below match what is
13
+ actually installed.
13
14
 
14
15
  Work in a sandbox: this track always creates a global-store intent; the throwaway repo below
15
16
  is never registered as a Plastic project. Pick a throwaway git repository if you have one
@@ -9,7 +9,8 @@ agent end to end, and pausing and resuming that delivery will feel familiar.
9
9
 
10
10
  ## Before you start
11
11
 
12
- Run `/plastic-update` first, so the commands below match what is actually installed.
12
+ Run `/plastic-update` first (`$plastic-update` on Codex), so the commands below match what is
13
+ actually installed.
13
14
 
14
15
  Work in a sandbox: a throwaway git repository, or a global-store intent. Nothing in this
15
16
  track touches a real project.
@@ -9,7 +9,8 @@ will exist with more than one intent inside it and a roadmap file describing the
9
9
 
10
10
  ## Before you start
11
11
 
12
- Run `/plastic-update` first, so the commands below match what is actually installed.
12
+ Run `/plastic-update` first (`$plastic-update` on Codex), so the commands below match what is
13
+ actually installed.
13
14
 
14
15
  Work in a sandbox: this whole track is a walked example. It creates a real project directory
15
16
  and a real roadmap file on disk, but the project is a throwaway one made for learning, not