@zalom/plastic 1.14.1 → 2.0.0-alpha.2

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 (200) 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 +9 -49
  13. package/hooks/record +4 -0
  14. package/hooks/savepoint +5 -5
  15. package/package.json +3 -2
  16. package/scripts/agent-report +8 -9
  17. package/scripts/append-ledger +208 -0
  18. package/scripts/codex-hook +78 -90
  19. package/scripts/dashboard.rb +4 -4
  20. package/scripts/day-summary +53 -0
  21. package/scripts/doctor.rb +106 -43
  22. package/scripts/end-intent +77 -76
  23. package/scripts/exec-worktree +11 -20
  24. package/scripts/file-session-intent +61 -0
  25. package/scripts/hook-capture +242 -0
  26. package/scripts/hook-close +36 -0
  27. package/scripts/hook-record +149 -0
  28. package/scripts/hook-savepoint +45 -0
  29. package/scripts/hook-session-start +106 -12
  30. package/scripts/install.rb +8 -6
  31. package/scripts/lib/agent_models.rb +1 -7
  32. package/scripts/lib/arm.rb +283 -0
  33. package/scripts/lib/backfill_intent.rb +316 -0
  34. package/scripts/lib/bridge.rb +24 -1597
  35. package/scripts/lib/compact_instructions.rb +56 -0
  36. package/scripts/lib/day_summary.rb +206 -0
  37. package/scripts/lib/doctor_core.rb +65 -88
  38. package/scripts/lib/doctor_session_ledger.rb +106 -0
  39. package/scripts/lib/exec_worktree.rb +14 -60
  40. package/scripts/lib/handoff.rb +184 -0
  41. package/scripts/lib/harness_text.rb +5 -4
  42. package/scripts/lib/hook_registry.rb +44 -107
  43. package/scripts/lib/insights.rb +3 -4
  44. package/scripts/lib/installer_core.rb +104 -40
  45. package/scripts/lib/lock.rb +6 -26
  46. package/scripts/lib/outcome_guard.rb +7 -5
  47. package/scripts/lib/project_validator.rb +30 -1
  48. package/scripts/lib/roadmap_queue.rb +3 -3
  49. package/scripts/lib/roadmap_savepoint.rb +1 -1
  50. package/scripts/lib/rule_catalog.rb +6 -3
  51. package/scripts/lib/savepoint.rb +320 -0
  52. package/scripts/lib/scaffold_intent.rb +13 -179
  53. package/scripts/lib/session_backfill.rb +184 -0
  54. package/scripts/lib/session_close.rb +104 -0
  55. package/scripts/lib/session_git.rb +469 -0
  56. package/scripts/lib/session_ledger.rb +497 -0
  57. package/scripts/lib/store_discovery.rb +1 -1
  58. package/scripts/lib/store_provisioning.rb +1 -1
  59. package/scripts/maintenance-run +7 -7
  60. package/scripts/new-intent +63 -4
  61. package/scripts/plastic-lock +94 -41
  62. package/scripts/promote-session-item +150 -0
  63. package/scripts/read-config +3 -0
  64. package/scripts/rebuild-graph +1 -1
  65. package/scripts/roadmap-next +2 -2
  66. package/scripts/rollback.rb +48 -3
  67. package/scripts/scaffold-intent +37 -63
  68. package/scripts/session-commit +116 -0
  69. package/scripts/spawn-preamble +5 -5
  70. package/scripts/update.rb +7 -3
  71. package/scripts/write-handoff +60 -0
  72. package/skills/agent-advisor/SKILL.md +3 -3
  73. package/skills/agent-advisor/references/advisor-protocol.md +20 -20
  74. package/skills/auto/SKILL.md +218 -317
  75. package/skills/auto/evals/evals.json +10 -10
  76. package/skills/auto/references/agent-architecture.md +102 -90
  77. package/skills/auto/references/agent-report-contract.md +4 -21
  78. package/skills/auto/references/end-tail.md +47 -53
  79. package/skills/auto/references/human-report-contract.md +8 -8
  80. package/skills/conventions/SKILL.md +1 -3
  81. package/skills/conventions/references/lifecycle-and-savepoints.md +35 -1
  82. package/skills/conventions/references/locks-and-worktrees.md +47 -55
  83. package/skills/conventions/references/maintenance-and-revisions.md +12 -12
  84. package/skills/dashboard/SKILL.md +1 -1
  85. package/skills/direct/SKILL.md +66 -0
  86. package/skills/direct/references/request-signals.md +59 -0
  87. package/skills/doctor/SKILL.md +58 -11
  88. package/skills/doctor/report.md +3 -1
  89. package/skills/install/SKILL.md +11 -9
  90. package/skills/intent-continuing/SKILL.md +132 -89
  91. package/skills/intent-continuing/references/boarding-matrix.md +34 -0
  92. package/skills/intent-continuing/references/context-management.md +2 -2
  93. package/skills/intent-creating/SKILL.md +2 -2
  94. package/skills/intent-creating/references/lifecycle.md +1 -1
  95. package/skills/intent-ending/SKILL.md +28 -37
  96. package/skills/intent-ending/evals/evals.json +4 -4
  97. package/skills/intent-executing/SKILL.md +12 -49
  98. package/skills/intent-executing/plan-reviewer-prompt.md +39 -0
  99. package/skills/intent-speccing/SKILL.md +108 -51
  100. package/skills/intent-speccing/evals/evals.json +5 -5
  101. package/skills/intent-speccing/references/per-section-fill-rules.md +4 -4
  102. package/skills/intent-speccing/references/self-verify-checklist.md +1 -2
  103. package/skills/project-creating/SKILL.md +1 -1
  104. package/skills/releasing/SKILL.md +7 -8
  105. package/skills/tutorial/SKILL.md +2 -2
  106. package/skills/tutorial/references/track-1-guided.md +16 -18
  107. package/skills/tutorial/references/track-2-auto.md +12 -13
  108. package/skills/tutorial/references/track-3-projects-and-roadmaps.md +2 -2
  109. package/templates/agents.md +4 -7
  110. package/templates/config.yml +5 -0
  111. package/templates/project.yml +7 -0
  112. package/templates/roadmap.md +1 -1
  113. package/templates/savepoint.md +2 -2
  114. package/templates/session-intent.md +29 -0
  115. package/agents/plastic-brainstorming.md +0 -48
  116. package/agents/plastic-future-intent-researcher.md +0 -35
  117. package/agents/plastic-intent-curator.md +0 -47
  118. package/agents/plastic-intent-discovery.md +0 -43
  119. package/agents/plastic-planner.md +0 -56
  120. package/agents/plastic-spec-specialist.md +0 -53
  121. package/hooks/auto-arm +0 -5
  122. package/hooks/bash-gate +0 -3
  123. package/hooks/continue +0 -31
  124. package/hooks/edit-gates +0 -3
  125. package/hooks/future-intent-check +0 -25
  126. package/hooks/gate-check +0 -12
  127. package/hooks/power-tools +0 -8
  128. package/scripts/hook-auto-arm +0 -51
  129. package/scripts/hook-bash-gate +0 -78
  130. package/scripts/hook-code-gate +0 -38
  131. package/scripts/hook-continue +0 -48
  132. package/scripts/hook-create-gate +0 -51
  133. package/scripts/hook-edit-gates +0 -58
  134. package/scripts/hook-future-intent-check +0 -90
  135. package/scripts/hook-gate-check +0 -169
  136. package/scripts/hook-links-gate +0 -54
  137. package/scripts/hook-lock-gate +0 -47
  138. package/scripts/hook-power-tools +0 -38
  139. package/scripts/hook-savepoint-pre +0 -32
  140. package/scripts/lib/codex_edit_gates.rb +0 -138
  141. package/scripts/lib/edit_gates.rb +0 -398
  142. package/scripts/lib/links_gate.rb +0 -140
  143. package/scripts/lib/qmd_hook.rb +0 -41
  144. package/scripts/lib/spec_header.rb +0 -83
  145. package/scripts/lib/start_intent.rb +0 -296
  146. package/scripts/start-intent +0 -89
  147. package/skills/_active-intent-gate.md +0 -26
  148. package/skills/auto/references/tiers.md +0 -100
  149. package/skills/continuing/SKILL.md +0 -34
  150. package/skills/continuing/evals/evals.json +0 -91
  151. package/skills/conventions/references/gates-and-enforcement.md +0 -53
  152. package/skills/conventions/references/tiers-and-dispatch.md +0 -135
  153. package/skills/doctor/references/gates-stuck-detection.md +0 -43
  154. package/skills/intent-brainstorming/SKILL.md +0 -118
  155. package/skills/intent-brainstorming/evals/evals.json +0 -67
  156. package/skills/intent-continuing/evals/evals.json +0 -145
  157. package/skills/intent-discovering/SKILL.md +0 -56
  158. package/skills/intent-grilling/SKILL.md +0 -108
  159. package/skills/intent-linking/SKILL.md +0 -128
  160. package/skills/intent-linking/evals/evals.json +0 -22
  161. package/skills/intent-linking/references/zettelkasten.md +0 -45
  162. package/skills/intent-locking/SKILL.md +0 -64
  163. package/skills/intent-planning/SKILL.md +0 -145
  164. package/skills/intent-planning/evals/evals.json +0 -81
  165. package/skills/intent-planning/references/plan-format.md +0 -130
  166. package/skills/intent-researching/SKILL.md +0 -123
  167. package/skills/intent-researching/evals/evals.json +0 -22
  168. package/skills/intent-savepoint/SKILL.md +0 -83
  169. package/skills/intent-savepoint/references/context-management.md +0 -32
  170. package/skills/intent-starting/SKILL.md +0 -151
  171. package/skills/intent-starting/evals/evals.json +0 -117
  172. package/skills/intent-starting/references/boarding-matrix.md +0 -35
  173. package/skills/project-continuing/SKILL.md +0 -119
  174. package/skills/project-continuing/evals/evals.json +0 -100
  175. package/skills/roadmap-continuing/SKILL.md +0 -89
  176. package/skills/roadmap-continuing/evals/evals.json +0 -82
  177. package/skills/skill-creating/SKILL.md +0 -75
  178. package/skills/skill-creating/evals/evals.json +0 -108
  179. package/skills/skill-creating/references/agents.md +0 -168
  180. package/skills/skill-creating/references/defaults-first.md +0 -23
  181. package/skills/skill-creating/references/evals.md +0 -41
  182. package/skills/skill-creating/references/hooks.md +0 -251
  183. package/skills/skill-creating/references/progressive-disclosure.md +0 -176
  184. package/skills/skill-creating/references/scripts.md +0 -166
  185. package/skills/skill-creating/references/skills.md +0 -169
  186. package/skills/skill-creating/scripts/scaffold.rb +0 -313
  187. package/skills/skill-evaluating/SKILL.md +0 -141
  188. package/skills/skill-evaluating/assets/eval-template.json +0 -12
  189. package/skills/skill-evaluating/evals/evals.json +0 -75
  190. package/skills/skill-evaluating/references/convention-checks.md +0 -76
  191. package/skills/skill-evaluating/references/eval-methodology.md +0 -154
  192. package/skills/store-curating/SKILL.md +0 -64
  193. package/skills/store-curating/evals/evals.json +0 -38
  194. package/skills/store-indexing/SKILL.md +0 -93
  195. package/skills/store-indexing/evals/evals.json +0 -22
  196. package/skills/store-indexing/references/zettelkasten-linking.md +0 -32
  197. package/skills/store-provisioning/SKILL.md +0 -55
  198. /package/skills/{project-continuing → intent-continuing}/references/board-fill.md +0 -0
  199. /package/skills/{roadmap-continuing → intent-continuing}/references/liveness-ranking.md +0 -0
  200. /package/skills/{intent-brainstorming → intent-speccing}/references/design-principles.md +0 -0
