@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
@@ -82,8 +82,6 @@ class Rollback < InstallerCore
82
82
  File.exist?(path) ? File.read(path).strip : nil
83
83
  end
84
84
 
85
- private
86
-
87
85
  def flag_value(argv, name)
88
86
  i = argv.index(name)
89
87
  return nil unless i && argv[i + 1]
@@ -93,11 +91,58 @@ class Rollback < InstallerCore
93
91
  def switch_to(target, current)
94
92
  action = action_for(target, current)
95
93
  puts "#{action == "downgrade" ? "\u{23ea}" : "\u{23e9}"} #{current} \u{2192} #{target} (#{action})"
96
- cmd = ["npx", "#{PKG}@#{target}", "install", "--reinstall", "--ledger-action", action, "--claude"]
94
+ prepare_switch(target, current)
95
+ cmd = ["npx", "#{PKG}@#{target}", "install", "--reinstall", "--ledger-action", action, *harness_flags]
97
96
  puts " $ #{cmd.join(" ")}"
98
97
  system(*cmd) ? 0 : 1
99
98
  end
100
99
 
100
+ # Before handing off to an older package (a downgrade), strip Plastic's own current hook
101
+ # registrations from every installed harness (intent 310, cut-inventory R2). The older
102
+ # installer purges only the names its own registry knows, so an entry this version
103
+ # registered would otherwise survive while the older manifest diff deletes its launcher,
104
+ # and the older doctor would then report an unowned plastic- command. The older installer
105
+ # registers its own set fresh on the cleaned file. Returns the paths it stripped.
106
+ def prepare_switch(target, current)
107
+ return [] unless action_for(target, current) == "downgrade"
108
+
109
+ stripped = []
110
+ installed_agents.each do |key|
111
+ agent = agents.find { |a| a[:key] == key }
112
+ next unless agent
113
+
114
+ case key
115
+ when "claude"
116
+ path = File.join(agent[:dir], "settings.json")
117
+ if File.exist?(path)
118
+ remove_claude_hooks(path)
119
+ stripped << path
120
+ end
121
+ # The compact-instructions block (intent 312), for the same reason: no older
122
+ # package knows the section exists, so nothing there would ever replace or
123
+ # remove it. The Codex AGENTS.md section needs no equivalent: every older
124
+ # package knows that one and rewrites it on the downgrade install.
125
+ claude_md = strip_claude_compact_section(File.join(agent[:dir], "CLAUDE.md"))
126
+ stripped << claude_md if claude_md
127
+ when "codex"
128
+ path = agent[:home_dir] && File.join(agent[:home_dir], "hooks.json")
129
+ if path && File.exist?(path)
130
+ remove_codex_hooks(path)
131
+ stripped << path
132
+ end
133
+ end
134
+ end
135
+ stripped
136
+ end
137
+
138
+ # The install flags for every harness with a per-agent record (intent 310); Claude alone
139
+ # when nothing is recorded, matching the first-install default.
140
+ def harness_flags
141
+ keys = installed_agents
142
+ keys = ["claude"] if keys.empty?
143
+ keys.map { |k| "--#{k}" }
144
+ end
145
+
101
146
  def print_table(ledger, current)
102
147
  puts "\nPlastic version history (versions.json — append-only):\n\n"
