@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.
- package/PLASTIC-reference.md +133 -0
- package/PLASTIC.md +398 -82
- package/README.md +98 -82
- package/agents/plastic-brainstorming.md +12 -9
- package/agents/plastic-enforcer.md +52 -12
- package/agents/plastic-executor.md +10 -10
- package/agents/plastic-future-intent-researcher.md +4 -8
- package/agents/plastic-intent-curator.md +12 -14
- package/agents/plastic-intent-discovery.md +35 -0
- package/agents/plastic-planner.md +15 -10
- package/agents/plastic-spec-specialist.md +15 -10
- package/bin/plastic.js +7 -3
- package/hooks/code-gate +17 -2
- package/hooks/hooks.json +28 -3
- package/hooks/lock-gate +21 -0
- package/hooks/retrieval-gate +10 -0
- package/hooks/savepoint-pre +10 -0
- package/hooks/statusline +48 -6
- package/package.json +2 -1
- package/scripts/agent-report +21 -0
- package/scripts/dashboard.rb +98 -23
- package/scripts/doctor.rb +232 -5
- package/scripts/hook-auto-arm +1 -1
- package/scripts/hook-bash-gate +22 -4
- package/scripts/hook-code-gate +30 -7
- package/scripts/hook-continue +17 -0
- package/scripts/hook-create-gate +45 -9
- package/scripts/hook-gate-check +24 -23
- package/scripts/hook-lock-gate +83 -0
- package/scripts/hook-retrieval-gate +148 -0
- package/scripts/hook-savepoint-pre +32 -0
- package/scripts/hook-session-start +1 -1
- package/scripts/insight-append +51 -0
- package/scripts/install.rb +44 -7
- package/scripts/lib/agent_models.rb +43 -0
- package/scripts/lib/bridge.rb +924 -84
- package/scripts/lib/dashboard_banner.rb +42 -0
- package/scripts/lib/hook_registry.rb +95 -0
- package/scripts/lib/insights.rb +86 -0
- package/scripts/lib/installer_core.rb +101 -60
- package/scripts/lib/link_suggestions.rb +319 -0
- package/scripts/lib/lock.rb +375 -0
- package/scripts/lib/power_tools.rb +22 -21
- package/scripts/lib/preflight.rb +79 -0
- package/scripts/lib/qmd_sync.rb +48 -6
- package/scripts/lib/retrieval_gate.rb +211 -0
- package/scripts/lib/worktree.rb +384 -0
- package/scripts/link-suggest +213 -0
- package/scripts/new-intent +58 -17
- package/scripts/plastic-lock +164 -0
- package/scripts/read-config +4 -0
- package/scripts/spawn-preamble +36 -4
- package/scripts/update.rb +16 -7
- package/skills/auto/SKILL.md +127 -20
- package/skills/auto/references/agent-architecture.md +7 -4
- package/skills/auto/references/agent-report-contract.md +36 -1
- package/skills/auto/references/end-tail.md +56 -0
- package/skills/auto/references/human-report-contract.md +55 -0
- package/skills/auto/references/tiers.md +77 -0
- package/skills/brainstorming/SKILL.md +7 -34
- package/skills/brainstorming/references/design-principles.md +49 -0
- package/skills/continuing/SKILL.md +26 -7
- package/skills/creating-intent/SKILL.md +13 -28
- package/skills/creating-project/SKILL.md +11 -74
- package/skills/creating-project/references/project-scaffolding.md +97 -0
- package/skills/creating-skills/SKILL.md +65 -0
- package/skills/creating-skills/evals/evals.json +108 -0
- package/skills/creating-skills/references/agents.md +168 -0
- package/skills/creating-skills/references/evals.md +41 -0
- package/skills/creating-skills/references/hooks.md +248 -0
- package/skills/creating-skills/references/progressive-disclosure.md +176 -0
- package/skills/creating-skills/references/scripts.md +166 -0
- package/skills/creating-skills/references/skills.md +165 -0
- package/skills/creating-skills/scripts/scaffold.rb +313 -0
- package/skills/dashboard/SKILL.md +13 -11
- package/skills/dashboard/references/classification.md +22 -0
- package/skills/doctor/SKILL.md +10 -7
- package/skills/executing-plan/SKILL.md +4 -4
- package/skills/humanizer/SKILL.md +39 -0
- package/skills/humanizer/evals/evals.json +70 -0
- package/skills/humanizer/references/always-on-snippet.md +9 -0
- package/skills/humanizer/references/examples.md +48 -0
- package/skills/install/SKILL.md +75 -84
- package/skills/intent-curator/SKILL.md +5 -1
- package/skills/intent-discovery/SKILL.md +47 -0
- package/skills/intent-starting/SKILL.md +130 -0
- package/skills/intent-starting/evals/evals.json +117 -0
- package/skills/intent-starting/references/boarding-matrix.md +35 -0
- package/skills/linking-intents/SKILL.md +48 -12
- package/skills/lock/SKILL.md +41 -0
- package/skills/managing-index/SKILL.md +6 -0
- package/skills/releasing/SKILL.md +44 -29
- package/skills/releasing/references/promotion-and-tagging.md +60 -0
- package/skills/roadmap/SKILL.md +51 -0
- package/skills/roadmap/references/file-format.md +91 -0
- package/skills/roadmap/references/operations.md +88 -0
- package/skills/uninstall/SKILL.md +29 -11
- package/skills/update/SKILL.md +34 -23
- package/skills/versions/SKILL.md +27 -12
- package/skills/writing-plans/SKILL.md +10 -88
- package/skills/writing-plans/references/plan-format.md +102 -0
- package/templates/config.yml +8 -0
- package/templates/outcome.md +3 -0
- package/templates/revisions.md +58 -0
- package/templates/roadmap.md +30 -0
- package/skills/writing-instructions/SKILL.md +0 -159
- package/skills/writing-instructions/references/agentskills-spec.md +0 -135
package/scripts/new-intent
CHANGED
|
@@ -73,15 +73,28 @@ def default_templates_dir
|
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
def render_tokens(text, tokens)
|
|
76
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
138
|
+
lines.insert(chain_idx + 1 + entry_lines.length, "#{prefix}#{quote}#{new_id}#{quote}\n")
|
|
139
|
+
end
|
|
105
140
|
|
|
106
|
-
|
|
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.
|
|
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
|
package/scripts/read-config
CHANGED
|
@@ -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
|
}
|
package/scripts/spawn-preamble
CHANGED
|
@@ -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,
|
|
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).
|
|
47
|
-
"
|
|
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
|
-
|
|
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
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
|
|
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.
|