@@ -9,6 +9,7 @@ require "time"
9
9
  require_relative "hook_registry"
10
10
  require_relative "agent_models"
11
11
  require_relative "harness_text"
12
+ require_relative "compact_instructions"
12
13
 
13
14
  # Shared installer machinery, instantiable with injected package root / store / agent
14
15
  # map so the verb scripts (install/update/uninstall/rollback) and their tests can run
@@ -33,6 +34,15 @@ class InstallerCore
33
34
  # Regex matching exactly one managed section (BEGIN line .. END line), non-greedy.
34
35
  CODEX_SECTION_RE = /^<!-- BEGIN PLASTIC INTEGRATION.*?-->\n.*?\n<!-- END PLASTIC INTEGRATION -->\n?/m
35
36
 
37
+ # Claude CLAUDE.md marked-section markers (intent 312). A pair of its own, not the
38
+ # Codex literals: the two managed files can be one file (a user who symlinks
39
+ # ~/.claude/CLAUDE.md at ~/.codex/AGENTS.md, or the reverse), and a shared literal
40
+ # would let one body silently replace the other and an uninstall of one strip both.
41
+ # doctor_core.rb matches these literals structurally, so keep the two in sync by hand.
42
+ CLAUDE_SECTION_BEGIN_PREFIX = "<!-- BEGIN PLASTIC COMPACT"
43
+ CLAUDE_SECTION_END = "<!-- END PLASTIC COMPACT -->"
44
+ CLAUDE_SECTION_RE = /^<!-- BEGIN PLASTIC COMPACT.*?-->\n.*?\n<!-- END PLASTIC COMPACT -->\n?/m
45
+
36
46
  # Curated essentials plus a pointer to ~/.plastic/PLASTIC.md and the plastic-conventions