103
148
  puts " %-22s %-10s %s" % ["version", "action", "at"]
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+
5
+ # savepoint-note - the writer CLI for the two savepoint kinds intent 317 adds:
6
+ # Review (one line per plan-review or post-execution-review verdict) and
7
+ # Commit (one line per commit landing during Exec). D17: there is no automatic
8
+ # seam for either (session-commit writes the day ledger, not the intent
9
+ # ledger; the executor's own commits are plain git), so this is an explicit
10
+ # thin wrapper on Savepoint.append_review_savepoint / append_commit_savepoint.
11
+ #
12
+ # Usage:
13
+ # savepoint-note <intent_dir> --kind Review|Commit --text "<text>"
14
+ #
15
+ # --text is normalized (D21): runs of two or more spaces collapse to one, so a
16
+ # free-text kind that happens to contain a double space cannot key the dedup
17
+ # primitive's split(/\s{2,}/) on the prefix alone and silently drop a later
18
+ # line. A newline in --text is refused outright (exit 2, ledger unchanged)
19
+ # rather than silently splitting one entry into two malformed ledger lines.
20
+ #
21
+ # Exit codes:
22
+ # 0 - the line was appended (or was already recorded; both are success)
23
+ # 2 - usage error, bad --kind, missing --text, a newline in --text, or the
24
+ # path is not an intent directory (one line on stderr, ledger unchanged)
25
+
26
+ require_relative "lib/savepoint"
27
+
28
+ def usage_abort(message)
29
+ warn "savepoint-note: #{message}"
30
+ exit 2
31
+ end
32
+
33
+ args = ARGV.dup
34
+ kind = nil
35
+ text = nil
36
+ positional = []
37
+ while (arg = args.shift)
38
+ case arg
39
+ when "--kind"
40
+ kind = args.shift
41
+ when "--text"
42
+ text = args.shift
43
+ else
44
+ usage_abort("unknown flag #{arg.inspect}") if arg.start_with?("--")
45
+ positional << arg
46
+ end
47
+ end
48
+
49
+ usage_abort("usage: savepoint-note <intent_dir> --kind Review|Commit --text \"<text>\"") unless positional.length == 1
50
+
51
+ intent_dir = File.expand_path(positional.first)
52
+ usage_abort("#{intent_dir} is not an intent directory") unless File.exist?(Savepoint.intent_file(intent_dir))
53
+
54
+ KINDS = %w[Review Commit].freeze
55
+ usage_abort("--kind must be one of #{KINDS.join(', ')}, got #{kind.inspect}") unless KINDS.include?(kind)
56
+ usage_abort("--text is required") if text.nil? || text.empty?
57
+ usage_abort("--text must not contain a newline") if text.include?("\n")
58
+
59
+ normalized = text.gsub(/ {2,}/, " ").strip
60
+ usage_abort("--text is empty after normalization") if normalized.empty?
61
+
62
+ case kind
63
+ when "Review" then Savepoint.append_review_savepoint(intent_dir, normalized)
64
+ when "Commit" then Savepoint.append_commit_savepoint(intent_dir, normalized)
65
+ end
66
+
67
+ exit 0
@@ -2,46 +2,38 @@
2
2
  # encoding: UTF-8
3
3
  # frozen_string_literal: true
4
4
 
5
- # scaffold-intent (intent 213) - one CLI, three subcommands, each writes one lifecycle
6
- # artifact by copying or mechanically deriving it from an already-committed source. Every
7
- # field it writes is a verbatim copy or a mechanical derivation (a `git diff --stat`, a
8
- # supplied file's contents) of an already-committed artifact; no subcommand interprets or
9
- # invents prose. A field it cannot derive mechanically is left as the template's own stub
10
- # text instead of being guessed at.
5
+ # scaffold-intent (intent 213; one verb since intent 308) - write an intent's judgment
6
+ # documents from its record. `backfill` fills spec.md, plan.md, actions/ACTION_1.md, and
7
+ # outcome.md wherever the file is missing or still the scaffold placeholder, from the
8
+ # intent file, checklist.md, and the diff (BackfillIntent). Every section it writes is a
9
+ # verbatim copy or a mechanical rendering of an already-committed artifact; a section with
10
+ # no source keeps the template's own stub text. A hand-written file is never touched, and
11
+ # there is no force flag: real content is edited by hand or not at all.
11
12
  #
