@zalom/plastic 1.14.1 → 2.0.0-alpha.10

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 (213) hide show
  1. package/PLASTIC.md +101 -294
  2. package/README.md +5 -6
  3. package/agents/plastic-advisor.md +18 -17
  4. package/agents/plastic-enforcer.md +57 -69
  5. package/agents/plastic-executor.md +34 -20
  6. package/agents/plastic-faux-advisor.md +18 -18
  7. package/bin/lib/context_budget.rb +453 -0
  8. package/bin/plastic-bench +78 -0
  9. package/config_asks.yml +38 -0
  10. package/hooks/capture +4 -0
  11. package/hooks/close +5 -0
  12. package/hooks/hooks.json +14 -42
  13. package/hooks/message-display +81 -0
  14. package/hooks/record +4 -0
  15. package/hooks/savepoint +5 -5
  16. package/package.json +3 -2
  17. package/scripts/agent-report +15 -10
  18. package/scripts/append-ledger +208 -0
  19. package/scripts/codex-hook +78 -90
  20. package/scripts/dashboard.rb +43 -14
  21. package/scripts/day-summary +53 -0
  22. package/scripts/doctor.rb +269 -43
  23. package/scripts/end-intent +170 -76
  24. package/scripts/exec-worktree +11 -20
  25. package/scripts/file-session-intent +61 -0
  26. package/scripts/hook-capture +255 -0
  27. package/scripts/hook-close +36 -0
  28. package/scripts/hook-message-display +74 -0
  29. package/scripts/hook-record +157 -0
  30. package/scripts/hook-savepoint +45 -0
  31. package/scripts/hook-session-start +128 -12
  32. package/scripts/install.rb +8 -6
  33. package/scripts/intent-screen +77 -0
  34. package/scripts/lib/agent_models.rb +1 -7
  35. package/scripts/lib/arm.rb +308 -0
  36. package/scripts/lib/backfill_intent.rb +316 -0
  37. package/scripts/lib/bridge.rb +24 -1597
  38. package/scripts/lib/compact_instructions.rb +56 -0
  39. package/scripts/lib/day_summary.rb +211 -0
  40. package/scripts/lib/doctor_core.rb +66 -88
  41. package/scripts/lib/doctor_session_ledger.rb +158 -0
  42. package/scripts/lib/exec_worktree.rb +14 -60
  43. package/scripts/lib/handoff.rb +184 -0
  44. package/scripts/lib/harness_text.rb +5 -4
  45. package/scripts/lib/hook_registry.rb +57 -106
  46. package/scripts/lib/insights.rb +3 -4
  47. package/scripts/lib/installer_core.rb +136 -41
  48. package/scripts/lib/intent_screen.rb +309 -0
  49. package/scripts/lib/intent_screen_ansi.rb +262 -0
  50. package/scripts/lib/lock.rb +6 -26
  51. package/scripts/lib/message_display.rb +290 -0
  52. package/scripts/lib/outcome_guard.rb +7 -5
  53. package/scripts/lib/project_validator.rb +30 -1
  54. package/scripts/lib/report_screen.rb +648 -0
  55. package/scripts/lib/roadmap_queue.rb +3 -3
  56. package/scripts/lib/roadmap_savepoint.rb +1 -1
  57. package/scripts/lib/rule_catalog.rb +6 -3
  58. package/scripts/lib/savepoint.rb +334 -0
  59. package/scripts/lib/scaffold_intent.rb +13 -179
  60. package/scripts/lib/screen_paint.rb +276 -0
  61. package/scripts/lib/session_backfill.rb +184 -0
  62. package/scripts/lib/session_close.rb +104 -0
  63. package/scripts/lib/session_git.rb +500 -0
  64. package/scripts/lib/session_ledger.rb +621 -0
  65. package/scripts/lib/store_discovery.rb +1 -1
  66. package/scripts/lib/store_provisioning.rb +1 -1
  67. package/scripts/maintenance-run +7 -7
  68. package/scripts/new-intent +63 -4
  69. package/scripts/plastic-lock +101 -41
  70. package/scripts/promote-session-item +150 -0
  71. package/scripts/read-config +3 -0
  72. package/scripts/rebuild-graph +1 -1
  73. package/scripts/report-screen +120 -0
  74. package/scripts/roadmap-next +2 -2
  75. package/scripts/rollback.rb +48 -3
  76. package/scripts/savepoint-note +67 -0
  77. package/scripts/scaffold-intent +37 -63
  78. package/scripts/session-commit +116 -0
  79. package/scripts/spawn-preamble +14 -7
  80. package/scripts/update.rb +7 -3
  81. package/scripts/write-handoff +60 -0
  82. package/skills/agent-advisor/SKILL.md +3 -3
  83. package/skills/agent-advisor/references/advisor-protocol.md +20 -20
  84. package/skills/auto/SKILL.md +223 -317
  85. package/skills/auto/evals/evals.json +10 -10
  86. package/skills/auto/references/agent-architecture.md +102 -90
  87. package/skills/auto/references/agent-report-contract.md +4 -21
  88. package/skills/auto/references/end-tail.md +47 -53
  89. package/skills/auto/references/human-report-contract.md +62 -56
  90. package/skills/conventions/SKILL.md +1 -3
  91. package/skills/conventions/references/lifecycle-and-savepoints.md +35 -1
  92. package/skills/conventions/references/locks-and-worktrees.md +62 -58
  93. package/skills/conventions/references/maintenance-and-revisions.md +12 -12
  94. package/skills/dashboard/SKILL.md +1 -1
  95. package/skills/direct/SKILL.md +66 -0
  96. package/skills/direct/references/request-signals.md +59 -0
  97. package/skills/doctor/SKILL.md +58 -11
  98. package/skills/doctor/report.md +3 -1
  99. package/skills/install/SKILL.md +11 -9
  100. package/skills/intent-continuing/SKILL.md +142 -91
  101. package/skills/intent-continuing/references/boarding-matrix.md +34 -0
  102. package/skills/intent-continuing/references/context-management.md +3 -3
  103. package/skills/intent-creating/SKILL.md +2 -2
  104. package/skills/intent-creating/references/lifecycle.md +1 -1
  105. package/skills/intent-ending/SKILL.md +34 -37
  106. package/skills/intent-ending/evals/evals.json +4 -4
  107. package/skills/intent-executing/SKILL.md +15 -46
  108. package/skills/intent-executing/plan-reviewer-prompt.md +39 -0
  109. package/skills/intent-speccing/SKILL.md +108 -51
  110. package/skills/intent-speccing/evals/evals.json +5 -5
  111. package/skills/intent-speccing/references/per-section-fill-rules.md +4 -4
  112. package/skills/intent-speccing/references/self-verify-checklist.md +1 -2
  113. package/skills/project-creating/SKILL.md +1 -1
  114. package/skills/releasing/SKILL.md +7 -8
  115. package/skills/tutorial/SKILL.md +2 -2
  116. package/skills/tutorial/references/track-1-guided.md +16 -18
  117. package/skills/tutorial/references/track-2-auto.md +12 -13
  118. package/skills/tutorial/references/track-3-projects-and-roadmaps.md +2 -2
  119. package/templates/agents.md +4 -7
  120. package/templates/config.yml +5 -0
  121. package/templates/intent-screen.md +17 -0
  122. package/templates/outcome.md +14 -1
  123. package/templates/project.yml +7 -0
  124. package/templates/report-state.md +11 -0
  125. package/templates/roadmap.md +1 -1
  126. package/templates/savepoint.md +2 -2
  127. package/templates/session-intent.md +29 -0
  128. package/agents/plastic-brainstorming.md +0 -48
  129. package/agents/plastic-future-intent-researcher.md +0 -35
  130. package/agents/plastic-intent-curator.md +0 -47
  131. package/agents/plastic-intent-discovery.md +0 -43
  132. package/agents/plastic-planner.md +0 -56
  133. package/agents/plastic-spec-specialist.md +0 -53
  134. package/hooks/auto-arm +0 -5
  135. package/hooks/bash-gate +0 -3
  136. package/hooks/continue +0 -31
  137. package/hooks/edit-gates +0 -3
  138. package/hooks/future-intent-check +0 -25
  139. package/hooks/gate-check +0 -12
  140. package/hooks/power-tools +0 -8
  141. package/scripts/hook-auto-arm +0 -51
  142. package/scripts/hook-bash-gate +0 -78
  143. package/scripts/hook-code-gate +0 -38
  144. package/scripts/hook-continue +0 -48
  145. package/scripts/hook-create-gate +0 -51
  146. package/scripts/hook-edit-gates +0 -58
  147. package/scripts/hook-future-intent-check +0 -90
  148. package/scripts/hook-gate-check +0 -169
  149. package/scripts/hook-links-gate +0 -54
  150. package/scripts/hook-lock-gate +0 -47
  151. package/scripts/hook-power-tools +0 -38
  152. package/scripts/hook-savepoint-pre +0 -32
  153. package/scripts/lib/codex_edit_gates.rb +0 -138
  154. package/scripts/lib/edit_gates.rb +0 -398
  155. package/scripts/lib/links_gate.rb +0 -140
  156. package/scripts/lib/qmd_hook.rb +0 -41
  157. package/scripts/lib/spec_header.rb +0 -83
  158. package/scripts/lib/start_intent.rb +0 -296
  159. package/scripts/start-intent +0 -89
  160. package/skills/_active-intent-gate.md +0 -26
  161. package/skills/auto/references/tiers.md +0 -100
  162. package/skills/continuing/SKILL.md +0 -34
  163. package/skills/continuing/evals/evals.json +0 -91
  164. package/skills/conventions/references/gates-and-enforcement.md +0 -53
  165. package/skills/conventions/references/tiers-and-dispatch.md +0 -135
  166. package/skills/doctor/references/gates-stuck-detection.md +0 -43
  167. package/skills/intent-brainstorming/SKILL.md +0 -118
  168. package/skills/intent-brainstorming/evals/evals.json +0 -67
  169. package/skills/intent-continuing/evals/evals.json +0 -145
  170. package/skills/intent-discovering/SKILL.md +0 -56
  171. package/skills/intent-grilling/SKILL.md +0 -108
  172. package/skills/intent-linking/SKILL.md +0 -128
  173. package/skills/intent-linking/evals/evals.json +0 -22
  174. package/skills/intent-linking/references/zettelkasten.md +0 -45
  175. package/skills/intent-locking/SKILL.md +0 -64
  176. package/skills/intent-planning/SKILL.md +0 -145
  177. package/skills/intent-planning/evals/evals.json +0 -81
  178. package/skills/intent-planning/references/plan-format.md +0 -130
  179. package/skills/intent-researching/SKILL.md +0 -123
  180. package/skills/intent-researching/evals/evals.json +0 -22
  181. package/skills/intent-savepoint/SKILL.md +0 -83
  182. package/skills/intent-savepoint/references/context-management.md +0 -32
  183. package/skills/intent-starting/SKILL.md +0 -151
  184. package/skills/intent-starting/evals/evals.json +0 -117
  185. package/skills/intent-starting/references/boarding-matrix.md +0 -35
  186. package/skills/project-continuing/SKILL.md +0 -119
  187. package/skills/project-continuing/evals/evals.json +0 -100
  188. package/skills/roadmap-continuing/SKILL.md +0 -89
  189. package/skills/roadmap-continuing/evals/evals.json +0 -82
  190. package/skills/skill-creating/SKILL.md +0 -75
  191. package/skills/skill-creating/evals/evals.json +0 -108
  192. package/skills/skill-creating/references/agents.md +0 -168
  193. package/skills/skill-creating/references/defaults-first.md +0 -23
  194. package/skills/skill-creating/references/evals.md +0 -41
  195. package/skills/skill-creating/references/hooks.md +0 -251
  196. package/skills/skill-creating/references/progressive-disclosure.md +0 -176
  197. package/skills/skill-creating/references/scripts.md +0 -166
  198. package/skills/skill-creating/references/skills.md +0 -169
  199. package/skills/skill-creating/scripts/scaffold.rb +0 -313
  200. package/skills/skill-evaluating/SKILL.md +0 -141
  201. package/skills/skill-evaluating/assets/eval-template.json +0 -12
  202. package/skills/skill-evaluating/evals/evals.json +0 -75
  203. package/skills/skill-evaluating/references/convention-checks.md +0 -76
  204. package/skills/skill-evaluating/references/eval-methodology.md +0 -154
  205. package/skills/store-curating/SKILL.md +0 -64
  206. package/skills/store-curating/evals/evals.json +0 -38
  207. package/skills/store-indexing/SKILL.md +0 -93
  208. package/skills/store-indexing/evals/evals.json +0 -22
  209. package/skills/store-indexing/references/zettelkasten-linking.md +0 -32
  210. package/skills/store-provisioning/SKILL.md +0 -55
  211. /package/skills/{project-continuing → intent-continuing}/references/board-fill.md +0 -0
  212. /package/skills/{roadmap-continuing → intent-continuing}/references/liveness-ranking.md +0 -0
  213. /package/skills/{intent-brainstorming → intent-speccing}/references/design-principles.md +0 -0