37
47
  # skill, injected into ~/.codex/AGENTS.md. Not a slice of PLASTIC.md itself: AGENTS.md is
38
48
  # a shared file Codex merges from multiple sources, so this block stays a small,
@@ -174,13 +184,6 @@ class InstallerCore
174
184
  keys.uniq
175
185
  end
176
186
 
177
- def channel_from(argv, default: "latest")
178
- return "alpha" if argv.include?("--alpha")
179
- return "beta" if argv.include?("--beta")
180
- return "latest" if argv.include?("--latest")
181
- default
182
- end
183
-
184
187
  def prompt_agents(input: $stdin)
185
188
  return ["claude"] unless input.tty?
186
189
 
@@ -329,25 +332,21 @@ class InstallerCore
329
332
  "scripts/write-config" => "scripts/write-config",
330
333
  "scripts/select-update-target" => "scripts/select-update-target",
331
334
  "scripts/hook-session-start" => "scripts/hook-session-start",
332
- "scripts/hook-continue" => "scripts/hook-continue",
333
- "scripts/hook-future-intent-check" => "scripts/hook-future-intent-check",
334
- "scripts/hook-gate-check" => "scripts/hook-gate-check",
335
- "scripts/hook-savepoint-pre" => "scripts/hook-savepoint-pre",
336
- "scripts/hook-power-tools" => "scripts/hook-power-tools",
337
- "scripts/lib/qmd_hook.rb" => "scripts/lib/qmd_hook.rb",
335
+ "scripts/hook-capture" => "scripts/hook-capture",
336
+ "scripts/hook-record" => "scripts/hook-record",
337
+ "scripts/hook-close" => "scripts/hook-close",
338
338
  "scripts/lib/power_tools.rb" => "scripts/lib/power_tools.rb",
339
339
  "scripts/lib/ruby_probe.rb" => "scripts/lib/ruby_probe.rb",
340
340
  "scripts/lib/agent_models.rb" => "scripts/lib/agent_models.rb",
341
341
  "scripts/lib/config_asks.rb" => "scripts/lib/config_asks.rb",
342
342
  "scripts/lib/release_guard.rb" => "scripts/lib/release_guard.rb",
343
- "scripts/hook-code-gate" => "scripts/hook-code-gate",
344
- "scripts/hook-lock-gate" => "scripts/hook-lock-gate",
345
- "scripts/hook-bash-gate" => "scripts/hook-bash-gate",
346
- "scripts/hook-auto-arm" => "scripts/hook-auto-arm",
347
343
  "scripts/lib/bridge.rb" => "scripts/lib/bridge.rb",