12
- # Usage:
13
- # scaffold-intent spec|checklist|outcome --store <path> --id <intent_id> [--force]
14
- # [--test-summary <path>] (outcome subcommand only)
13
+ # `scripts/end-intent` runs the same writer at every close, so this CLI exists for the
14
+ # doctor fix hint (backfilled_complete) and for filling an intent by hand before its close.
15
15
  #
16
- # Subcommand is the first positional argument. --store and --id are required for every
17
- # subcommand. --test-summary is valid only on the outcome subcommand; on spec or
18
- # checklist, or naming a path that does not exist, it is a usage error (exit 1). No other
19
- # flags exist: --store, --id, --force, and --test-summary (outcome only) are the whole
20
- # shipped surface (D5, a one-way door).
16
+ # Usage:
17
+ # scaffold-intent backfill --store <path> --id <intent_id> --disposition delivered|abandoned
18
+ # [--summary <text>]
21
19
  #
22
20
  # Exit codes:
23
- # 0 scaffolded
21
+ # 0 written, or nothing left to write
24
22
  # 1 usage or path-resolution failure
25
- # 2 refused to overwrite real content without --force
26
- # 3 a prerequisite committed artifact is missing or still sentinel
27
- #
28
- # The actions/ rule (intent 133a, D11): scaffold-intent does not scaffold actions/ in any
29
- # form. Actions require judgment and stay entirely with the planner. No subcommand
30
- # creates the directory, writes a .gitkeep, or writes a sentinel-only ACTION_*.md.
23
+ # 3 a source is unusable (the intent file is missing)
31
24
  #
32
- # outcome is an end-of-Exec step. Run it once the diff and the test summary exist, right
33
- # before plastic-intent-ending writes the narrative. Writing outcome.md makes the intent
34
- # read as Done to any stage-derived display (Bridge.derive_stage keys on outcome.md's
35
- # presence). This does NOT purge a bridge:
36
- # Bridge.purge_done_bridges keys on INDEX Active status plus lock presence, never on the
37
- # derived stage.
25
+ # The spec, checklist, and outcome subcommands, --force, and --test-summary were removed
26
+ # in 2.0 (intent 308): the backfill writer covers their two derivable targets and the
27
+ # outcome diffstat, and nothing else called them.
38
28
 
39
29
  require_relative "lib/scaffold_intent"
30
+ require_relative "lib/backfill_intent"
40
31
 
41
- SUBCOMMANDS = %w[spec checklist outcome].freeze
32
+ SUBCOMMANDS = %w[backfill].freeze
33
+ DISPOSITIONS = %w[delivered abandoned].freeze
42
34
 
43
35
  def parse_args(argv)
44
- opts = { subcommand: nil, store: nil, id: nil, force: false, test_summary: nil }
36
+ opts = { subcommand: nil, store: nil, id: nil, disposition: nil, summary: nil }
45
37
  args = argv.dup
46
38
  opts[:subcommand] = args.shift
47
39
 
@@ -49,10 +41,10 @@ def parse_args(argv)
49
41
  while i < args.length
50
42
  arg = args[i]
51
43
  case arg
52
- when "--store" then opts[:store] = args[i += 1]
53
- when "--id" then opts[:id] = args[i += 1]
54
- when "--force" then opts[:force] = true
55
- when "--test-summary" then opts[:test_summary] = args[i += 1]
44
+ when "--store" then opts[:store] = args[i += 1]
45
+ when "--id" then opts[:id] = args[i += 1]
46
+ when "--disposition" then opts[:disposition] = args[i += 1]
47
+ when "--summary" then opts[:summary] = args[i += 1]
56
48
  else
57
49
  usage_abort("unknown argument #{arg.inspect}")
58
50
  end
@@ -62,8 +54,8 @@ def parse_args(argv)
62
54
  end
63
55
 
64
56
  def usage
65
- "usage: scaffold-intent spec|checklist|outcome --store <store_path> --id <intent_id> " \
66
- "[--force] [--test-summary <path>]"
57
+ "usage: scaffold-intent backfill --store <store_path> --id <intent_id> " \
58
+ "--disposition delivered|abandoned [--summary <text>]"
67
59
  end
