@zalom/plastic 1.0.0-beta.9 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/PLASTIC-reference.md +133 -0
  2. package/PLASTIC.md +398 -82
  3. package/README.md +98 -82
  4. package/agents/plastic-brainstorming.md +12 -9
  5. package/agents/plastic-enforcer.md +52 -12
  6. package/agents/plastic-executor.md +10 -10
  7. package/agents/plastic-future-intent-researcher.md +4 -8
  8. package/agents/plastic-intent-curator.md +12 -14
  9. package/agents/plastic-intent-discovery.md +35 -0
  10. package/agents/plastic-planner.md +15 -10
  11. package/agents/plastic-spec-specialist.md +15 -10
  12. package/bin/plastic.js +7 -3
  13. package/hooks/code-gate +17 -2
  14. package/hooks/hooks.json +28 -3
  15. package/hooks/lock-gate +21 -0
  16. package/hooks/retrieval-gate +10 -0
  17. package/hooks/savepoint-pre +10 -0
  18. package/hooks/statusline +48 -6
  19. package/package.json +2 -1
  20. package/scripts/agent-report +21 -0
  21. package/scripts/dashboard.rb +98 -23
  22. package/scripts/doctor.rb +232 -5
  23. package/scripts/hook-auto-arm +1 -1
  24. package/scripts/hook-bash-gate +22 -4
  25. package/scripts/hook-code-gate +30 -7
  26. package/scripts/hook-continue +17 -0
  27. package/scripts/hook-create-gate +45 -9
  28. package/scripts/hook-gate-check +24 -23
  29. package/scripts/hook-lock-gate +83 -0
  30. package/scripts/hook-retrieval-gate +148 -0
  31. package/scripts/hook-savepoint-pre +32 -0
  32. package/scripts/hook-session-start +1 -1
  33. package/scripts/insight-append +51 -0
  34. package/scripts/install.rb +44 -7
  35. package/scripts/lib/agent_models.rb +43 -0
  36. package/scripts/lib/bridge.rb +924 -84
  37. package/scripts/lib/dashboard_banner.rb +42 -0
  38. package/scripts/lib/hook_registry.rb +95 -0
  39. package/scripts/lib/insights.rb +86 -0
  40. package/scripts/lib/installer_core.rb +101 -60
  41. package/scripts/lib/link_suggestions.rb +319 -0
  42. package/scripts/lib/lock.rb +375 -0
  43. package/scripts/lib/power_tools.rb +22 -21
  44. package/scripts/lib/preflight.rb +79 -0
  45. package/scripts/lib/qmd_sync.rb +48 -6
  46. package/scripts/lib/retrieval_gate.rb +211 -0
  47. package/scripts/lib/worktree.rb +384 -0
  48. package/scripts/link-suggest +213 -0
  49. package/scripts/new-intent +58 -17
  50. package/scripts/plastic-lock +164 -0
  51. package/scripts/read-config +4 -0
  52. package/scripts/spawn-preamble +36 -4
  53. package/scripts/update.rb +16 -7
  54. package/skills/auto/SKILL.md +127 -20
  55. package/skills/auto/references/agent-architecture.md +7 -4
  56. package/skills/auto/references/agent-report-contract.md +36 -1
  57. package/skills/auto/references/end-tail.md +56 -0
  58. package/skills/auto/references/human-report-contract.md +55 -0
  59. package/skills/auto/references/tiers.md +77 -0
  60. package/skills/brainstorming/SKILL.md +7 -34
  61. package/skills/brainstorming/references/design-principles.md +49 -0
  62. package/skills/continuing/SKILL.md +26 -7
  63. package/skills/creating-intent/SKILL.md +13 -28
  64. package/skills/creating-project/SKILL.md +11 -74
  65. package/skills/creating-project/references/project-scaffolding.md +97 -0
  66. package/skills/creating-skills/SKILL.md +65 -0
  67. package/skills/creating-skills/evals/evals.json +108 -0
  68. package/skills/creating-skills/references/agents.md +168 -0
  69. package/skills/creating-skills/references/evals.md +41 -0
  70. package/skills/creating-skills/references/hooks.md +248 -0
  71. package/skills/creating-skills/references/progressive-disclosure.md +176 -0
  72. package/skills/creating-skills/references/scripts.md +166 -0
  73. package/skills/creating-skills/references/skills.md +165 -0
  74. package/skills/creating-skills/scripts/scaffold.rb +313 -0
  75. package/skills/dashboard/SKILL.md +13 -11
  76. package/skills/dashboard/references/classification.md +22 -0
  77. package/skills/doctor/SKILL.md +10 -7
  78. package/skills/executing-plan/SKILL.md +4 -4
  79. package/skills/humanizer/SKILL.md +39 -0
  80. package/skills/humanizer/evals/evals.json +70 -0
  81. package/skills/humanizer/references/always-on-snippet.md +9 -0
  82. package/skills/humanizer/references/examples.md +48 -0
  83. package/skills/install/SKILL.md +75 -84
  84. package/skills/intent-curator/SKILL.md +5 -1
  85. package/skills/intent-discovery/SKILL.md +47 -0
  86. package/skills/intent-starting/SKILL.md +130 -0
  87. package/skills/intent-starting/evals/evals.json +117 -0
  88. package/skills/intent-starting/references/boarding-matrix.md +35 -0
  89. package/skills/linking-intents/SKILL.md +48 -12
  90. package/skills/lock/SKILL.md +41 -0
  91. package/skills/managing-index/SKILL.md +6 -0
  92. package/skills/releasing/SKILL.md +44 -29
  93. package/skills/releasing/references/promotion-and-tagging.md +60 -0
  94. package/skills/roadmap/SKILL.md +51 -0
  95. package/skills/roadmap/references/file-format.md +91 -0
  96. package/skills/roadmap/references/operations.md +88 -0
  97. package/skills/uninstall/SKILL.md +29 -11
  98. package/skills/update/SKILL.md +34 -23
  99. package/skills/versions/SKILL.md +27 -12
  100. package/skills/writing-plans/SKILL.md +10 -88
  101. package/skills/writing-plans/references/plan-format.md +102 -0
  102. package/templates/config.yml +8 -0
  103. package/templates/outcome.md +3 -0
  104. package/templates/revisions.md +58 -0
  105. package/templates/roadmap.md +30 -0
  106. package/skills/writing-instructions/SKILL.md +0 -159
  107. package/skills/writing-instructions/references/agentskills-spec.md +0 -135