344
+ "scripts/lib/savepoint.rb" => "scripts/lib/savepoint.rb",
345
+ "scripts/lib/arm.rb" => "scripts/lib/arm.rb",
348
346
  "scripts/lib/lock.rb" => "scripts/lib/lock.rb",
349
347
  "scripts/plastic-lock" => "scripts/plastic-lock",
350
348
  "scripts/lib/hook_registry.rb" => "scripts/lib/hook_registry.rb",
349
+ "scripts/lib/compact_instructions.rb" => "scripts/lib/compact_instructions.rb",
351
350
  "scripts/agent-report" => "scripts/agent-report",
352
351
  "scripts/lib/insights.rb" => "scripts/lib/insights.rb",
353
352
  "scripts/insight-append" => "scripts/insight-append",
@@ -380,13 +379,7 @@ class InstallerCore
380
379
  "scripts/validate-intent" => "scripts/validate-intent",
381
380
  "scripts/new-intent" => "scripts/new-intent",
382
381
  "scripts/end-intent" => "scripts/end-intent",
383
- "scripts/hook-create-gate" => "scripts/hook-create-gate",
384
- "scripts/hook-links-gate" => "scripts/hook-links-gate",
385
- "scripts/lib/links_gate.rb" => "scripts/lib/links_gate.rb",
386
- "scripts/lib/edit_gates.rb" => "scripts/lib/edit_gates.rb",
387
- "scripts/hook-edit-gates" => "scripts/hook-edit-gates",
388
382
  "scripts/lib/apply_patch_envelope.rb" => "scripts/lib/apply_patch_envelope.rb",
389
- "scripts/lib/codex_edit_gates.rb" => "scripts/lib/codex_edit_gates.rb",
390
383
  "scripts/lib/harness_text.rb" => "scripts/lib/harness_text.rb",
391
384
  "scripts/codex-hook" => "scripts/codex-hook",
392
385
  "scripts/spawn-preamble" => "scripts/spawn-preamble",
@@ -401,24 +394,36 @@ class InstallerCore
401
394
  "scripts/uninstall.rb" => "scripts/uninstall.rb",
402
395
  "scripts/rollback.rb" => "scripts/rollback.rb",
403
396
  "scripts/lib/outcome_guard.rb" => "scripts/lib/outcome_guard.rb",
404
- "scripts/lib/spec_header.rb" => "scripts/lib/spec_header.rb",
405
397
  "scripts/lib/scaffold_intent.rb" => "scripts/lib/scaffold_intent.rb",
398
+ "scripts/lib/backfill_intent.rb" => "scripts/lib/backfill_intent.rb",
406
399
  "scripts/scaffold-intent" => "scripts/scaffold-intent",
407
400
  "scripts/lib/verify_intent.rb" => "scripts/lib/verify_intent.rb",
408
401
  "scripts/verify-intent" => "scripts/verify-intent",
409
- "scripts/lib/start_intent.rb" => "scripts/lib/start_intent.rb",
410
- "scripts/start-intent" => "scripts/start-intent",
411
402
  "scripts/lib/exec_worktree.rb" => "scripts/lib/exec_worktree.rb",
412
403
  "scripts/exec-worktree" => "scripts/exec-worktree",
413
404
  "scripts/doctor.rb" => "scripts/doctor.rb",
414
405
  "scripts/lib/doctor_core.rb" => "scripts/lib/doctor_core.rb",
415
406
  "scripts/lib/rule_catalog.rb" => "scripts/lib/rule_catalog.rb",
416
407
  "scripts/lib/doctor_exclusions.rb" => "scripts/lib/doctor_exclusions.rb",
408
+ "scripts/lib/doctor_session_ledger.rb" => "scripts/lib/doctor_session_ledger.rb",
417
409
  "scripts/dashboard.rb" => "scripts/dashboard.rb",
418
410
  "scripts/skill-lint" => "scripts/skill-lint",
419
411
  "scripts/lib/skill_lint.rb" => "scripts/lib/skill_lint.rb",
420
412
  "scripts/feedback-report" => "scripts/feedback-report",
421
413
  "scripts/lib/feedback_report.rb" => "scripts/lib/feedback_report.rb",
414
+ "scripts/append-ledger" => "scripts/append-ledger",
415
+ "scripts/lib/session_ledger.rb" => "scripts/lib/session_ledger.rb",
416
+ "scripts/session-commit" => "scripts/session-commit",
417
+ "scripts/file-session-intent" => "scripts/file-session-intent",
418
+ "scripts/promote-session-item" => "scripts/promote-session-item",
419
+ "scripts/lib/session_backfill.rb" => "scripts/lib/session_backfill.rb",
420
+ "scripts/lib/session_close.rb" => "scripts/lib/session_close.rb",
421
+ "scripts/lib/session_git.rb" => "scripts/lib/session_git.rb",
422
+ "scripts/lib/handoff.rb" => "scripts/lib/handoff.rb",
423
+ "scripts/lib/day_summary.rb" => "scripts/lib/day_summary.rb",
424
+ "scripts/write-handoff" => "scripts/write-handoff",
425
+ "scripts/day-summary" => "scripts/day-summary",
426
+ "scripts/hook-savepoint" => "scripts/hook-savepoint",
422
427
  }
423
428
  end
424
429
 
@@ -432,6 +437,8 @@ class InstallerCore
432
437
  version: 3
433
438
  execution_mode: subagent-driven
434
439
  stale_threshold_days: 3
440
+ context_offer_tokens: 350000
441
+ context_insist_tokens: 500000
435
442
  hash_length: 6