68
60
 
69
61
  def usage_abort(message)
@@ -75,11 +67,11 @@ end
75
67
  def main(argv)
76
68
  opts = parse_args(argv)
77
69
 
78
- usage_abort("subcommand must be one of #{SUBCOMMANDS.join('|')}") unless SUBCOMMANDS.include?(opts[:subcommand])
70
+ usage_abort("subcommand must be #{SUBCOMMANDS.join('|')}") unless SUBCOMMANDS.include?(opts[:subcommand])
79
71
  usage_abort("--store is required") if opts[:store].nil? || opts[:store].empty?
80
72
  usage_abort("--id is required") if opts[:id].nil? || opts[:id].empty?
81
- if opts[:test_summary] && opts[:subcommand] != "outcome"
82
- usage_abort("--test-summary is valid only on the outcome subcommand")
73
+ unless DISPOSITIONS.include?(opts[:disposition])
74
+ usage_abort("--disposition must be one of #{DISPOSITIONS.join('|')}")
83
75
  end
84
76
 
85
77
  store = ScaffoldIntent.expand(opts[:store])
@@ -88,33 +80,15 @@ def main(argv)
88
80
  intent_dir, err = ScaffoldIntent.resolve_intent_dir(store, opts[:id])
89
81
  usage_abort(err) if intent_dir.nil?
90
82
 
91
- test_summary_path = nil
92
- if opts[:test_summary]
93
- test_summary_path = ScaffoldIntent.expand(opts[:test_summary])
94
- usage_abort("--test-summary path does not exist: #{test_summary_path}") unless File.exist?(test_summary_path)
95
- end
83
+ result = BackfillIntent.run(intent_dir: intent_dir, store: store, id: opts[:id],
84
+ disposition: opts[:disposition], summary: opts[:summary])
96
85
 
97
- result =
98
- case opts[:subcommand]
99
- when "spec"
100
- ScaffoldIntent.scaffold_spec(intent_dir: intent_dir, force: opts[:force])
101
- when "checklist"
102
- ScaffoldIntent.scaffold_checklist(intent_dir: intent_dir, force: opts[:force])
103
- when "outcome"
104
- ScaffoldIntent.scaffold_outcome(intent_dir: intent_dir, force: opts[:force], store: store,
105
- id: opts[:id], test_summary: test_summary_path)
106
- end
107
-
108
- if result[:message]
109
- if result[:code] == 0
110
- puts result[:message]
111
- else
112
- warn "scaffold-intent: #{result[:message]}"
113
- end
114
- end
115
- puts result[:path] if result[:path]
86
+ result[:written].each { |rel| puts "scaffold-intent: backfilled #{File.join(intent_dir, rel)}" }
87
+ result[:skipped].each { |rel| puts "scaffold-intent: kept #{rel} (real content)" }
88
+ result[:notes].each { |note| warn "scaffold-intent: #{note}" }
89
+ puts "scaffold-intent: nothing to backfill" if result[:written].empty? && result[:notes].empty?
116
90
 
117
- exit result[:code]
91
+ exit(result[:notes].any? { |n| n.include?("intent file is missing") } ? 3 : 0)
118
92
  end
119
93
 