@@ -21,22 +21,37 @@
21
21
  # [--tags tag,tag] [--templates <dir>]
22
22
  #
23
23
  # Exit codes: 0 (born complete), 1 (scaffold failed self-validation or bad input).
24
+ #
25
+ # A second, unrelated mode (intent 297):
26
+ # new-intent --tmp [--store <dir>] [--day YYYYMMDD] [--author <name>] \
27
+ # [--templates <dir>]
28
+ #
29
+ # `--tmp` scaffolds the shared session intent day ledger under the global
30
+ # store's `.sessions/<YYYYMMDD>/` instead of allocating a real intent id. It
31
+ # is a thin CLI over SessionLedger.open_day (the single scaffold
32
+ # implementation, spec D12) and never touches the id-allocation path above:
33
+ # no folgezettel-id call, no actions/ or resources/ dirs, no sentinel
34
+ # placeholders, no born savepoint stamp, no Links projection. Stdout is
35
+ # exactly one line, `created <dir>` or `joined <dir>`; exit 0 either way,
36
+ # exit 1 on bad input or failed self-validation.
24
37
 
25
38
  require "fileutils"
26
39
  require "date"
27
- require_relative "lib/bridge"
40
+ require_relative "lib/savepoint"
28
41
  require_relative "lib/intent_validator"