436
443
  hash_algorithm: sha256-base36
437
444
  max_slug_words: 5
@@ -718,7 +725,7 @@ class InstallerCore
718
725
  end
719
726
 
720
727
  # `install_skills_flat` relocates any top-level underscore-prefixed markdown
721
- # fragment (today: `_active-intent-gate.md`, `_decision-tables.md`) out of the
728
+ # fragment (today: `_decision-tables.md`) out of the
722
729
  # per-agent skills tree and into `plastic_home` directly, then lists them in
723
730
  # `installed`, so they are manifest-tracked and eligible for prune. But
724
731
  # `install_for_agent`'s prune roots are `[config[:dir], config[:home_dir]]`,
@@ -790,6 +797,11 @@ class InstallerCore
790
797
  choice = statusline_choice(settings_path, argv: argv, input: input, reinstall: reinstall)
791
798
  merge_claude_hooks(settings_path, choice: choice)
792
799
 
800
+ # Instruction injection (intent 312): the compact-instructions block into
801
+ # ~/.claude/CLAUDE.md. A partial-ownership user file, so it is NOT manifest-tracked
802
+ # (stripped surgically on uninstall), the same treatment ~/.codex/AGENTS.md gets.
803
+ inject_claude_compact_md(File.join(config[:dir], "CLAUDE.md"))
804
+
793
805
  # Write manifest
794
806
  manifest_path = File.join(plastic_dir, "manifest.json")
795
807
  write_manifest(installed, manifest_path)
@@ -1012,7 +1024,7 @@ class InstallerCore
1012
1024
 
1013
1025
  # Copy each skills/<name>/ to <skills_root>/plastic-<name>/ (flat, namespaced by
1014
1026
  # directory name -- the only personal-skill namespacing Claude Code supports).
1015
- # Any top-level underscore-prefixed markdown fragment (e.g. `_active-intent-gate.md`,
1027
+ # Any top-level underscore-prefixed markdown fragment (e.g. `_decision-tables.md`,
1016
1028
  # `_decision-tables.md`) is a shared non-skill fragment and relocates to ~/.plastic/
1017
1029
  # instead, so every skill can read it from one shared location. `exclude` skips
1018
1030
  # named top-level skill directories entirely (intent 185: the agent-advisor skill
@@ -1273,8 +1285,8 @@ class InstallerCore
1273
1285
  plastic_hooks.each do |event, group|
1274
1286
  hooks[event] ||= []
1275
1287
 
1276
- # An event may map to a LIST of plastic groups (PreToolUse carries the
1277
- # code-gate AND the create-gate). The purge pass above already removed all
1288
+ # An event may map to a LIST of plastic groups (none does since the edit-path
1289
+ # gates left in 2.0, intent 302; the shape stays). The purge pass above already removed all
1278
1290
  # prior plastic groups, so appending each desired group fresh is idempotent
1279
1291
  # across re-runs and never collapses two matchers into one group.
1280
1292
  groups = group.is_a?(Array) ? group : [group]
@@ -1355,14 +1367,28 @@ class InstallerCore
1355
1367
  raise e
1356
1368
  end
1357
1369
 
1358
- def codex_section(body: CODEX_AGENTS_MD_BODY)
1370
+ # A managed instruction file may be a symlink into a dotfiles repo. write_text_atomic
1371
+ # renames a temp file over its argument, which would replace the link with a regular
1372
+ # file and silently detach it, so every read and write resolves the link first and the
1373
+ # change lands on its target (intent 312).
1374
+ def resolve_managed_path(path)
1375
+ File.symlink?(path) ? File.realpath(path) : path
1376
+ rescue Errno::ENOENT
1377
+ path
1378
+ end
1379
+
1380
+ def marked_section(body: CODEX_AGENTS_MD_BODY, begin_prefix: CODEX_SECTION_BEGIN_PREFIX,
1381
+ end_marker: CODEX_SECTION_END)
1359
1382
  hash = Digest::SHA256.hexdigest(body)[0, 12]
1360
- "#{CODEX_SECTION_BEGIN_PREFIX} hash:#{hash} -->\n#{body.strip}\n#{CODEX_SECTION_END}\n"
1383
+ "#{begin_prefix} hash:#{hash} -->\n#{body.strip}\n#{end_marker}\n"
1361
1384
  end
1362
1385
 
1363
1386
  # Returns :created / :appended / :replaced / :refused. Never raises on a normal user file.
1364
- def inject_codex_agents_md(path, body: CODEX_AGENTS_MD_BODY)
1365
- section = codex_section(body: body)
1387
+ def inject_marked_section(path, body: CODEX_AGENTS_MD_BODY,
1388
+ begin_prefix: CODEX_SECTION_BEGIN_PREFIX,
1389
+ end_marker: CODEX_SECTION_END, section_re: CODEX_SECTION_RE)
1390
+ section = marked_section(body: body, begin_prefix: begin_prefix, end_marker: end_marker)
1391
+ path = resolve_managed_path(path)
1366
1392
 
1367
1393
  unless File.exist?(path)
1368
1394
  FileUtils.mkdir_p(File.dirname(path))
@@ -1371,14 +1397,14 @@ class InstallerCore
1371
1397
  end
1372
1398
 
1373
1399
  content = File.read(path)
1374
- has_begin = content.include?(CODEX_SECTION_BEGIN_PREFIX)
1375
- has_end = content.include?(CODEX_SECTION_END)
1400
+ has_begin = content.include?(begin_prefix)
1401
+ has_end = content.include?(end_marker)
1376
1402
 