120
94
  main(ARGV) if $PROGRAM_NAME == __FILE__
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+
5
+ # session-commit (intent 300) - makes one commit for a verified checklist
6
+ # item on the right branch of the repo that contains `--cwd`, applying the
7
+ # branch model and per-repo flow settings in `scripts/lib/session_git.rb`.
8
+ # Idempotent and fail-open: no repository, a detached HEAD, a clean tree, an
9
+ # agent-owned branch, a refused push, a missing `gh`, or a rejected
10
+ # commit-msg hook all degrade to no commit plus a Note savepoint line,
11
+ # never a non-zero exit (spec D1).
12
+ #
13
+ # Usage:
14
+ # session-commit --cwd <dir> --summary <text> [--session <id>] [--day <YYYYMMDD>]
15
+ # [--store <dir>] [--plastic-home <dir>]
16
+ #
17
+ # Exit codes:
18
+ # 0 - always, except a usage error
19
+ # 2 - usage error (missing --cwd or --summary); nothing is written
20
+
21
+ require "fileutils"
22
+ require_relative "lib/session_git"
23
+ require_relative "lib/session_ledger"
24
+
25
+ def usage_abort(message)
26
+ warn "session-commit: #{message}"
27
+ exit 2
28
+ end
29
+
30
+ def next_value!(argv, i, flag)
31
+ usage_abort("#{flag} requires a value") if i + 1 >= argv.length
32
+ argv[i + 1]
33
+ end
34
+
35
+ def parse_args(argv)
36
+ opts = { cwd: nil, summary: nil, session: nil, day: nil, store: nil, plastic_home: nil }
37
+ i = 0
38
+ while i < argv.length
39
+ arg = argv[i]
40
+ case arg
41
+ when "--cwd" then opts[:cwd] = next_value!(argv, i, arg); i += 1
42
+ when "--summary" then opts[:summary] = next_value!(argv, i, arg); i += 1
43
+ when "--session" then opts[:session] = next_value!(argv, i, arg); i += 1
44
+ when "--day" then opts[:day] = next_value!(argv, i, arg); i += 1
45
+ when "--store" then opts[:store] = next_value!(argv, i, arg); i += 1
46
+ when "--plastic-home" then opts[:plastic_home] = next_value!(argv, i, arg); i += 1
47
+ else
48
+ usage_abort("unknown flag #{arg.inspect}")
49
+ end
50
+ i += 1
51
+ end
52
+ opts
53
+ end
54
+
55
+ def expand(path)
56
+ File.expand_path(path.to_s.sub(/\A~/, Dir.home))
57
+ end
58
+
59
+ # Sibling of this script's dir, matching append-ledger's own resolution:
60
+ # in-repo (<repo>/scripts/session-commit -> <repo>/templates) and installed
61
+ # (~/.plastic/scripts/session-commit -> ~/.plastic/templates).
62
+ def default_templates_dir
63
+ File.expand_path("../templates", __dir__)
64
+ end
65
+
66
+ # The fail-open guarantee (spec D1: exit 0 always, except a usage error)
67
+ # must survive a store or ledger failure too (review R2), not only a git
68
+ # failure: a read-only store, a damaged install missing templates/, or any
69
+ # other write failure used to sit outside every rescue, so the CLI could
70
+ # exit 1 with a raw Ruby backtrace on stderr and zero savepoint lines, even
71
+ # after the git commit itself had already landed. `main`'s own body is
72
+ # wrapped so nothing here can turn into a crash; the ledger write is a
73
+ # second, inner rescue so a lost savepoint line never also swallows the
74
+ # result the caller already needs on stdout.
75
+ def main(argv)
76
+ opts = parse_args(argv)
77
+ usage_abort("--cwd is required") if opts[:cwd].nil?
78
+ usage_abort("--summary is required") if opts[:summary].nil?
79
+
80
+ plastic_home = expand(opts[:plastic_home] || ENV.fetch("PLASTIC_HOME", "~/.plastic"))
81
+ store = opts[:store] ? expand(opts[:store]) : File.join(plastic_home, "store")
82
+ cwd = expand(opts[:cwd])
83
+
84
+ day = opts[:day] || SessionLedger.day_id
85
+ session = SessionLedger.short_session_id(opts[:session], ENV["CLAUDE_CODE_SESSION_ID"])
86
+ project = SessionLedger.project_slug(cwd, plastic_home: plastic_home)
87
+
88
+ begin
89
+ templates = default_templates_dir
90
+ SessionLedger.open_day(store: store, day: day, templates: templates, author: session) if Dir.exist?(templates)
91
+ rescue StandardError => e
92
+ warn "session-commit: could not open the day ledger: #{e.message}"
93
+ end
94
+
95
+ result = SessionGit.commit!(cwd: cwd, summary: opts[:summary], day: day, session: session,
96
+ plastic_home: plastic_home, store: store)
97
+
98
+ begin
99
+ # Does not depend on open_day having run: a damaged install with no
100
+ # templates/, or a store error above, must not also block the append.
101
+ savepoint_path = SessionLedger.savepoint_path(store, day)
102
+ FileUtils.mkdir_p(File.dirname(savepoint_path))
103
+ line = SessionLedger.savepoint_line(result.event, session, project, result.message, now: Time.now)
104
+ SessionLedger.append_line(savepoint_path, line, header: nil)
105
+ rescue StandardError => e
106
+ warn "session-commit: could not write the savepoint line: #{e.message}"
107
+ end
108
+
109
+ puts result.message
110
+ exit 0
111
+ rescue StandardError => e
112
+ warn "session-commit: #{e.message}"
113
+ exit 0
114
+ end
115
+
116
+ main(ARGV) if $PROGRAM_NAME == __FILE__
@@ -22,8 +22,9 @@
22
22
  #