29
42
  require_relative "lib/graph_rebuild"
30
43
  require_relative "lib/links_projection"
31
44
  require_relative "lib/links_section"
32
45
  require_relative "lib/store_discovery"
46
+ require_relative "lib/session_ledger"
33
47
 
34
48
  # --- Explicit flag parsing (no eval, no global injection) ------------------
35
49
 
36
50
  def parse_args(argv)
37
51
  opts = {
38
52
  store: nil, intent: nil, slug: nil, parent: nil,
39
- author: "claude-code", sources: [], tags: [], templates: nil
53
+ author: "claude-code", sources: [], tags: [], templates: nil,
54
+ tmp: false, day: nil
40
55
  }
41
56
  i = 0
42
57
  while i < argv.length
@@ -50,6 +65,8 @@ def parse_args(argv)
50
65
  when "--sources" then opts[:sources] = split_list(argv[i += 1])
51
66
  when "--tags" then opts[:tags] = split_list(argv[i += 1])
52
67
  when "--templates" then opts[:templates] = argv[i += 1]
68
+ when "--tmp" then opts[:tmp] = true
69
+ when "--day" then opts[:day] = argv[i += 1]
53
70
  else
54
71
  abort "new-intent: unknown argument #{arg.inspect}"
55
72
  end
@@ -253,8 +270,50 @@ def project_links_for(file_path, referer_store_key, maps)
253
270
  true
254
271
  end
255
272
 
273
+ # The --tmp branch (intent 297, spec D1): scaffold the shared session intent
274
+ # day ledger instead of a real intent. A thin CLI over
275
+ # SessionLedger.open_day, the single scaffold implementation (spec D12); this
276
+ # method never allocates a folgezettel id, never creates actions/ or
277
+ # resources/, never writes a sentinel placeholder, never stamps a savepoint
278
+ # line, and never runs the Links projection build.
279
+ def run_tmp(opts)
280
+ { intent: "--intent", slug: "--slug", parent: "--parent" }.each do |key, flag|
281
+ abort "new-intent: #{flag} is not valid with --tmp" if opts[key] && !opts[key].to_s.empty?
282
+ end
283
+ abort "new-intent: --sources is not valid with --tmp" unless opts[:sources].empty?
284
+ abort "new-intent: --tags is not valid with --tmp" unless opts[:tags].empty?
285
+
286
+ store = opts[:store] ? expand(opts[:store]) : expand(File.join(ENV.fetch("PLASTIC_HOME", "~/.plastic"), "store"))
287
+ templates = opts[:templates] ? expand(opts[:templates]) : default_templates_dir
288
+ abort "new-intent: templates dir not found: #{templates}" unless Dir.exist?(templates)
289
+
290
+ day = opts[:day] || SessionLedger.day_id
291
+ unless SessionLedger.valid_day_id?(day)
292
+ abort "new-intent: --day must be eight digits parsing as a real date, got #{opts[:day].inspect}"
293
+ end
294
+
295
+ result = SessionLedger.open_day(store: store, day: day, templates: templates, author: opts[:author], now: Time.now)
296
+
297
+ plastic_home, = store_context(store)
298
+ known_stores = StoreDiscovery.known_slugs(plastic_home)
299
+ validation = IntentValidator.validate(result[:dir], known_stores: known_stores)
300
+ unless validation[:ok]
301
+ warn "new-intent: scaffolded day ledger is NOT born complete:"
302
+ validation[:missing].each { |f| warn " missing field: #{f}" }
303
+ validation[:errors].each { |e| warn " #{e}" }
304
+ exit 1
305
+ end
306
+
307
+ puts "#{result[:created] ? "created" : "joined"} #{result[:dir]}"
308
+ exit 0
309
+ end
310
+
256
311
  def main(argv)
257
312
  opts = parse_args(argv)
313
+ return run_tmp(opts) if opts[:tmp]
314
+
315
+ abort "new-intent: --day is only valid with --tmp" if opts[:day]
316
+
258
317
  abort "new-intent: --store is required" if opts[:store].nil? || opts[:store].empty?
259
318
  abort "new-intent: --intent is required" if opts[:intent].nil? || opts[:intent].empty?
260
319
  abort "new-intent: --slug is required" if opts[:slug].nil? || opts[:slug].empty?