@@ -73,15 +73,28 @@ def default_templates_dir
73
73
  end
74
74
 
75
75
  def render_tokens(text, tokens)
76
- tokens.reduce(text) { |acc, (k, v)| acc.gsub("{{#{k}}}", v.to_s) }
76
+ # Block-form gsub: a String replacement argument reinterprets backslash
77
+ # sequences (so a doubled backslash from escape_double_quoted_scalar
78
+ # collapses back to one); the block form substitutes the value literally.
79
+ tokens.reduce(text) { |acc, (k, v)| acc.gsub("{{#{k}}}") { v.to_s } }
80
+ end
81
+
82
+ # Escape the characters that would break a double-quoted YAML scalar: backslash
83
+ # first, so a real backslash in the input isn't re-escaped by the quote step,
84
+ # then the double quote itself. Used only for the frontmatter-bound INTENT
85
+ # token; the body's DESCRIPTION copy stays raw (markdown prose, not a YAML
86
+ # scalar) so escaping it would corrupt what the user actually typed.
87
+ def escape_double_quoted_scalar(value)
88
+ value.to_s.gsub("\\") { "\\\\" }.gsub('"') { '\"' }
77
89
  end
78
90
 
79
91
  # Add an id to a source intent's frontmatter `chain` array, idempotently (I1
80
92
  # reciprocity: `child in parent.sources` => `parent.chain` gains `child`). A
81
- # targeted edit of the `chain:` line only; the body (including `## Links`) is
93
+ # targeted edit that preserves every existing entry and appends the new id in
94
+ # the SAME on-disk YAML style (flow `chain: [...]` or block `chain:` + indented
95
+ # `- "id"` lines) the target already uses; the body (including `## Links`) is
82
96
  # preserved byte-for-byte and no other frontmatter key is touched, so the file