23
23
  # Exit codes: 0 (preamble emitted), 2 (usage).
24
24
 
25
- require_relative "lib/bridge"
25
+ require_relative "lib/savepoint"
26
26
  require_relative "lib/worktree"
27
+ require_relative "lib/intent_screen"
27
28
 
28
29
  # Verbatim honoring instruction. Kept as one constant so the contract doc and the
29
30
  # test assert against the exact same string.
@@ -56,7 +57,7 @@ REPORT_CONTRACT =
56
57
  "insights field carrying 0..N durable nuggets (what I discovered worth keeping, " \
57
58
  "the most interesting residue of this turn; none if there were none); plus a " \
58
59
  "role-specific payload that fulfils your place in the What, Why, How, Exec cycle " \
59
- "(for example the planner explains the plan back to the orchestrator). Populate " \
60
+ "(for example the executor reports what was built and the test result). Populate " \
60
61
  "the insights field even when you cannot write the intent file yourself: a " \
61
62
  "background or dispatched agent carries each nugget home in the report and the " \
62
63
  "orchestrator persists it via scripts/insight-append, so an insight never " \
@@ -90,7 +91,7 @@ end
90
91
  # id/intent we report match what the rest of Plastic sees. Returns a Hash (possibly
91
92
  # empty) — never raises.
92
93
  def frontmatter_for(intent_dir)
93
- ifile = Bridge.intent_file(intent_dir)
94
+ ifile = Savepoint.intent_file(intent_dir)
94
95
  return {} unless File.exist?(ifile)
95
96
 
96
97
  content = File.read(ifile)
@@ -115,13 +116,19 @@ STAGE_LABELS = {
115
116
  "exec" => "Exec", "done" => "Done"
116
117
  }.freeze
117
118
 
119
+ # Intent 317, D6: a dispatched agent's "Current stage:" line must name the last
120
+ # LIFECYCLE line (What/Why/How/Exec/Done), never a trailing Lock/Review/Commit
121
+ # line - the bug spawn-preamble shared with IntentScreen.savepoint_fields
122
+ # before this guard (plan review finding B1).
118
123
  def current_stage(intent_dir)
119
- ledger = File.join(intent_dir, Bridge::SAVEPOINT_FILE)
124
+ ledger = File.join(intent_dir, Savepoint::SAVEPOINT_FILE)
120
125
  if File.exist?(ledger)