1377
1403
  # 22a safety rule: never write if the existing section cannot be parsed.
1378
1404
  return :refused if has_begin && !has_end
1379
1405
 
1380
1406
  if has_begin
1381
- write_text_atomic(path, content.sub(CODEX_SECTION_RE, section))
1407
+ write_text_atomic(path, content.sub(section_re, section))
1382
1408
  :replaced
1383
1409
  else
1384
1410
  base = content.end_with?("\n") ? content : content + "\n"
@@ -1387,18 +1413,41 @@ class InstallerCore
1387
1413
  end
1388
1414
  end
1389
1415
 
1416
+ # --- The two blocks Plastic ships, each with its own marker pair ---
1417
+
1418
+ def codex_section(body: CODEX_AGENTS_MD_BODY)
1419
+ marked_section(body: body)
1420
+ end
1421
+
1422
+ def inject_codex_agents_md(path, body: CODEX_AGENTS_MD_BODY)
1423
+ inject_marked_section(path, body: body)
1424
+ end
1425
+
1426
+ # The compact-instructions block for ~/.claude/CLAUDE.md (intent 312).
1427
+ def claude_compact_section(body: CompactInstructions::BODY)
1428
+ marked_section(body: body, begin_prefix: CLAUDE_SECTION_BEGIN_PREFIX,
1429
+ end_marker: CLAUDE_SECTION_END)
1430
+ end
1431
+
1432
+ def inject_claude_compact_md(path, body: CompactInstructions::BODY)
1433
+ inject_marked_section(path, body: body, begin_prefix: CLAUDE_SECTION_BEGIN_PREFIX,
1434
+ end_marker: CLAUDE_SECTION_END, section_re: CLAUDE_SECTION_RE)
1435
+ end
1436
+
1390
1437
  # Remove exactly Plastic's managed section from a user-owned AGENTS.md. Preserve all other
1391
1438
  # content. Delete the file only if Plastic created it and nothing else remains. Returns the
1392
1439
  # path when it acted, nil on no-op. Mirrors remove_claude_hooks: dedicated surgical strip,
1393
1440
  # never the manifest whole-file-delete path.
1394
- def strip_codex_section(path)
1441
+ def strip_marked_section(path, begin_prefix: CODEX_SECTION_BEGIN_PREFIX,
1442
+ section_re: CODEX_SECTION_RE)
1443
+ path = resolve_managed_path(path)
1395
1444
  return nil unless File.exist?(path)
1396
1445
  content = File.read(path)
1397
- return nil unless content.include?(CODEX_SECTION_BEGIN_PREFIX)
1446
+ return nil unless content.include?(begin_prefix)
1398
1447
 
1399
1448
  # Remove the section plus the single separator newline the append introduced, so a
1400
1449
  # standard user file round-trips byte-identical.