83
- # stays born-complete. Renders the array in flow style (`["a", "b"]`) to match
84
- # templates/intent.md. No-op when the id is already present.
97
+ # stays born-complete. No-op when the id is already present.
85
98
  def add_to_chain(file_path, new_id)
86
99
  return unless File.exist?(file_path)
87
100
  content = File.read(file_path)
@@ -91,21 +104,41 @@ def add_to_chain(file_path, new_id)
91
104
  return unless parts.length >= 3
92
105
 
93
106
  fm = parts[1]
94
- chain_line = fm.lines.find { |l| l.match?(/\A\s*chain\s*:/) }
95
- return unless chain_line
107
+ lines = fm.lines
108
+ chain_idx = lines.index { |l| l.match?(/\A\s*chain\s*:/) }
109
+ return unless chain_idx
110
+
111
+ flow_match = lines[chain_idx].match(/\A\s*chain\s*:\s*\[(.*?)\]\s*\z/m)
112
+
113
+ if flow_match
114
+ ids = flow_match[1].scan(/"([^"]*)"|'([^']*)'/).flatten.compact
115
+ return if ids.include?(new_id)
116
+
117
+ ids << new_id
118
+ lines[chain_idx] = "chain: [#{ids.map { |i| "\"#{i}\"" }.join(", ")}]\n"
119
+ else
120
+ entry_re = /\A(\s*-\s*)(?:"([^"]*)"|'([^']*)'|(\S+))\s*\z/
121
+ entry_lines = []
122
+ j = chain_idx + 1
123
+ while j < lines.length && lines[j].match?(entry_re)
124
+ entry_lines << lines[j]
125
+ j += 1
126
+ end
127
+
128
+ ids = entry_lines.map { |l| (m = l.match(entry_re)) && (m[2] || m[3] || m[4]) }
129
+ return if ids.include?(new_id)
96
130
 
97
- existing = fm.match(/\bchain\s*:\s*\[(.*?)\]/m)
98
- ids =
99
- if existing
100
- existing[1].scan(/"([^"]*)"|'([^']*)'/).flatten.compact
131
+ if entry_lines.empty?
132
+ prefix, quote = " - ", "\""
101
133
  else
102
- []
134
+ m = entry_lines.last.match(entry_re)
135
+ prefix = m[1]
136
+ quote = m[2] ? "\"" : (m[3] ? "'" : "")
103
137
  end
104
- return if ids.include?(new_id)
138
+ lines.insert(chain_idx + 1 + entry_lines.length, "#{prefix}#{quote}#{new_id}#{quote}\n")
139
+ end
105
140
 
106
- ids << new_id
107
- rendered = "chain: [#{ids.map { |i| "\"#{i}\"" }.join(", ")}]"
108
- new_fm = fm.sub(/^\s*chain\s*:.*$/, rendered)
141
+ new_fm = lines.join
109
142
  File.write(file_path, ["", new_fm, parts[2]].join("---"))
110
143
  end
111
144
 
@@ -260,7 +293,7 @@ def main(argv)
260
293
  intent_template = File.read(File.join(templates, "intent.md"))