121
- last = File.read(ledger).each_line.map(&:strip).reject(&:empty?).last
122
- return last if last
126
+ lines = File.read(ledger).each_line.map(&:strip).reject(&:empty?)
127
+ lifecycle = lines.select { |l| IntentScreen.lifecycle_line?(l) }
128
+ return lifecycle.last if lifecycle.any?
129
+ return lines.last if lines.any?
123
130
  end
124
- STAGE_LABELS.fetch(Bridge.derive_stage(intent_dir), Bridge.derive_stage(intent_dir))
131
+ STAGE_LABELS.fetch(Savepoint.derive_stage(intent_dir), Savepoint.derive_stage(intent_dir))
125
132
  end
126
133
 
127
134
  intent_dir_arg, role, step = parse_args(ARGV)
package/scripts/update.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  # frozen_string_literal: true
4
4
 
5
5
  # Plastic — `update` verb. Runs via `npx @zalom/plastic update` (bin/plastic.js) or directly.
6
- # Usage: ruby scripts/update.rb [--beta|--latest|--alpha] [--claude|--codex|--hermes|--all] [--help]
6
+ # Usage: ruby scripts/update.rb [--beta|--latest|--alpha] [--yes] [--claude|--codex|--hermes|--all] [--help]
7
7
  #
8
8
  # Forward-only version transitions, sourced from npm dist-tags:
9
9
  # - no flag = next version on the CURRENT channel (derived from VERSION)
@@ -62,7 +62,7 @@ class Update < InstallerCore
62
62
  warn "No published version on the #{requested} channel."
63
63
  return 1
64
64
  when :ok
65
- if res[:kind] == :cross_bleeding && !confirm_bleeding(iv, res[:target])
65
+ if res[:kind] == :cross_bleeding && !confirm_bleeding(iv, res[:target], argv)
66
66
  puts "Aborted."
67
67
  return 1
68
68
  end
@@ -222,7 +222,10 @@ class Update < InstallerCore
222
222
  nil
223
223
  end
224
224
 
225
- def confirm_bleeding(current, target)
225
+ # --yes (intent 310) answers the bleeding-edge question without a tty, so a script or an
226
+ # agent can run `update --alpha --yes`; without it a non-tty run aborts as before.
227
+ def confirm_bleeding(current, target, argv = [])
228
+ return true if argv.include?("--yes")
226
229
  return false unless $stdin.tty?
227
230
  print "Switch from #{current} to the less-stable #{target}? [y/N]: "
228
231
  ($stdin.gets&.strip || "").downcase.start_with?("y")
@@ -248,6 +251,7 @@ class Update < InstallerCore
248
251
  --latest Switch to / advance the stable channel
249
252
  --beta Switch to / advance the beta channel
250
253
  --alpha Switch to / advance the alpha channel (bleeding edge — confirmed)
254
+ --yes Confirm a switch toward bleeding edge without a prompt (scripts, agents)
251
255
 
252
256
  Agent options (default: --claude):