@@ -338,7 +397,7 @@ def main(argv)
338
397
  template_path = File.join(templates, name)
339
398
  body = File.exist?(template_path) ? File.read(template_path) : ""
340
399
  body = render_tokens(body, { "INTENT_NAME" => opts[:intent] })
341
- File.write(File.join(intent_dir, name), "#{Bridge::PLACEHOLDER_SENTINEL}\n#{body}")
400
+ File.write(File.join(intent_dir, name), "#{Savepoint::PLACEHOLDER_SENTINEL}\n#{body}")
342
401
  end
343
402
 
344
403
  # 6. Stamp the born savepoint line (intent 81). The first ledger line is the
@@ -347,7 +406,7 @@ def main(argv)
347
406
  # sessions / harnesses). The intent file is never a sentinel placeholder, so
348
407
  # append_savepoint records `What {id}--{slug}.md`. Idempotent: a later gate
349
408
  # fire adds nothing.
350
- Bridge.append_savepoint(intent_dir, intent_file)
409
+ Savepoint.append_savepoint(intent_dir, intent_file)
351
410
 
352
411
  # 7. Self-validate (frontmatter + sanctioned sections), including that every
353
412
  # sources/chain cross-store token names a real store (intent 189 D3).
@@ -2,19 +2,29 @@
2
2
  # encoding: UTF-8
3
3
  # frozen_string_literal: true
4
4
  #
5
- # plastic-lock: inspect and repair the durable delivery lock (intent 108, D5),
6
- # and take/free per-artifact claim tokens (intent 111 D1/D5).
5
+ # plastic-lock: take, inspect, repair, and give back the durable delivery lock
6
+ # (intent 108, D5; intent 307), and take/free per-artifact claim tokens
7
+ # (intent 111 D1/D5).
7
8
  #
8
- # Usage: plastic-lock <status|who|fix|release|reclaim|delegate|claim|release-claim>
9
+ # Usage: plastic-lock <arm|status|who|fix|release|reclaim|delegate|claim|release-claim>
9
10
  # [--intent-dir DIR] [--session SID] [--delegate SID] [--artifact NAME]
11
+ # [--harness claude|codex] [--agent NAME] [--model MODEL] [--thread ID]
12
+ # [--mode auto|guided] [--status finished|failed]
10
13
  #
11
14
  # Verbs:
15
+ # arm take the intent for this session (intent 307): acquire
16
+ # delivery.lock stamped with --mode (default auto) and the
17
+ # provenance flags, provision the code worktree, and point the
18
+ # session at the intent; exit 1 on a held, stale, excluded, or
19
+ # corrupt lock with the resolving doctor hint
12
20
  # who compact, read-only durable owner/activity view
13
- # status report the lock file, the bridge cache, their agreement,
14
- # and any live per-artifact claims
15
- # fix idempotent repair: rebuild lock + bridge from disk truth for
16
- # the current session; never touches a fresh foreign lock
17
- # release owner clears the lock (End tail / abandoning a boarding)
21
+ # status report the lock file, its freshness, the derived worktree,
22
+ # whether this session's pointer names the intent, and any
23
+ # live per-artifact claims
24
+ # fix idempotent repair of the lock for the current session; never
25
+ # touches a fresh foreign lock
26
+ # release owner clears the lock and points the session back at the day
27
+ # ledger (End tail / abandoning a boarding)
18
28
  # reclaim explicit takeover of a stale lock, audited in savepoint.md
19
29
  # delegate owner registers a subagent session under the lock (D4)
20
30
  # claim acquire the named --artifact's claim token (intent 111);
@@ -22,15 +32,23 @@
22
32
  # including this same session; takes over a stale claim
23
33
  # release-claim free the named --artifact's claim token
24
34
  #
25
- # Without --intent-dir the intent is resolved from this session's bridge.
26
- # Exit 0 on success/report; exit 1 when the verb is blocked (held elsewhere).
35
+ # Without --intent-dir the intent is resolved from this session's pointer
36
+ # (~/.plastic/store/.tmp/<session>/current) when it names an intent id, looked
37
+ # up in the working directory's project store and then the global store. The
38
+ # /tmp bridge JSON this CLI used to resolve through was removed in 2.0
39
+ # (intent 307). Exit 0 on success/report; exit 1 when the verb is blocked.
27
40
 
28
41
  require "json"
42
+ require "yaml"
29
43
  require_relative "lib/bridge"
30
44
  require_relative "lib/lock"
45
+ require_relative "lib/arm"
46
+ require_relative "lib/session_ledger"
47
+
48
+ VERBS = %w[arm status who fix release reclaim delegate claim release-claim].freeze
31
49
 
32
50
  def usage!
33
- warn "usage: plastic-lock <status|who|fix|release|reclaim|delegate|claim|release-claim> " \
51
+ warn "usage: plastic-lock <#{VERBS.join('|')}> " \
34
52
  "[--intent-dir DIR] [--session SID] [--delegate SID] [--artifact NAME] " \
35
53
  "[--harness claude|codex] [--agent NAME] [--model MODEL] [--thread ID] " \
36
54
  "[--mode auto|guided] [--status finished|failed]"
@@ -38,7 +56,7 @@ def usage!
38
56
  end
39
57
 
40
58
  verb = ARGV.shift
41
- usage! unless %w[status who fix release reclaim delegate claim release-claim].include?(verb)
59
+ usage! unless VERBS.include?(verb)
42
60
 
43
61
  opts = {}
44
62
  until ARGV.empty?
@@ -52,6 +70,7 @@ until ARGV.empty?
52
70
  when "--model" then opts[:model] = ARGV.shift
53
71
  when "--thread" then opts[:thread] = ARGV.shift
54
72
  when "--mode" then opts[:mode] = ARGV.shift
73
+ when "--allow-inline" then opts[:allow_inline] = true
55
74
  when "--status" then opts[:status] = ARGV.shift
56
75
  else
57
76
  warn "unknown flag #{flag}"
@@ -64,15 +83,29 @@ session = opts[:session]
64
83
  session = ENV["CLAUDE_CODE_SESSION_ID"] if session.nil? || session.strip.empty?
65
84
  harness = opts[:harness]
66
85
  hint_harness = harness || "claude"