261
294
  intent_body = render_tokens(intent_template, {
262
295
  "ID" => id,
263
- "INTENT" => opts[:intent],
296
+ "INTENT" => escape_double_quoted_scalar(opts[:intent]),
264
297
  "SOURCES" => sources_str,
265
298
  "DATE" => Date.today.iso8601,
266
299
  "AUTHOR" => opts[:author],
@@ -311,7 +344,15 @@ def main(argv)
311
344
  File.write(File.join(intent_dir, name), "#{Bridge::PLACEHOLDER_SENTINEL}\n#{body}")
312
345
  end
313
346
 
314
- # 6. Self-validate (frontmatter + sanctioned sections).
347
+ # 6. Stamp the born savepoint line (intent 81). The first ledger line is the
348
+ # `What` bookend, written deterministically at creation rather than relying on
349
+ # a PostToolUse gate firing on the intent-file write (which is missed in some
350
+ # sessions / harnesses). The intent file is never a sentinel placeholder, so
351
+ # append_savepoint records `What {id}--{slug}.md`. Idempotent: a later gate
352
+ # fire adds nothing.
353
+ Bridge.append_savepoint(intent_dir, intent_file)
354
+
355
+ # 7. Self-validate (frontmatter + sanctioned sections).
315
356
  result = IntentValidator.validate(intent_dir)
316
357
  unless result[:ok]
317
358
  warn "new-intent: scaffolded intent is NOT born complete:"
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
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).
7
+ #
8
+ # Usage: plastic-lock <status|fix|release|reclaim|delegate|claim|release-claim>
9
+ # [--intent-dir DIR] [--session SID] [--delegate SID] [--artifact NAME]
10
+ #
11
+ # Verbs:
12
+ # status report the lock file, the bridge cache, their agreement,
13
+ # and any live per-artifact claims
14
+ # fix idempotent repair: rebuild lock + bridge from disk truth for
15
+ # the current session; never touches a fresh foreign lock
16
+ # release owner clears the lock (End tail / abandoning a boarding)
17
+ # reclaim explicit takeover of a stale lock, audited in savepoint.md
18
+ # delegate owner registers a subagent session under the lock (D4)
19
+ # claim acquire the named --artifact's claim token (intent 111);
20
+ # rejected (exit 1) while a fresh claim is held by anyone,
21
+ # including this same session; takes over a stale claim
22
+ # release-claim free the named --artifact's claim token
23
+ #
24
+ # Without --intent-dir the intent is resolved from this session's bridge.
25
+ # Exit 0 on success/report; exit 1 when the verb is blocked (held elsewhere).
26
+
27
+ require "json"
28
+ require_relative "lib/bridge"
29
+ require_relative "lib/lock"
30
+
31
+ def usage!
32
+ warn "usage: plastic-lock <status|fix|release|reclaim|delegate|claim|release-claim> " \
33
+ "[--intent-dir DIR] [--session SID] [--delegate SID] [--artifact NAME]"
34
+ exit 1
35
+ end
36
+
37
+ verb = ARGV.shift
38
+ usage! unless %w[status fix release reclaim delegate claim release-claim].include?(verb)
39
+
40
+ opts = {}
41
+ until ARGV.empty?
42
+ case (flag = ARGV.shift)
43
+ when "--intent-dir" then opts[:dir] = ARGV.shift
44
+ when "--session" then opts[:session] = ARGV.shift
45
+ when "--delegate" then opts[:delegate] = ARGV.shift
46
+ when "--artifact" then opts[:artifact] = ARGV.shift
47
+ else
48
+ warn "unknown flag #{flag}"
49
+ usage!
50
+ end
51
+ end
52
+
53
+ session = opts[:session]
54
+ session = ENV["CLAUDE_CODE_SESSION_ID"] if session.nil? || session.strip.empty?
55
+
56
+ dir = opts[:dir]
57
+ if dir.nil? || dir.strip.empty?
58
+ bridge = Bridge.discover_bridge(session: session, cwd: Dir.pwd)
59
+ dir = Bridge.bridge_intent_dir(bridge)
60
+ end
61
+ if dir.nil?
62
+ warn "plastic-lock: no intent resolved; pass --intent-dir <intent dir>"
63
+ exit 1
64
+ end
65
+ dir = File.expand_path(dir)
66
+
67
+ intent_id = Bridge.intent_id_from_dir(dir)
68
+ store = File.dirname(dir)
69
+ name = File.basename(dir)
70
+ key = Bridge.resolve_session(session, intent_id: intent_id, store: store)
71
+
72
+ case verb
73
+ when "status"
74
+ lock = Lock.read(dir)
75
+ bridge = Bridge.read(key, intent_id: intent_id)
76
+ report = {
77
+ "intent_dir" => dir,
78
+ "session" => key,
79
+ "lock" => lock,
80
+ "lock_fresh" => Lock.fresh?(dir),
81
+ "lock_corrupt" => Lock.corrupt?(dir),
82
+ "bridge_present" => !bridge.nil?,
83
+ "agreement" => (lock && bridge) ?
84
+ (lock["owner_session"] == bridge.dig("lock", "owner_session")) : nil,
85
+ "claims" => Claim.claims_status(dir),
86
+ }
87
+ puts JSON.pretty_generate(report)
88
+ when "fix"
89
+ report = Bridge.repair_lock(key, intent_id: intent_id, intent_dir: dir,
90
+ store: store, name: name)
91
+ puts JSON.pretty_generate(report)
92
+ unless report["status"] == "repaired"
93
+ warn "plastic-lock: #{report['status']} by #{report['owner']}" \
94
+ "#{report['hint'] ? " (#{report['hint']})" : ''}"
95
+ exit 1
96
+ end
97
+ when "release"
98
+ result = Lock.release(dir, session: key)
99
+ if result == :not_owner
100
+ warn "plastic-lock: not the owner; run plastic-lock status"
101
+ exit 1
102
+ end
103
+ data = Bridge.read(key, intent_id: intent_id)
104
+ if data
105
+ data["lock"] = { "owner_session" => nil, "acquired_at" => nil,
106
+ "host" => nil, "type" => nil, "delegates" => [] }
107
+ Bridge.write(key, data)
108
+ end
109
+ puts "released (#{result})"
110
+ when "reclaim"
111
+ status, lock_data = Lock.takeover(dir, session: key)
112
+ if status == :fresh
113
+ warn "plastic-lock: lock is FRESH and held by #{lock_data['owner_session']}; " \
114
+ "back off (no silent reclaim)"
115
+ exit 1
116
+ end
117
+ report = Bridge.repair_lock(key, intent_id: intent_id, intent_dir: dir,
118
+ store: store, name: name)
119
+ puts JSON.pretty_generate(report)
120
+ when "delegate"
121
+ usage! if opts[:delegate].nil?
122
+ ok = Lock.add_delegate(dir, delegate: opts[:delegate], session: key)
123
+ unless ok
124
+ warn "plastic-lock: only the lock owner may delegate; run plastic-lock status"
125
+ exit 1
126
+ end
127
+ puts "delegated #{opts[:delegate]} under #{key}"
128
+ when "claim"
129
+ usage! if opts[:artifact].nil?
130
+ status, data = Claim.acquire_claim(dir, opts[:artifact], session: key,
131
+ delegate: opts[:delegate])
132
+ case status
133
+ when :acquired
134
+ puts "claimed #{opts[:artifact]} under #{key}"
135
+ when :held
136
+ warn "plastic-lock: #{opts[:artifact]} is claimed by " \
137
+ "#{data['owner_session']} since #{data['acquired_at']}; back off"
138
+ exit 1
139
+ when :stale
140
+ old = data && data["owner_session"]
141
+ Claim.release_claim(dir, opts[:artifact], session: key, force: true)
142
+ status2, = Claim.acquire_claim(dir, opts[:artifact], session: key,
143
+ delegate: opts[:delegate])
144
+ if status2 == :acquired
145
+ warn "plastic-lock: took over stale claim on #{opts[:artifact]} from #{old}"
146
+ puts "claimed #{opts[:artifact]} under #{key}"
147
+ else
148
+ warn "plastic-lock: could not take over #{opts[:artifact]} (#{status2})"
149
+ exit 1
150
+ end
151
+ when :corrupt
152
+ Claim.release_claim(dir, opts[:artifact], session: key, force: true)
153
+ Claim.acquire_claim(dir, opts[:artifact], session: key, delegate: opts[:delegate])
154
+ puts "claimed #{opts[:artifact]} under #{key} (repaired corrupt claim)"
155
+ end
156
+ when "release-claim"
157
+ usage! if opts[:artifact].nil?
158
+ result = Claim.release_claim(dir, opts[:artifact], session: key)
159
+ if result == :not_owner
160
+ warn "plastic-lock: not the claim owner; run plastic-lock status"
161
+ exit 1
162
+ end
163
+ puts "released-claim #{opts[:artifact]} (#{result})"
164
+ end
@@ -9,6 +9,7 @@
9
9
 
10
10
  require "yaml"
11
11
  require "json"
12
+ require_relative "lib/agent_models"
12
13
 
13
14
  DEFAULTS = {
14
15
  "version" => 3,
@@ -23,6 +24,9 @@ DEFAULTS = {
23
24
  "type" => "claude-code",
24
25
  "parallel_mode" => "linear"
25
26
  },
27
+ "agents" => {
28
+ "models" => AgentModels::TIER_DEFAULTS
29
+ },
26
30
  "architect" => {
27
31
  "style" => nil
28
32
  }
@@ -23,6 +23,7 @@
23
23
  # Exit codes: 0 (preamble emitted), 2 (usage).
24
24
 
25
25
  require_relative "lib/bridge"
26
+ require_relative "lib/worktree"
26
27
 
27
28
  # Verbatim honoring instruction. Kept as one constant so the contract doc and the
28
29
  # test assert against the exact same string.
@@ -31,6 +32,16 @@ HONOR_INSTRUCTION =
31
32
  "Emit VALID lifecycle artifacts; do not hallucinate intents or stages. " \
32
33
  "Your primary output is valid lifecycle artifacts; you close with a structured report about them."
33
34
 
35
+ # Verbatim cd-fallback instruction (intent 152). The harness's own EnterWorktree
36
+ # tool assumes cwd IS the repo root, which fails when a dispatched agent's
37
+ # session launches from a directory that is not itself a git repo (the normal
38
+ # shape for a background job): it cannot discover the nested project repo
39
+ # underneath it. Kept as one constant so the doc and the test assert against
40
+ # the exact same string.
41
+ WORKTREE_FALLBACK_INSTRUCTION =
42
+ "If EnterWorktree fails to discover the nested repo from a non-repo launch " \
43
+ "directory, cd directly to the code worktree path above before editing project code."
44
+
34
45
  # Verbatim completion-report contract (intent 74). Kept as one constant so the
35
46
  # contract doc (skills/auto/references/agent-report-contract.md), the role prompts,
36
47
  # and the test assert against the exact same string. SINGLE SOURCE OF TRUTH for the
@@ -41,10 +52,18 @@ REPORT_CONTRACT =
41
52
  "FINAL MESSAGE (your return value), not a side-channel file. Do not go idle or " \
42
53
  "finish silently. The report carries a common envelope: role, intent id, stage, " \
43
54
  "status (delivered or blocked), artifacts written, verification or tests run, " \
44
- "checklist deltas, deviations from spec, and blockers or handoff notes; plus a " \
55
+ "checklist deltas, deviations from spec, blockers or handoff notes, and an " \
56
+ "insights field carrying 0..N durable nuggets (what I discovered worth keeping, " \
57
+ "the most interesting residue of this turn; none if there were none); plus a " \
45
58
  "role-specific payload that fulfils your place in the What, Why, How, Exec cycle " \
46
- "(for example the planner explains the plan back to the orchestrator). See " \
47
- "skills/auto/references/agent-report-contract.md for the per-role format."
59
+ "(for example the planner explains the plan back to the orchestrator). Populate " \
60
+ "the insights field even when you cannot write the intent file yourself: a " \
61
+ "background or dispatched agent carries each nugget home in the report and the " \
62
+ "orchestrator persists it via scripts/insight-append, so an insight never " \
63
+ "depends on the discovering session's file-write access. Keep the report " \
64
+ "prose-stripped: the envelope and payload only, no greeting, no preamble, " \
65
+ "no end-recap, no restating of the task; reasoning stays in the thinking channel. " \
66
+ "See skills/auto/references/agent-report-contract.md for the per-role format."
48
67
 
49
68
  def parse_args(argv)
50
69
  role = nil
@@ -115,7 +134,8 @@ end
115
134
  intent_dir = File.expand_path(intent_dir_arg)
116
135
 
117
136
  fm = frontmatter_for(intent_dir)
118
- id = fm["id"].to_s.strip
137
+ raw_id = fm["id"].to_s.strip
138
+ id = raw_id
119
139
  intent_name = fm["intent"].to_s.strip
120
140
  id = "(unknown)" if id.empty?
121
141
  intent_name = "(unknown)" if intent_name.empty?
@@ -123,12 +143,24 @@ intent_name = "(unknown)" if intent_name.empty?
123
143
  stage = current_stage(intent_dir)
124
144
  cycle = role || step || stage
125
145
 
146
+ # Already-provisioned code worktree path (intent 152), resolved purely from
147
+ # filesystem/YAML state (no git shell-out): see Worktree.paths's own doc
148
+ # comment. Fails open (no lines added) when unresolvable or not on disk.
149
+ store_root = File.dirname(intent_dir)
150
+ project_slug = Worktree.slug_for_store(store_root)
151
+ intent_slug = Worktree.slug_from_dir(intent_dir)
152
+ code_path = Worktree.paths(slug: project_slug, intent_id: raw_id, intent_slug: intent_slug)["code"]
153
+
126
154
  lines = []
127
155
  lines << "=== Plastic spawn preamble ==="
128
156
  lines << "Store: #{intent_dir}"
129
157
  lines << "Active intent: #{id} - #{intent_name}"
130
158
  lines << "Current stage: #{stage}"
131
159
  lines << "Cycle step / role: #{cycle}"
160
+ if code_path && Dir.exist?(code_path)
161
+ lines << "Code worktree: #{code_path}"
162
+ lines << WORKTREE_FALLBACK_INSTRUCTION
163
+ end
132
164
  lines << ""
133
165
  lines << HONOR_INSTRUCTION
134
166
  lines << ""
package/scripts/update.rb CHANGED
@@ -53,18 +53,22 @@ class Update < InstallerCore
53
53
  end
54
54
  puts "\u{2b06}\u{fe0f} Updating Plastic #{iv} \u{2192} #{res[:target]}"
55
55
  exit_code = perform_switch(res[:target], agent_args(argv))
56
- run_post_update_doctor if exit_code == 0
56
+ run_post_update_doctor(full: argv.include?("--full-doctor")) if exit_code == 0
57
57
  exit_code
58
58
  end
59
59
  end
60
60
 
61
- # Run the full doctor after a successful update and print a human-readable
62
- # summary. Informational only: does not raise and does not affect the update's
63
- # exit code. Accepts injected `doctor` and `out` for hermetic unit tests.
64
- def run_post_update_doctor(doctor: nil, out: $stdout)
61
+ # Run doctor after a successful update and print a human-readable summary.
62
+ # Defaults to the fast core tier (agent registration + core files + manifest
63
+ # sync, binary pass|fail, no store walk) so a newcomer's first post-update
64
+ # run is not buried in convention warns they cannot act on. `full: true`
65
+ # (via `--full-doctor`) runs the complete store walk instead. Informational
66
+ # only: does not raise and does not affect the update's exit code. Accepts
67
+ # injected `doctor` and `out` for hermetic unit tests.
68
+ def run_post_update_doctor(doctor: nil, out: $stdout, full: false)
65
69
  doctor ||= Doctor.new
66
- out.puts "\nRunning full doctor after update..."
67
- result = doctor.run_checks("claude")
70
+ out.puts full ? "\nRunning full doctor after update..." : "\nRunning core doctor after update..."
71
+ result = full ? doctor.run_checks("claude") : doctor.run_core_checks("claude")
68
72
  s = result[:summary]
69
73
  out.puts " Doctor status: #{result[:status]} " \
70
74
  "(pass: #{s[:pass]}, warn: #{s[:warn]}, fail: #{s[:fail]}, total: #{s[:total]})"
@@ -152,6 +156,11 @@ class Update < InstallerCore
152
156
  Agent options (default: --claude):
153
157
  --claude --codex --hermes --all
154
158
 
159
+ Post-update doctor:
160
+ By default, a successful update runs the fast core doctor sync (agent
161
+ registration, core files, manifest — binary pass|fail, no store walk).
162
+ --full-doctor Run the full doctor (complete store walk) after updating.
163
+
155
164
  Behaviour:
156
165
  No flag advances to the next version on your current channel. Switching toward a
157
166
  more stable channel is frictionless; switching toward bleeding edge is confirmed.