1401
- stripped = content.sub(/\n?#{CODEX_SECTION_RE}/, "")
1450
+ stripped = content.sub(/\n?#{section_re}/, "")
1402
1451
 
1403
1452
  if stripped.strip.empty?
1404
1453
  File.delete(path) # Plastic-created file: nothing else left
@@ -1409,6 +1458,15 @@ class InstallerCore
1409
1458
  path
1410
1459
  end
1411
1460
 
1461
+ def strip_codex_section(path)
1462
+ strip_marked_section(path)
1463
+ end
1464
+
1465
+ def strip_claude_compact_section(path)
1466
+ strip_marked_section(path, begin_prefix: CLAUDE_SECTION_BEGIN_PREFIX,
1467
+ section_re: CLAUDE_SECTION_RE)
1468
+ end
1469
+
1412
1470
  # --- Uninstall ---
1413
1471
 
1414
1472
  def handle_uninstall(uninstall_agents)
@@ -1435,6 +1493,7 @@ class InstallerCore
1435
1493
  puts " ls ~/.claude/skills | grep '^plastic-' # → no output"
1436
1494
  puts " ls ~/.claude/hooks | grep '^plastic-' # → no output"
1437
1495
  puts " grep -c plastic ~/.claude/settings.json # → only hook refs gone"
1496
+ puts " grep 'PLASTIC COMPACT' ~/.claude/CLAUDE.md # → no output"
1438
1497
  puts "\n To also delete your intent store: rm -rf #{tilde(plastic_home)}\n\n"
1439
1498
  end
1440
1499
 
@@ -1481,6 +1540,11 @@ class InstallerCore
1481
1540
  settings_path = File.join(config[:dir], "settings.json")
1482
1541
  remove_claude_hooks(settings_path) if File.exist?(settings_path)
1483
1542
  removed.concat(migrate_legacy_plugin(config[:dir]))
1543
+
1544
+ # The compact-instructions block in the user-owned CLAUDE.md (intent 312): a
1545
+ # surgical strip, never the manifest whole-file-delete path above.
1546
+ stripped = strip_claude_compact_section(File.join(config[:dir], "CLAUDE.md"))
1547
+ removed << stripped if stripped
1484
1548
  end
1485
1549
 
1486
1550
  # Codex: surgically strip Plastic's marked section from the user-owned AGENTS.md
@@ -49,11 +49,11 @@ module Lock
49
49
  DELEGATE_ACTIVITY_LIMIT = 20
50
50
  DELEGATE_STATUSES = %w[active finished failed].freeze
51
51
 
52
- # Lease TTL. Heartbeats fire from the write-path hooks (PostToolUse
53
- # gate-check and the lock-gate allow path), so a delivering session
54
- # refreshes constantly; 30 minutes tolerates long read-only stretches
55
- # without opening a takeover window mid-delivery. Reclaim is explicit
56
- # either way (takeover), so the TTL only bounds WHEN takeover is allowed.
52
+ # Lease TTL. Heartbeats fire from the PostToolUse record hook on every write
53
+ # inside the intent dir (intent 302), so a delivering session refreshes
54
+ # constantly; 30 minutes tolerates long read-only stretches without opening
55
+ # a takeover window mid-delivery. Reclaim is explicit either way (takeover),
56
+ # so the TTL only bounds WHEN takeover is allowed.
57
57
  TTL_SECONDS = 1800
58
58
 
59
59
  # The write guard is a mutex, not a lock in the Plastic sense: it carries no
@@ -122,7 +122,7 @@ module Lock
122
122
  Array(data["delegates"]).map(&:to_s).include?(session.to_s)
123
123
  end
124
124
 
125
- # The one question gates ask: does session hold this intent's lock?
125
+ # The one question the write path asks: does session hold this intent's lock?
126
126
  # Owner/delegate on an EXISTING lock counts even when stale (a stale lock is
127
127
  # still theirs until an explicit takeover replaces it); freshness only
128
128
  # guards AGAINST other sessions.
@@ -676,24 +676,4 @@ module Claim
676
676
  end
677
677
  end
678
678
 
679
- # Second, independent write gate at the artifact grain (intent 111 D7). Returns a
680
- # deny reason String to BLOCK, or nil to ALLOW. Composes UNDER the delivery-lock
681
- # gate: only reached after the session already holds the intent's delivery lock.
682
- # ENGAGES only when a claim file exists (dormant otherwise, so single-owner flows
683
- # and the existing suite stay green, AC7). Fails open on stale/corrupt via
684
- # fail_open?, the named contract.
685
- def claim_gate_reason(intent_dir, artifact, session:, ttl: Lock::TTL_SECONDS, now: Time.now,
686
- harness: :claude)
687
- return nil if Lock.blank?(artifact)
688
- return nil unless File.exist?(path(intent_dir, artifact)) # dormant: no claim
689
- return nil if holds_claim?(intent_dir, artifact, session: session) # you hold it
690
- return nil if fail_open?(intent_dir, artifact, ttl: ttl, now: now) # stale/corrupt: yield
691
- data = read(intent_dir, artifact)
692
- holder = data && data["owner_session"]
693
- since = data && data["acquired_at"]
694
- "artifact #{artifact} is claimed by #{holder} since #{since}; another writer holds " \
695
- "it. Back off or run #{Lock.skill_ref('plastic-doctor', harness: harness)} check the " \
696
- "lock status. If you are a distinct delegate, the owner must register you: " \
697
- "plastic-lock delegate --intent-dir #{intent_dir} --session <your-session-id>"
698
- end
699
679
  end
@@ -1,14 +1,16 @@
1
1
  # encoding: UTF-8
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative "bridge"
4
+ require_relative "savepoint"
5
5
  require_relative "intent_validator"
6
6
 
7
7
  # OutcomeGuard - the single definition of "is outcome.md real and disposition-matched for
8
8
  # THIS close" (intent 222). Extracted verbatim from scripts/end-intent's own
9
- # outcome_guard_reason (intent 161/188) so end-intent's existing exit-2 path and doctor's new
10
- # per-intent check (intent_lifecycle_artifacts) can never independently drift on what counts
11
- # as a valid outcome.md. Pure, read-only: authors nothing regardless of outcome.
9
+ # outcome_guard_reason (intent 161/188) so its two callers, doctor's per-intent check
10
+ # (intent_lifecycle_artifacts) and end-intent's post-backfill self-check, can never drift on
11
+ # what counts as a valid outcome.md. Since intent 308 nothing blocks on it: end-intent
12
+ # backfills a missing or placeholder outcome.md first, prints a non-nil reason, and proceeds.
13
+ # Pure, read-only: authors nothing regardless of outcome.
12
14
  module OutcomeGuard
13
15
  module_function
14
16
 
@@ -23,7 +25,7 @@ module OutcomeGuard
23
25
 
24
26
  content = File.read(path)
25
27
  first_line = content.each_line.first.to_s.chomp
26
- if first_line == Bridge::PLACEHOLDER_SENTINEL
28
+ if first_line == Savepoint::PLACEHOLDER_SENTINEL
27
29
  return "outcome.md is still the scaffold placeholder (first line is the sentinel)"
28
30
  end
29
31
 
@@ -24,6 +24,15 @@ require "yaml"
24
24
  module ProjectValidator
25
25
  module_function
26
26
 
27
+ # The two enumerated flow: knobs (intent 300, spec D2). Kept as a plain
28
+ # literal here rather than a require on SessionGit: this validator only
29
+ # reports a bad value in `errors` (project.yml is still "accepted", spec
30
+ # D2's matrix row), it never blocks spawn completeness, since a bad value
31
+ # degrades gracefully at commit time (SessionGit falls back with a Note)
32
+ # rather than blocking the spawn.
33
+ FLOW_MODES = %w[direct pull_request].freeze
34
+ FLOW_WORKSPACES = %w[checkout worktree].freeze
35
+
27
36
  def validate(slug, plastic_home: File.join(Dir.home, ".plastic"))
28
37
  missing = []
29
38
  errors = []
@@ -53,7 +62,9 @@ module ProjectValidator
53
62
  rescue StandardError
54
63
  nil
55
64
  end
56
- unless parsed.is_a?(Hash)
65
+ if parsed.is_a?(Hash)
66
+ validate_flow_block(parsed, errors)
67
+ else
57
68
  missing << "project.yml (valid YAML)"
58
69
  errors << "project.yml exists at #{project_yml_path} but does not parse as YAML"
59
70
  end
@@ -88,6 +99,24 @@ module ProjectValidator
88
99
  { ok: missing.empty?, missing: missing, errors: errors }
89
100
  end
90
101
 
102
+ # Non-blocking: an unknown `mode` or `workspace` value in project.yml's
103
+ # `flow:` block, if present, is reported in `errors` but never added to
104
+ # `missing`, so it never flips `ok`.
105
+ def validate_flow_block(parsed, errors)
106
+ flow = parsed["flow"]
107
+ return unless flow.is_a?(Hash)
108
+
109
+ validate_flow_knob(flow, "mode", FLOW_MODES, errors)
110
+ validate_flow_knob(flow, "workspace", FLOW_WORKSPACES, errors)
111
+ end
112
+
113
+ def validate_flow_knob(flow, key, allowed, errors)
114
+ value = flow[key]
115
+ return if value.nil? || allowed.include?(value.to_s)
116
+
117
+ errors << "project.yml flow.#{key} is #{value.inspect}, must be one of #{allowed.join(", ")}"
118
+ end
119
+
91
120
  # Invariant 1: registered in projects.yml with a 'path'. Returns the
92
121
  # project's entry Hash, or nil when unregistered or the entry has no path.
93
122
  def registration_for(slug, plastic_home)
@@ -20,10 +20,10 @@ class FileOrderRanker
20
20
  end
21
21
  end
22
22
 
23
- # RoadmapQueue - the one deterministic reader the auto loop and plastic-roadmap-continuing
23
+ # RoadmapQueue - the one deterministic reader the auto loop and plastic-intent-continuing
24
24
  # both call (intent 148). Constructor-DI, hermetic: clock and paths injected, no eval, no ENV
25
25
  # or global config seam. It does two things: liveness-ranks a tier's roadmaps/*.md files
26
- # (porting plastic-roadmap-continuing's read-time algorithm), and, within the winning
26
+ # (porting plastic-intent-continuing's read-time algorithm), and, within the winning
27
27
  # roadmap, selects the frontier wave plus its dispatchable set (D-b), value-ordered by the
28
28
  # injected ranker (default FileOrderRanker, the intent-173 swap seam). Every frontier token is
29
29
  # reconciled against INDEX.md first, INDEX wins. Reads through the 134 ledger via the public
@@ -181,7 +181,7 @@ class RoadmapQueue
181
181
  File.join(File.dirname(@roadmaps_dir), "INDEX.md")
182
182
  end
183
183
 
184
- # --- liveness ranking (ports plastic-roadmap-continuing's read-time algorithm) -
184
+ # --- liveness ranking (ports plastic-intent-continuing's read-time algorithm) -
185
185
 
186
186
  def rank_candidates(parsed_list)
187
187
  parsed_list.map do |c|
@@ -102,7 +102,7 @@ module RoadmapSavepoint
102
102
  # entry has a `merged` line. Every timestamp comes from an on-disk source (the Log, or INDEX
103
103
  # `## Completed`); an entry with no recoverable timestamp is not emitted (D4, never invented).
104
104
  # Overwrites the ledger (the one operation allowed to rewrite it, matching
105
- # `Bridge.rebuild_savepoint`). Returns the number of lines written.
105
+ # `Savepoint.rebuild_savepoint`). Returns the number of lines written.
106
106
  def rebuild(roadmap_path)
107
107
  text = File.read(roadmap_path)
108
108
  log_lines = classify_log(section_body(text, "Log"))
@@ -5,9 +5,9 @@
5
5
  # different axes so there is exactly one place to look up or register a rule name:
6
6
  #
7
7
  # EXCLUDABLE_CHECKS - a doctor check `name` a per-store doctor-exclusions file may name. A
8
- # check name says WHICH DIAGNOSTIC FIRED. v1 carries exactly one key, savepoint_operational
9
- # (see spec D3): most doctor checks have no exclusion mechanism at all, and this is the only
10
- # one the owner asked to make skippable.
8
+ # check name says WHICH DIAGNOSTIC FIRED. Two keys: savepoint_operational (274, spec D3) and
9
+ # backfilled_complete (308). Most doctor checks have no exclusion mechanism at all; these two
10
+ # are the ones whose gap can be a known, accepted fact on an old or migrated intent.
11
11
  #
12
12
  # REVISION_RULES - the `[rule: <tag>]` vocabulary every revisions.md entry must carry
13
13
  # (scripts/lib/revisions_writer.rb). A tag says WHY an intent's files were structurally
@@ -33,6 +33,9 @@ module RuleCatalog
33
33
  "delivered|abandoned echo, on a terminal intent. Usually " \
34
34
  "repairable via maintenance-run --tool rebuild-savepoint; " \
35
35
  "excludable for the gaps 219 D6 forbids ever repairing.",
36
+ "backfilled_complete" => "spec.md or plan.md missing or still the placeholder, or no real " \
37
+ "action file, on a terminal intent. Repairable via scaffold-intent " \
38
+ "backfill; excludable for an old intent whose record was never kept.",
36
39
  }.freeze
37
40
 
38
41
  # Measured from live store data across all eight stores, 2026-08-24 (spec D1).