86
+ # The plastic home every hook uses (PLASTIC_HOME, else ~/.plastic); Arm takes the
87
+ # OS home above it, the same convention Worktree resolves projects.yml from.
88
+ plastic_home_env = ENV["PLASTIC_HOME"].to_s.strip
89
+ home = plastic_home_env.empty? ? Dir.home : File.dirname(File.expand_path(plastic_home_env))
90
+
91
+ # The stores this session's pointer is resolved against: the project store of
92
+ # the working directory (projects.yml, longest path match), then the global one.
93
+ def candidate_stores(home, cwd)
94
+ plastic_home = File.join(home, ".plastic")
95
+ stores = []
96
+ slug = SessionLedger.project_slug(cwd, plastic_home: plastic_home)
97
+ stores << File.join(plastic_home, "projects", slug, "store") if slug && slug != "global"
98
+ stores << File.join(plastic_home, "store")
99
+ stores
100
+ end
67
101
 
68
102
  dir = opts[:dir]
69
103
  if dir.nil? || dir.strip.empty?
70
- if verb == "who"
71
- warn "plastic-lock: who is strictly durable-state only; pass --intent-dir <intent dir>"
104
+ if %w[who arm].include?(verb)
105
+ warn "plastic-lock: #{verb} needs --intent-dir <intent dir>"
72
106
  exit 1
73
107
  end
74
- bridge = Bridge.discover_bridge(session: session, cwd: Dir.pwd)
75
- dir = Bridge.bridge_intent_dir(bridge)
108
+ dir = Arm.intent_dir_from_pointer(session, home: home, stores: candidate_stores(home, Dir.pwd))
76
109
  end
77
110
  if dir.nil?
78
111
  warn "plastic-lock: no intent resolved; pass --intent-dir <intent dir>"
@@ -85,7 +118,7 @@ if verb == "who"
85
118
  basename = File.basename(dir)
86
119
  intent_id, slug = basename.split("--", 2)
87
120
  display = ->(value) { value.nil? || value.to_s.strip.empty? || value == "unknown" ? "Unknown" : value.to_s }
