@zalom/plastic 1.0.0 → 1.0.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.
- package/PLASTIC.md +14 -5
- package/README.md +2 -3
- package/hooks/code-gate +7 -1
- package/package.json +1 -1
- package/scripts/hook-code-gate +19 -1
- package/scripts/lib/installer_core.rb +1 -0
- package/scripts/lib/qmd_sync.rb +33 -6
- package/scripts/new-intent +49 -16
- package/scripts/spawn-preamble +25 -1
- package/skills/auto/SKILL.md +1 -1
- package/skills/roadmap/references/operations.md +10 -0
package/PLASTIC.md
CHANGED
|
@@ -113,9 +113,9 @@ entries stay append-only, newest at the bottom; the prefix only stamps each line
|
|
|
113
113
|
which stage, and who.
|
|
114
114
|
|
|
115
115
|
The blessed write path is the `insight-append` helper
|
|
116
|
-
(`scripts/insight-append <intent_dir> <text> --stage S --author A`), which
|
|
117
|
-
validates it, and appends at the bottom. Hand-editing
|
|
118
|
-
helper is the default so the format cannot drift.
|
|
116
|
+
(`scripts/insight-append <intent_dir> <text> --stage S --author A`), which ships with every
|
|
117
|
+
install and update, formats the prefix, validates it, and appends at the bottom. Hand-editing
|
|
118
|
+
`## Insights` is an escape hatch; the helper is the default so the format cannot drift.
|
|
119
119
|
|
|
120
120
|
Background sessions and dispatched sub-agents do not write the insight themselves. They carry
|
|
121
121
|
each nugget home in the completion report's `insights:` field, and the orchestrator (or any
|
|
@@ -191,6 +191,13 @@ adapter layer that maps Plastic's hooks and model aliases onto each supported ag
|
|
|
191
191
|
(Claude, Codex, Hermes) is the cross-harness portability layer; see
|
|
192
192
|
docs/reference/harness-adapters.md for the adapter contract.
|
|
193
193
|
|
|
194
|
+
**Spawn preamble (intent 152).** `scripts/spawn-preamble` emits a live-state block purely from
|
|
195
|
+
filesystem state: the active intent, stage, role/cycle-step, the honor instruction, and the
|
|
196
|
+
report contract. When the intent's code worktree is resolvable and exists on disk, it also
|
|
197
|
+
appends the worktree's absolute path plus a verbatim instruction to `cd` there directly, for
|
|
198
|
+
harnesses whose `EnterWorktree` cannot discover a nested repo from a non-repo launch directory.
|
|
199
|
+
Output is byte-identical when no worktree resolves.
|
|
200
|
+
|
|
194
201
|
**Orchestrator advisory.** At auto-mode start, the orchestrator recommends once that the user
|
|
195
202
|
run the main session on the best available thinking model (Fable, Opus, or whatever supersedes
|
|
196
203
|
them). This is advisory only: it changes no behavior and blocks nothing if ignored, and it
|
|
@@ -343,7 +350,7 @@ ALL work flows through intents.
|
|
|
343
350
|
3. On completion, capture observations in `## Insights`.
|
|
344
351
|
4. When done, write `outcome.md` + `## Outcome` summary. Update INDEX.md.
|
|
345
352
|
5. Researches are intents. No separate folder.
|
|
346
|
-
6. Intents are created only via `plastic-creating-intent`. Never hand-author an intent file. The skill self-verifies the written intent with `scripts/validate-intent` before announcing or committing, so every intent is born complete.
|
|
353
|
+
6. Intents are created only via `plastic-creating-intent`. Never hand-author an intent file. The skill self-verifies the written intent with `scripts/validate-intent` before announcing or committing, so every intent is born complete. `--intent` text is escaped for double quotes and backslashes before it lands in frontmatter, so free-form text is safe to pass as-is, and a reciprocal `chain:` append preserves the target intent's existing flow- or block-style entries.
|
|
347
354
|
|
|
348
355
|
## House Style (self-check)
|
|
349
356
|
|
|
@@ -407,7 +414,9 @@ Each gate guards one thing. All are hard except the retrieval gate:
|
|
|
407
414
|
`>` redirect, a `ruby -e` or `python -c` write), so the same rules apply whether an edit goes
|
|
408
415
|
through the Write tool or a shell. A trailing `# plastic-ok` comment is an auditable escape that
|
|
409
416
|
lets a deliberate command through, and every use is logged to
|
|
410
|
-
`~/.plastic/.cache/gate-escapes.log`.
|
|
417
|
+
`~/.plastic/.cache/gate-escapes.log`. The code gate (Write and Edit) carries the identical
|
|
418
|
+
audited `# plastic-ok` escape, logged to the same file. The escape does not extend to
|
|
419
|
+
`NotebookEdit` or MCP structural edits: they are still gated, just without an escape hatch.
|
|
411
420
|
- **retrieval-gate** is advisory only (see the Retrieval Gate section): it hints at QMD and never
|
|
412
421
|
blocks a read or search.
|
|
413
422
|
|
package/README.md
CHANGED
|
@@ -102,14 +102,13 @@ Plastic delivers all work through successive stages ->
|
|
|
102
102
|
|
|
103
103
|
## Compatibility and ownership
|
|
104
104
|
|
|
105
|
-
- Native installers for Claude
|
|
105
|
+
- Native installers for Claude (Codex, Hermes, and and others coming soon).
|
|
106
106
|
- Plain Markdown plus Git. The work stays in files you own.
|
|
107
107
|
- Personal stores by default.
|
|
108
108
|
- Guided delivery with a human at every gate, or autonomous delivery when you
|
|
109
109
|
ask for it.
|
|
110
110
|
|
|
111
|
-
Plastic needs Ruby (already on macOS and Linux) and Node.js 18 or later.
|
|
112
|
-
`--claude` for `--codex`, `--hermes`, or `--all` to match your agent. Bun
|
|
111
|
+
Plastic needs Ruby (already on macOS and Linux) and Node.js 18 or later. Bun
|
|
113
112
|
users can run `bunx` in place of `npx`; Bun is never required.
|
|
114
113
|
|
|
115
114
|
## Built with Plastic
|
package/hooks/code-gate
CHANGED
|
@@ -17,5 +17,11 @@ fi
|
|
|
17
17
|
|
|
18
18
|
SESSION_ID=$(echo "$INPUT" | ruby -rjson -e 'data = JSON.parse(STDIN.read); puts data.dig("session_id") || ""' 2>/dev/null)
|
|
19
19
|
|
|
20
|
+
NEW_CONTENT=$(echo "$INPUT" | ruby -rjson -e '
|
|
21
|
+
data = JSON.parse(STDIN.read)
|
|
22
|
+
ti = data["tool_input"] || data["tool_params"] || {}
|
|
23
|
+
print ti["content"] || ti["new_string"] || ""
|
|
24
|
+
' 2>/dev/null)
|
|
25
|
+
|
|
20
26
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
21
|
-
ruby "$SCRIPT_DIR/../scripts/hook-code-gate" "$FILE_PATH" "$SESSION_ID"
|
|
27
|
+
ruby "$SCRIPT_DIR/../scripts/hook-code-gate" "$FILE_PATH" "$SESSION_ID" "$NEW_CONTENT"
|
package/package.json
CHANGED
package/scripts/hook-code-gate
CHANGED
|
@@ -20,8 +20,26 @@ exit 0 unless file_path && !file_path.empty?
|
|
|
20
20
|
|
|
21
21
|
session = (ARGV[1] unless ARGV[1].to_s.empty?) || ENV["CLAUDE_CODE_SESSION_ID"]
|
|
22
22
|
|
|
23
|
+
# Auditable escape (intent 150, mirrors the Bash gate's intent 108 D7): a trailing
|
|
24
|
+
# `# plastic-ok` in the new Write/Edit content allows the edit and logs it, so a
|
|
25
|
+
# deliberately sanctioned code edit no longer needs the Bash-funneling workaround.
|
|
26
|
+
new_content = ARGV[2]
|
|
27
|
+
if new_content && Bridge::PLASTIC_OK_RE.match?(new_content.chomp)
|
|
28
|
+
begin
|
|
29
|
+
require "fileutils"
|
|
30
|
+
log = File.join(Dir.home, ".plastic", ".cache", "gate-escapes.log")
|
|
31
|
+
FileUtils.mkdir_p(File.dirname(log))
|
|
32
|
+
File.open(log, "a") do |io|
|
|
33
|
+
io.puts("#{Time.now.utc.iso8601}\t#{session}\t#{file_path}")
|
|
34
|
+
end
|
|
35
|
+
rescue StandardError
|
|
36
|
+
# the escape still applies; logging is best-effort
|
|
37
|
+
end
|
|
38
|
+
exit 0
|
|
39
|
+
end
|
|
40
|
+
|
|
23
41
|
# --- Load bridge (shared resolution; stdin session_id -> CLAUDE_CODE_SESSION_ID -> /tmp scan) ---
|
|
24
|
-
bridge_data = Bridge.discover_bridge(session: session, cwd:
|
|
42
|
+
bridge_data = Bridge.discover_bridge(session: session, cwd: file_path)
|
|
25
43
|
exit 0 unless bridge_data
|
|
26
44
|
|
|
27
45
|
reason = Bridge.code_gate_decision(bridge_data, file_path) ||
|
|
@@ -254,6 +254,7 @@ class InstallerCore
|
|
|
254
254
|
"scripts/lib/hook_registry.rb" => "scripts/lib/hook_registry.rb",
|
|
255
255
|
"scripts/agent-report" => "scripts/agent-report",
|
|
256
256
|
"scripts/lib/insights.rb" => "scripts/lib/insights.rb",
|
|
257
|
+
"scripts/insight-append" => "scripts/insight-append",
|
|
257
258
|
"scripts/lib/worktree.rb" => "scripts/lib/worktree.rb",
|
|
258
259
|
"scripts/lib/boot_banner.rb" => "scripts/lib/boot_banner.rb",
|
|
259
260
|
"scripts/lib/dashboard_banner.rb" => "scripts/lib/dashboard_banner.rb",
|
package/scripts/lib/qmd_sync.rb
CHANGED
|
@@ -42,8 +42,10 @@ module QmdSync
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
# Collection name for a store directory.
|
|
45
|
-
# global store (<plastic_home>/store)
|
|
46
|
-
# project store (<.../projects/<slug>/store)
|
|
45
|
+
# global store (<plastic_home>/store) -> "plastic-global"
|
|
46
|
+
# project store (<.../projects/<slug>/store) -> "plastic-<slug>"
|
|
47
|
+
# roadmaps dir (sibling of either store above) -> the sibling store's
|
|
48
|
+
# collection name, suffixed "-roadmaps"
|
|
47
49
|
# Slug is resolved from projects.yml by matching the project path; falls back
|
|
48
50
|
# to the directory's parent name when no registry match exists.
|
|
49
51
|
def collection_name(store_dir, plastic_home:)
|
|
@@ -51,12 +53,19 @@ module QmdSync
|
|
|
51
53
|
global_store = File.expand_path(File.join(plastic_home, "store"))
|
|
52
54
|
return "plastic-global" if store_dir == global_store
|
|
53
55
|
|
|
56
|
+
if File.basename(store_dir) == "roadmaps"
|
|
57
|
+
sibling_store = File.join(File.dirname(store_dir), "store")
|
|
58
|
+
return "#{collection_name(sibling_store, plastic_home: plastic_home)}-roadmaps"
|
|
59
|
+
end
|
|
60
|
+
|
|
54
61
|
slug = slug_for_store(store_dir, plastic_home: plastic_home)
|
|
55
62
|
"plastic-#{slug}"
|
|
56
63
|
end
|
|
57
64
|
|
|
58
65
|
# Every store Plastic knows about: the global store plus each registered
|
|
59
|
-
# project store
|
|
66
|
+
# project store, plus a roadmap-companion entry per tier when that tier's
|
|
67
|
+
# sibling `roadmaps/` directory exists on disk (intent 135). Returns
|
|
68
|
+
# [{collection:, dir:}, ...].
|
|
60
69
|
def enumerate_stores(plastic_home:)
|
|
61
70
|
stores = [{
|
|
62
71
|
collection: "plastic-global",
|
|
@@ -75,7 +84,12 @@ module QmdSync
|
|
|
75
84
|
dir = Dir.exist?(mirror_store) ? mirror_store : project_store
|
|
76
85
|
stores << { collection: "plastic-#{slug}", dir: dir }
|
|
77
86
|
end
|
|
78
|
-
|
|
87
|
+
|
|
88
|
+
stores.flat_map do |s|
|
|
89
|
+
roadmaps_dir = File.join(File.dirname(s[:dir]), "roadmaps")
|
|
90
|
+
next [s] unless Dir.exist?(roadmaps_dir)
|
|
91
|
+
[s, { collection: "#{s[:collection]}-roadmaps", dir: roadmaps_dir }]
|
|
92
|
+
end
|
|
79
93
|
end
|
|
80
94
|
|
|
81
95
|
# Register a store directory as a collection. Idempotent: re-running is safe.
|
|
@@ -185,7 +199,12 @@ module QmdSync
|
|
|
185
199
|
# project's collection plus plastic-global, or just plastic-global when the
|
|
186
200
|
# CWD is not inside any registered project. Project match = CWD equals the
|
|
187
201
|
# registered path or is nested under it.
|
|
188
|
-
|
|
202
|
+
#
|
|
203
|
+
# Each collection's roadmap companion (intent 135) is appended right after it
|
|
204
|
+
# when actually registered (checked via the injected runner's `collection
|
|
205
|
+
# list`), because `qmd search -c <unregistered-collection>` hard-fails the
|
|
206
|
+
# whole call, so an unregistered companion must never be added blindly.
|
|
207
|
+
def collections_for_cwd(cwd, plastic_home:, runner: default_runner, detector: method(:detect))
|
|
189
208
|
cwd = File.expand_path(cwd)
|
|
190
209
|
projects = load_projects(plastic_home)
|
|
191
210
|
slug, = projects.find do |_s, info|
|
|
@@ -194,7 +213,15 @@ module QmdSync
|
|
|
194
213
|
root = File.expand_path(path)
|
|
195
214
|
cwd == root || cwd.start_with?(root + File::SEPARATOR)
|
|
196
215
|
end
|
|
197
|
-
|
|
216
|
+
|
|
217
|
+
base = slug ? ["plastic-#{slug}", "plastic-global"] : ["plastic-global"]
|
|
218
|
+
return base unless detector.call
|
|
219
|
+
|
|
220
|
+
registered = list_collections(runner)
|
|
221
|
+
base.flat_map do |c|
|
|
222
|
+
roadmap = "#{c}-roadmaps"
|
|
223
|
+
registered.include?(roadmap) ? [c, roadmap] : [c]
|
|
224
|
+
end
|
|
198
225
|
end
|
|
199
226
|
|
|
200
227
|
# --- internals ---
|
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)
|
|
96
112
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if
|
|
100
|
-
|
|
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)
|
|
130
|
+
|
|
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],
|
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
|
|
@@ -123,7 +134,8 @@ end
|
|
|
123
134
|
intent_dir = File.expand_path(intent_dir_arg)
|
|
124
135
|
|
|
125
136
|
fm = frontmatter_for(intent_dir)
|
|
126
|
-
|
|
137
|
+
raw_id = fm["id"].to_s.strip
|
|
138
|
+
id = raw_id
|
|
127
139
|
intent_name = fm["intent"].to_s.strip
|
|
128
140
|
id = "(unknown)" if id.empty?
|
|
129
141
|
intent_name = "(unknown)" if intent_name.empty?
|
|
@@ -131,12 +143,24 @@ intent_name = "(unknown)" if intent_name.empty?
|
|
|
131
143
|
stage = current_stage(intent_dir)
|
|
132
144
|
cycle = role || step || stage
|
|
133
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
|
+
|
|
134
154
|
lines = []
|
|
135
155
|
lines << "=== Plastic spawn preamble ==="
|
|
136
156
|
lines << "Store: #{intent_dir}"
|
|
137
157
|
lines << "Active intent: #{id} - #{intent_name}"
|
|
138
158
|
lines << "Current stage: #{stage}"
|
|
139
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
|
|
140
164
|
lines << ""
|
|
141
165
|
lines << HONOR_INSTRUCTION
|
|
142
166
|
lines << ""
|
package/skills/auto/SKILL.md
CHANGED
|
@@ -105,7 +105,7 @@ Roster (one role per cycle stage):
|
|
|
105
105
|
|
|
106
106
|
Dispatch rule: sequential, one specialist per stage on one branch (the deliverables share files). Gate each deliverable against the stage's exit criteria before handing off. The How and Exec phases below default to Plastic's native dispatch (`plastic-executing-plan`) and delegate to the superpowers skills only when they are available or the user asks; do not restate the phase mechanics here.
|
|
107
107
|
|
|
108
|
-
Spawn preamble (live-state injection): before dispatching any specialist, run `scripts/spawn-preamble <intent_dir> --role <role>` and PREPEND its output to that specialist's prompt. The preamble is a deterministic, filesystem-only snapshot of the active intent (id, intent line, current stage) plus the honoring instruction, so every spawned agent boots with accurate live state instead of guessing. This is the authoritative L2 mechanism for harnesses whose sub-agents do not inherit a top-level session event (see `docs/reference/harness-adapters.md`).
|
|
108
|
+
Spawn preamble (live-state injection): before dispatching any specialist, run `scripts/spawn-preamble <intent_dir> --role <role>` and PREPEND its output to that specialist's prompt. The preamble is a deterministic, filesystem-only snapshot of the active intent (id, intent line, current stage, and the provisioned code worktree path when one exists on disk) plus the honoring instruction, so every spawned agent boots with accurate live state instead of guessing. This is the authoritative L2 mechanism for harnesses whose sub-agents do not inherit a top-level session event (see `docs/reference/harness-adapters.md`).
|
|
109
109
|
|
|
110
110
|
Dispatch-time model contract (belt-and-braces): alongside the preamble, resolve each specialist's model through the config chain (`read-config agents.models.<basename> --project <repo>`: project override, then global, then the shipped tier default) and pass it explicitly at dispatch. Never rely on the dispatched role's frontmatter alone; a resolved subagent model is never Fable.
|
|
111
111
|
|
|
@@ -20,6 +20,8 @@ next" in under a minute, just from this one file.
|
|
|
20
20
|
status mirroring that intent's current `INDEX.md` status.
|
|
21
21
|
6. Append the first `## Log` line, a short `YYYY-MM-DD HH:MM UTC`-prefixed plain-language note
|
|
22
22
|
that the roadmap was created.
|
|
23
|
+
7. Refresh the QMD index for this roadmap (no-op when QMD is absent), in the background so it
|
|
24
|
+
never blocks: `ruby ~/.plastic/scripts/qmd-sync reindex --store <roadmaps-dir> --async`.
|
|
23
25
|
|
|
24
26
|
## Add / reorder entries
|
|
25
27
|
|
|
@@ -32,6 +34,8 @@ next" in under a minute, just from this one file.
|
|
|
32
34
|
entry is eligible to run.
|
|
33
35
|
- After any add/reorder, append a `## Log` line describing the change (e.g.
|
|
34
36
|
`- <YYYY-MM-DD HH:MM UTC> added 132 to wave 2`).
|
|
37
|
+
- Refresh the QMD index for this roadmap (no-op when QMD is absent), in the background so it
|
|
38
|
+
never blocks: `ruby ~/.plastic/scripts/qmd-sync reindex --store <roadmaps-dir> --async`.
|
|
35
39
|
|
|
36
40
|
## Sync status mirror
|
|
37
41
|
|
|
@@ -45,6 +49,8 @@ next" in under a minute, just from this one file.
|
|
|
45
49
|
one-line EM-to-CTO entry described in `file-format.md` (date, what shipped and its impact in
|
|
46
50
|
plain language, then a link to that intent's `outcome.md`). For other transitions, write a
|
|
47
51
|
short dated plain-language line (no codenames, no jargon).
|
|
52
|
+
5. Refresh the QMD index for this roadmap (no-op when QMD is absent), in the background so it
|
|
53
|
+
never blocks: `ruby ~/.plastic/scripts/qmd-sync reindex --store <roadmaps-dir> --async`.
|
|
48
54
|
|
|
49
55
|
## Append a log line
|
|
50
56
|
|
|
@@ -54,6 +60,8 @@ next" in under a minute, just from this one file.
|
|
|
54
60
|
A delivery event follows the EM-to-CTO one-line shape with an `outcome.md` link (see
|
|
55
61
|
`file-format.md`); bookkeeping events (created, an intent added to a wave, a wave completed, a
|
|
56
62
|
roadmap closed) are short dated plain-language lines.
|
|
63
|
+
- Refresh the QMD index for this roadmap (no-op when QMD is absent), in the background so it
|
|
64
|
+
never blocks: `ruby ~/.plastic/scripts/qmd-sync reindex --store <roadmaps-dir> --async`.
|
|
57
65
|
|
|
58
66
|
## Read / consume
|
|
59
67
|
|
|
@@ -76,3 +84,5 @@ next" in under a minute, just from this one file.
|
|
|
76
84
|
lists only live (open or in-flight) roadmaps.
|
|
77
85
|
4. Append the final `## Log` line before or as part of the move:
|
|
78
86
|
`- <YYYY-MM-DD HH:MM UTC> roadmap closed`.
|
|
87
|
+
5. Refresh the QMD index for this roadmap (no-op when QMD is absent), in the background so it
|
|
88
|
+
never blocks: `ruby ~/.plastic/scripts/qmd-sync reindex --store <roadmaps-dir> --async`.
|