253
257
  --claude --codex --hermes --all
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+
5
+ # write-handoff (intent 311): render and write one session's hand-off into
6
+ # the day directory (.sessions/<day>/handoff--<session>.md). Derived from the
7
+ # day ledger, so it can be regenerated at any time. Prints the path.
8
+ #
9
+ # Usage:
10
+ # write-handoff --trigger tick|precompact|close [--store <dir>] [--day <YYYYMMDD>]
11
+ # [--session <id>] [--templates <dir>]
12
+ #
13
+ # Defaults match append-ledger: the store is $PLASTIC_HOME/store, the day is
14
+ # today (local wall clock), the session is $CLAUDE_CODE_SESSION_ID, and the
15
+ # templates dir is the one shipped next to this script.
16
+ #
17
+ # Exit codes: 0 done; 2 usage error.
18
+
19
+ require_relative "lib/session_ledger"
20
+ require_relative "lib/handoff"
21
+
22
+ def usage_abort(message)
23
+ warn "write-handoff: #{message}"
24
+ exit 2
25
+ end
26
+
27
+ def expand(path)
28
+ File.expand_path(path.to_s.sub(/\A~/, Dir.home))
29
+ end
30
+
31
+ def parse_args(argv)
32
+ opts = {}
33
+ i = 0
34
+ while i < argv.length
35
+ flag = argv[i]
36
+ usage_abort("unknown argument #{flag.inspect}") unless flag.start_with?("--")
37
+ usage_abort("#{flag} requires a value") if i + 1 >= argv.length
38
+ key = flag.delete_prefix("--").to_sym
39
+ usage_abort("unknown flag #{flag.inspect}") unless %i[store day session trigger templates].include?(key)
40
+ opts[key] = argv[i + 1]
41
+ i += 2
42
+ end
43
+ opts
44
+ end
45
+
46
+ opts = parse_args(ARGV)
47
+ usage_abort("--trigger is required (#{Handoff::TRIGGERS.join(', ')})") unless opts[:trigger]
48
+ usage_abort("--trigger must be one of #{Handoff::TRIGGERS.join(', ')}") unless Handoff::TRIGGERS.include?(opts[:trigger])
49
+
50
+ plastic_home = expand(ENV.fetch("PLASTIC_HOME", "~/.plastic"))
51
+ store = opts[:store] ? expand(opts[:store]) : File.join(plastic_home, "store")
52
+ templates = opts[:templates] ? expand(opts[:templates]) : File.expand_path("../templates", __dir__)
53
+ usage_abort("templates dir not found: #{templates}") unless Dir.exist?(templates)
54
+
55
+ day = opts[:day] || SessionLedger.day_id
56
+ usage_abort("--day must be eight digits parsing as a real date, got #{day.inspect}") unless SessionLedger.valid_day_id?(day)
57
+ session = SessionLedger.short_session_id(opts[:session], ENV["CLAUDE_CODE_SESSION_ID"])
58
+
59
+ puts Handoff.write(store: store, day: day, session: session, trigger: opts[:trigger], templates: templates)
60
+ exit 0
@@ -33,7 +33,7 @@ when the ordering decides where you spend the next day.
33
33
  Never buy a consultation for: anything a tool can answer (search, reading code, running
34
34
  tests, documentation), writing code at volume, confirming a decision you already made,
35
35
  style or naming a linter would settle, or anything reversible and cheap you have not
36
- tried first. The full buy/never-buy list, the tier table, and the entry test live in
36
+ tried first. The full buy/never-buy list, the effort table, and the entry test live in
37
37
  `references/advisor-protocol.md`; read it before writing a brief for the first time in
38
38
  a session.
39
39
 
@@ -50,7 +50,7 @@ a session.
50
50
  advisor is disabled and point at "Setting the default" below.
51
51
  4. Dispatch the resolved agent with a brief built per `references/advisor-protocol.md`
52
52
  section 4 (natural prose, the block is a completeness check, not a form to fill).
53
- State TIER (S, M, or L) and EFFORT explicitly; classify low and prove your way up,
53
+ State EFFORT and the answer shape explicitly; classify low and prove your way up,
54
54
  never open high "to be safe".
55
55
  5. Consume the answer per the protocol's section 5: run the Operating Manual's
56
56
  five-question self-test on the advisor's plan before executing it. Advice is input,
@@ -86,7 +86,7 @@ the new default back to the user in one line.
86
86
  ## References
87
87
 
88
88
  - `references/advisor-protocol.md`: the full shipped Advisor Protocol (what to buy,
89
- tiers and effort, the entry test, how to write a brief that earns its cost, the
89
+ effort and answer shape, the entry test, how to write a brief that earns its cost, the
90
90
  answer contract, session economics, anti-patterns). Read it before the first
91
91
  consultation in a session; the second consultation in the same advisor thread costs a
92
92
  fraction of the first, so keep follow-ups on one thread rather than opening a new one.