88
- harness = ->(value) {
121
+ harness_name = ->(value) {
89
122
  shown = display.call(value)
90
123
  shown == "Unknown" ? shown : shown.sub(/\A./) { |char| char.upcase }
91
124
  }
@@ -99,12 +132,12 @@ if verb == "who"
99
132
 
100
133
  puts "#{intent_id} · #{slug || basename}"
101
134
  puts "State: #{view['state']}"
102
- puts "Controller: #{display.call(owner['agent'])} via #{harness.call(owner['harness'])}"
135
+ puts "Controller: #{display.call(owner['agent'])} via #{harness_name.call(owner['harness'])}"
103
136
  puts "Session: #{display.call(view['owner_session'])}"
104
137
  puts "Heartbeat: #{view['heartbeat_at'] || 'none'}"
105
138
  puts "Claims: #{claim_text.empty? ? 'none' : claim_text.join(', ')}"
106
139
  if delegate
107
- puts "Delegate: #{display.call(delegate['agent'])} via #{harness.call(delegate['harness'])}, #{display.call(delegate['status'])}"
140
+ puts "Delegate: #{display.call(delegate['agent'])} via #{harness_name.call(delegate['harness'])}, #{display.call(delegate['status'])}"
108
141
  else
109
142
  puts "Delegate: none"
110
143
  end
@@ -113,31 +146,65 @@ end
113
146
 
114
147
  intent_id = Bridge.intent_id_from_dir(dir)
115
148
  store = File.dirname(dir)
116
- name = File.basename(dir)
117
- key = Bridge.resolve_session(session, intent_id: intent_id, store: store)
149
+ key = Arm.resolve_session(session, store: store, intent_id: intent_id)
150
+
151
+ def doctor_hint(verb_text, harness)
152
+ "run #{Lock.skill_ref('plastic-doctor', harness: harness)} #{verb_text}"
153
+ end
118
154
 
119
155
  case verb
156
+ when "arm"
157
+ result = Arm.arm(intent_dir: dir, session: key, mode: opts[:mode] || "auto", home: home,
158
+ harness: harness, agent: opts[:agent], model: opts[:model], thread: opts[:thread],
159
+ allow_inline: opts[:allow_inline] || false)
160
+ case result[:status]
161
+ when :acquired, :owned
162
+ wt = result[:worktree] || {}
163
+ puts JSON.pretty_generate(
164
+ "intent_dir" => dir, "session" => result[:session], "status" => result[:status].to_s,
165
+ "run_mode" => result[:lock] && result[:lock]["run_mode"],
166
+ "worktree" => wt, "pointer" => result[:pointer]
167
+ )
168
+ when :held
169
+ warn "plastic-lock: delivery lock for intent #{intent_id} is held by session " \
170
+ "#{result[:lock] && result[:lock]['owner_session']}; #{doctor_hint('check the lock status', hint_harness)}"
171
+ exit 1
172
+ when :stale
173
+ warn "plastic-lock: delivery lock for intent #{intent_id} is stale (owner " \
174
+ "#{result[:lock] && result[:lock]['owner_session']}); #{doctor_hint('reclaim the lock to take it over with an audit', hint_harness)}"
175
+ exit 1
176
+ when :excluded
177
+ warn "plastic-lock: a #{result[:lock] && result[:lock]['type']} lock is active on intent " \
178
+ "#{intent_id}; #{doctor_hint('check the lock status', hint_harness)}"
179
+ exit 1
180
+ when :corrupt
181
+ warn "plastic-lock: delivery.lock for intent #{intent_id} is unreadable; #{doctor_hint('fix the lock', hint_harness)}"
182
+ exit 1
183
+ when :inline_refused
184
+ warn "plastic-lock: refusing to arm intent #{intent_id} from a conversation session " \
185
+ "(owner rule 2026-08-31: no inline intent delivery). Dispatch the delivery team " \
186
+ "instead (plastic-auto), or pass --allow-inline with explicit owner approval."
187
+ exit 1
188
+ end
120
189
  when "status"
121
190
  lock = Lock.read(dir)
122
- bridge = Bridge.read(key, intent_id: intent_id)
191
+ pointer = Arm.read_pointer(key, home: home)
123
192
  report = {
124
193
  "intent_dir" => dir,
125
194
  "session" => key,
126
195
  "lock" => lock,
127
196
  "lock_fresh" => Lock.fresh?(dir),
128
197
  "lock_corrupt" => Lock.corrupt?(dir),
129
- "bridge_present" => !bridge.nil?,
130
- "agreement" => (lock && bridge) ?
131
- (lock["owner_session"] == bridge.dig("lock", "owner_session")) : nil,
198
+ "worktree" => Arm.worktree_block(intent_dir: dir, home: home),
199
+ "pointer" => pointer,
200
+ "pointer_names_intent" => pointer == intent_id,
132
201
  "claims" => Claim.claims_status(dir),
133
202
  }
134
203
  puts JSON.pretty_generate(report)
135
204
  when "fix"
136
- report = Bridge.repair_lock(key, intent_id: intent_id, intent_dir: dir,
137
- store: store, name: name, harness: harness,
138
- agent: opts[:agent], model: opts[:model], thread: opts[:thread],
139
- run_mode: opts[:mode],
140
- hint_harness: hint_harness)
205
+ report = Arm.repair(intent_dir: dir, session: key, home: home, harness: harness,
206
+ agent: opts[:agent], model: opts[:model], thread: opts[:thread],
207
+ run_mode: opts[:mode], hint_harness: hint_harness)
141
208
  puts JSON.pretty_generate(report)
142
209
  unless report["status"] == "repaired"
143
210
  warn "plastic-lock: #{report['status']} by #{report['owner']}" \
@@ -150,12 +217,7 @@ when "release"
150
217
  warn "plastic-lock: not the owner; run plastic-lock status"
151
218
  exit 1
152
219
  end
153
- data = Bridge.read(key, intent_id: intent_id)
154
- if data
155
- data["lock"] = { "owner_session" => nil, "acquired_at" => nil,
156
- "host" => nil, "type" => nil, "delegates" => [] }
157
- Bridge.write(key, data)
158
- end
220
+ Arm.reset_pointer(key, intent_id, home: home)
159
221
  puts "released (#{result})"
160
222
  when "reclaim"
161
223
  status, lock_data = Lock.takeover(dir, session: key, harness: harness,
@@ -166,11 +228,9 @@ when "reclaim"
166
228
  "back off (no silent reclaim)"
167
229
  exit 1
168
230
  end
169
- report = Bridge.repair_lock(key, intent_id: intent_id, intent_dir: dir,
170
- store: store, name: name, harness: harness,
171
- agent: opts[:agent], model: opts[:model], thread: opts[:thread],
172
- run_mode: opts[:mode],
173
- hint_harness: hint_harness)
231
+ report = Arm.repair(intent_dir: dir, session: key, home: home, harness: harness,
232
+ agent: opts[:agent], model: opts[:model], thread: opts[:thread],
233
+ run_mode: opts[:mode], hint_harness: hint_harness)
174
234
  puts JSON.pretty_generate(report)
175
235
  when "delegate"
176
236
  usage! if opts[:delegate].nil?
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+
5
+ # promote-session-item (intent 301): turn one session day-ledger line into a
6
+ # registered intent through scripts/new-intent (never hand-authored), record
7
+ # the day of origin in the new intent, and mark the line promoted.
8
+ #
9
+ # Usage:
10
+ # promote-session-item --day <YYYYMMDD> --match <substring> [--session <id>]
11
+ # [--store <dir>] [--project <slug>] [--templates <dir>]
12
+ # [--plastic-home <dir>] [--no-reindex]
13
+ #
14
+ # Target: the newest open line, else the newest pending line, whose summary
15
+ # contains --match, tagged with --session when given, any session otherwise.
16
+ # The intent lands in the project's store when --project names a registered
17
+ # project, else in --store ($PLASTIC_HOME/store by default). No graph edge to
18
+ # the day id is written (the day ledger is not a store node); the origin is
19
+ # the `session_day:` frontmatter key plus a line under ## Context.
20
+ #
21
+ # Exit codes: 0 promoted (prints the intent dir last), 1 new-intent failed,
22
+ # 2 usage error or nothing to promote.
23
+
24
+ require "open3"
25
+ require "yaml"
26
+ require_relative "lib/session_ledger"
27
+ require_relative "lib/session_backfill"
28
+ require_relative "lib/store_provisioning"
29
+
30
+ def usage_abort(message)
31
+ warn "promote-session-item: #{message}"
32
+ exit 2
33
+ end
34
+
35
+ def next_value!(argv, i, flag)
36
+ value = argv[i + 1]
37
+ usage_abort("#{flag} needs a value") if value.nil? || value.start_with?("--")
38
+ value
39
+ end
40
+
41
+ opts = { day: nil, match: nil, session: nil, store: nil, project: nil, templates: nil,
42
+ plastic_home: nil, reindex: true }
43
+ i = 0
44
+ while i < ARGV.length
45
+ case ARGV[i]
46
+ when "--day" then opts[:day] = next_value!(ARGV, i, "--day"); i += 1
47
+ when "--match" then opts[:match] = next_value!(ARGV, i, "--match"); i += 1
48
+ when "--session" then opts[:session] = next_value!(ARGV, i, "--session"); i += 1
49
+ when "--store" then opts[:store] = next_value!(ARGV, i, "--store"); i += 1
50
+ when "--project" then opts[:project] = next_value!(ARGV, i, "--project"); i += 1
51
+ when "--templates" then opts[:templates] = next_value!(ARGV, i, "--templates"); i += 1
52
+ when "--plastic-home" then opts[:plastic_home] = next_value!(ARGV, i, "--plastic-home"); i += 1
53
+ when "--no-reindex" then opts[:reindex] = false
54
+ else usage_abort("unknown argument #{ARGV[i].inspect}")
55
+ end
56
+ i += 1
57
+ end
58
+
59
+ usage_abort("--day is required") if opts[:day].nil?
60
+ usage_abort("--day must be a real calendar date, YYYYMMDD") unless SessionLedger.valid_day_id?(opts[:day])
61
+ usage_abort("--match is required") if opts[:match].nil? || opts[:match].empty?
62
+
63
+ plastic_home = File.expand_path(opts[:plastic_home] || ENV.fetch("PLASTIC_HOME", "~/.plastic"))
64
+ store = opts[:store] || File.join(plastic_home, "store")
65
+ templates = opts[:templates] || File.expand_path("../templates", __dir__)
66
+ session = opts[:session] ? SessionLedger.short_session_id(opts[:session]) : nil
67
+
68
+ target_store = store
69
+ if opts[:project]
70
+ projects = StoreProvisioning.load_projects(plastic_home)
71
+ usage_abort("--project #{opts[:project]} is not a registered project") unless projects.key?(opts[:project])
72
+ target_store = File.join(plastic_home, "projects", opts[:project], "store")
73
+ end
74
+
75
+ # --- pick the target line ---------------------------------------------------
76
+
77
+ day_dir = SessionLedger.day_dir(store, opts[:day])
78
+ checklist = SessionLedger.checklist_path(store, opts[:day])
79
+ items = SessionBackfill.read_items(day_dir)
80
+ candidates = items.select do |item|
81
+ (session.nil? || item[:session] == session) && item[:summary].include?(opts[:match])
82
+ end
83
+ target = candidates.reverse.find { |item| item[:state] == :open } ||
84
+ candidates.reverse.find { |item| item[:state] == :pending }
85
+ usage_abort("nothing to promote in #{opts[:day]} matching #{opts[:match].inspect}") if target.nil?
86
+
87
+ # --- scaffold through new-intent ---------------------------------------------
88
+
89
+ def slug_for(summary)
90
+ words = summary.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-+|-+\z/, "").split("-").reject(&:empty?)
91
+ slug = words.first(5).join("-")
92
+ slug.empty? ? "item" : slug
93
+ end
94
+
95
+ new_intent = File.expand_path("new-intent", __dir__)
96
+ out, err, status = Open3.capture3({ "RUBYOPT" => nil }, RbConfig.ruby, new_intent,
97
+ "--store", target_store, "--intent", target[:summary],
98
+ "--slug", slug_for(target[:summary]), "--templates", templates)
99
+ unless status.success?
100
+ warn "promote-session-item: new-intent failed:"
101
+ warn err
102
+ warn out
103
+ exit 1
104
+ end
105
+ intent_dir = out.lines.last.to_s.strip
106
+ usage_abort("new-intent printed no intent dir") if intent_dir.empty? || !Dir.exist?(intent_dir)
107
+ intent_id = File.basename(intent_dir).split("--", 2).first
108
+
109
+ # --- record the origin in the new intent (one atomic rewrite) -----------------
110
+
111
+ intent_file = File.join(intent_dir, "#{File.basename(intent_dir)}.md")
112
+ content = File.read(intent_file)
113
+ parts = content.split(/^(---\s*\n)/, 3)
114
+ if parts.length >= 4
115
+ parts[2] += "session_day: \"#{opts[:day]}\"\n"
116
+ content = parts[0] + parts[1] + parts[2] + parts[3] + parts[4].to_s
117
+ end
118
+ origin = "Promoted from the session ledger of #{opts[:day]}: #{target[:summary]}\n"
119
+ content = content.sub(/^## Context\n/) { |heading| heading + origin }
120
+ SessionBackfill.write_atomic(intent_dir, File.basename(intent_file), content)
121
+
122
+ # --- register the intent under ## Future in the store's INDEX.md ----------------
123
+
124
+ def register_in_index(index_path, intent_id, intent_dir, summary)
125
+ return unless File.exist?(index_path)
126
+
127
+ relative = "store/#{File.basename(intent_dir)}/#{File.basename(intent_dir)}.md"
128
+ entry = "- [#{intent_id} - #{summary}](#{relative}) - promoted from a session ledger\n"
129
+ content = File.read(index_path)
130
+ return if content.include?("(#{relative})")
131
+
132
+ updated = content.sub(/^## Future\n(?:\(no future intents\)\n)?/) { |heading| heading.sub(/\(no future intents\)\n/, "") + entry }
133
+ SessionBackfill.write_atomic(File.dirname(index_path), File.basename(index_path), updated)
134
+ end
135
+
136
+ register_in_index(File.join(File.dirname(target_store), "INDEX.md"), intent_id, intent_dir, target[:summary])
137
+
138
+ # --- mark the line, note it, reindex ---------------------------------------------
139
+
140
+ SessionLedger.set_state(checklist, from: target[:state], to: :promoted, session: target[:session],
141
+ match: target[:summary])
142
+ SessionLedger.append_line(SessionLedger.savepoint_path(store, opts[:day]),
143
+ SessionLedger.savepoint_line("Note", target[:session], target[:project],
144
+ "promoted \"#{target[:summary]}\" to intent #{intent_id}",
145
+ now: Time.now))
146
+ if opts[:reindex]
147
+ qmd_sync = File.expand_path("qmd-sync", __dir__)
148
+ system({ "RUBYOPT" => nil }, RbConfig.ruby, qmd_sync, "reindex", out: File::NULL, err: File::NULL) if File.exist?(qmd_sync)
149
+ end
150
+ puts intent_dir
@@ -14,6 +14,9 @@ require_relative "lib/agent_models"
14
14
  DEFAULTS = {
15
15
  "version" => 3,
16
16
  "stale_threshold_days" => 3,
17
+ # Absolute token counts for a 1M window, 35 and 50 percent (intent 312, 296 D38).
18
+ "context_offer_tokens" => 350_000,
19
+ "context_insist_tokens" => 500_000,
17
20
  "execution_mode" => "subagent-driven",
18
21
  "hash_length" => 6,
19
22
  "hash_algorithm" => "sha256-base36",
@@ -82,7 +82,7 @@ class RebuildGraph
82
82
  end
83
83
 
84
84
  # Registered projects.yml slugs with no store directory on disk (legal;
85
- # plastic-store-provisioning exists for exactly this state). Reported in the audit
85
+ # the plastic-doctor provisioning section exists for exactly this state). Reported in the audit
86
86
  # (intent 189 D5) so this never looks like a silent empty store.
87
87
  def missing_stores
88
88
  discovery[:missing]
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+
5
+ # report-screen - the three delivery-report screens (intent 317): mid-delivery
6
+ # state, post-delivery delivered, and delay. Each fills from the record via
7
+ # scripts/lib/report_screen.rb; no number here is written by eye.
8
+ #
9
+ # Usage:
10
+ # report-screen state <intent_dir> [--changed "<text>"] [--ansi]
11
+ # report-screen state --all <store_root> [--changed "<text>"] [--ansi]
12
+ # report-screen delivered <intent_dir> [--ansi]
13
+ # report-screen delay <intent_dir> [--ansi]
14
+ #
15
+ # --ansi delegates to ScreenPaint (intent 317a, D1), the parser/re-layouter
16
+ # in the shared TUI core. Selection is by capability, never by harness:
17
+ # NO_COLOR forces plain, a non-TTY stdout stays plain unless
18
+ # PLASTIC_FORCE_COLOR=1 (the test seam), and an unparseable screen falls
19
+ # open to the plain text untouched.
20
+ #
21
+ # Exit codes:
22
+ # 0 - the screen is on stdout
23
+ # 2 - usage error, or the path/store is not what the verb expects (one line
24
+ # on stderr, stdout empty)
25
+
26
+ require_relative "lib/report_screen"
27
+ require_relative "lib/intent_screen"
28
+ require_relative "lib/screen_paint"
29
+
30
+ def usage_abort(message)
31
+ warn "report-screen: #{message}"
32
+ exit 2
33
+ end
34
+
35
+ # The one place allowed to shell out (D2/D20): resolves the nearest git tag
36
+ # reachable from the repo this script lives in, so `render_delivered`'s
37
+ # version field and the `ship` evidence row are never invented. The pure
38
+ # module never reads git itself; this is injected as `tag_reader:`.
39
+ def git_tag_reader(repo_root)
40
+ lambda do |_intent_dir|
41
+ return nil unless File.exist?(File.join(repo_root, ".git"))
42
+ tag = `git -C #{repo_root} describe --tags --abbrev=0 2>/dev/null`.strip
43
+ tag.empty? ? nil : tag
44
+ end
45
+ end
46
+
47
+ args = ARGV.dup
48
+ verb = args.shift
49
+ changed = nil
50
+ ansi = false
51
+ template_path = nil
52
+ positional = []
53
+
54
+ while (arg = args.shift)
55
+ case arg
56
+ when "--changed"
57
+ usage_abort("--changed needs a value") if args.empty?
58
+ changed = args.shift
59
+ when "--ansi"
60
+ ansi = true
61
+ when "--all"
62
+ positional << "--all"
63
+ when "--template"
64
+ template_path = args.shift or usage_abort("--template needs a path")
65
+ else
66
+ usage_abort("unknown flag #{arg.inspect}") if arg.start_with?("--") && arg != "--all"
67
+ positional << arg
68
+ end
69
+ end
70
+
71
+ usage_abort("usage: report-screen state|delivered|delay <intent_dir> [--changed \"<text>\"] [--ansi]") unless verb
72
+
73
+ all_mode = positional.delete("--all") ? true : false
74
+ target = positional.first
75
+
76
+ ansi_enabled = ansi && ENV["NO_COLOR"].to_s.empty? &&
77
+ ($stdout.tty? || ENV["PLASTIC_FORCE_COLOR"] == "1")
78
+
79
+ def paint(text, ansi_enabled, _renderer_path = nil)
80
+ return text unless ansi_enabled
81
+ ScreenPaint.paint(text, color: true) || text
82
+ end
83
+
84
+ case verb
85
+ when "state"
86
+ if all_mode
87
+ usage_abort("usage: report-screen state --all <store_root>") unless target
88
+ store_root = File.expand_path(target)
89
+ usage_abort("#{store_root} is not a store (no INDEX.md)") unless File.exist?(File.join(store_root, "INDEX.md"))
90
+ out = ReportScreen.render_roster(store_root, changed: changed)
91
+ $stdout.write paint(out, ansi_enabled)
92
+ else
93
+ usage_abort("usage: report-screen state <intent_dir> [--changed \"<text>\"]") unless target
94
+ intent_dir = File.expand_path(target)
95
+ usage_abort("#{intent_dir} is not an intent directory") unless IntentScreen.intent_dir?(intent_dir)
96
+ store_root = File.expand_path("../..", intent_dir)
97
+ template_path ||= File.expand_path("../templates/report-state.md", __dir__)
98
+ usage_abort("template not found at #{template_path}") unless File.exist?(template_path)
99
+ out = ReportScreen.render_state(intent_dir: intent_dir, store_root: store_root, changed: changed,
100
+ template: File.read(template_path))
101
+ $stdout.write paint(out, ansi_enabled)
102
+ end
103
+ when "delivered"
104
+ usage_abort("usage: report-screen delivered <intent_dir>") unless target
105
+ intent_dir = File.expand_path(target)
106
+ usage_abort("#{intent_dir} is not an intent directory") unless IntentScreen.intent_dir?(intent_dir)
107
+ repo_root = File.expand_path("..", __dir__)
108
+ out = ReportScreen.render_delivered(intent_dir: intent_dir, tag_reader: git_tag_reader(repo_root))
109
+ $stdout.write paint(out, ansi_enabled)
110
+ when "delay"
111
+ usage_abort("usage: report-screen delay <intent_dir>") unless target
112
+ intent_dir = File.expand_path(target)
113
+ usage_abort("#{intent_dir} is not an intent directory") unless IntentScreen.intent_dir?(intent_dir)
114
+ out = ReportScreen.render_delay(intent_dir: intent_dir)
115
+ $stdout.write paint(out, ansi_enabled)
116
+ else
117
+ usage_abort("unknown verb #{verb.inspect} (use state|delivered|delay)")
118
+ end
119
+
120
+ exit 0
@@ -4,10 +4,10 @@
4
4
 
5
5
  # roadmap-next - thin CLI over RoadmapQueue (intent 148).
6
6
  #
7
- # The auto loop and plastic-roadmap-continuing both call this instead of ranking roadmap
7
+ # The auto loop and plastic-intent-continuing both call this instead of ranking roadmap
8
8
  # liveness in prose. Queue mode (default) is for the auto loop: it breaks a tie
9
9
  # deterministically and reports the winner's frontier state. Which mode (--which) is for
10
- # plastic-roadmap-continuing: it returns tie_candidates so the skill's single ask can resolve
10
+ # plastic-intent-continuing: it returns tie_candidates so the skill's roadmap route can resolve
11
11
  # a genuine tie.
12
12
  #
13
13
  # Usage: