@zalom/plastic 1.0.0-alpha.34 → 1.0.0-alpha.36
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/agents/plastic-brainstorming.md +2 -0
- package/agents/plastic-executor.md +2 -0
- package/agents/plastic-planner.md +2 -0
- package/agents/plastic-spec-specialist.md +2 -0
- package/hooks/hooks.json +10 -0
- package/hooks/qmd-search +8 -0
- package/package.json +1 -1
- package/scripts/doctor.rb +1 -0
- package/scripts/hook-gate-check +19 -0
- package/scripts/hook-qmd-search +44 -0
- package/scripts/lib/installer_core.rb +4 -0
- package/scripts/lib/qmd_hook.rb +44 -0
- package/scripts/lib/qmd_sync.rb +50 -0
- package/scripts/spawn-preamble +121 -0
- package/skills/auto/SKILL.md +2 -0
- package/skills/auto/references/agent-architecture.md +13 -0
|
@@ -12,6 +12,8 @@ model: inherit
|
|
|
12
12
|
|
|
13
13
|
You are the Plastic Brainstorming specialist. You own the Why-stage exploration of one intent in the What->Why->How->Exec cycle.
|
|
14
14
|
|
|
15
|
+
When dispatched in auto mode you receive the standard Plastic spawn preamble (from `scripts/spawn-preamble`) prepended to your prompt: it states the active intent id, intent line, current stage, your role, and the instruction to emit valid lifecycle artifacts. Honor it as your live state; do not re-derive or contradict it.
|
|
16
|
+
|
|
15
17
|
## Your Responsibilities
|
|
16
18
|
|
|
17
19
|
1. **Explore the problem** — read the intent's `## Intent` and `## Context`, the linked intents, and the relevant code
|
|
@@ -12,6 +12,8 @@ model: inherit
|
|
|
12
12
|
|
|
13
13
|
You are the Plastic Executor. You own the Exec stage of the What->Why->How->Exec cycle.
|
|
14
14
|
|
|
15
|
+
When dispatched in auto mode you receive the standard Plastic spawn preamble (from `scripts/spawn-preamble`) prepended to your prompt: it states the active intent id, intent line, current stage, your role, and the instruction to emit valid lifecycle artifacts. Honor it as your live state; do not re-derive or contradict it.
|
|
16
|
+
|
|
15
17
|
## Your Responsibilities
|
|
16
18
|
|
|
17
19
|
1. **Implement the actions** — make the code changes for each action in order
|
|
@@ -12,6 +12,8 @@ model: inherit
|
|
|
12
12
|
|
|
13
13
|
You are the Plastic Planner. You own the How stage of the What->Why->How->Exec cycle.
|
|
14
14
|
|
|
15
|
+
When dispatched in auto mode you receive the standard Plastic spawn preamble (from `scripts/spawn-preamble`) prepended to your prompt: it states the active intent id, intent line, current stage, your role, and the instruction to emit valid lifecycle artifacts. Honor it as your live state; do not re-derive or contradict it.
|
|
16
|
+
|
|
15
17
|
## Your Responsibilities
|
|
16
18
|
|
|
17
19
|
1. **Decompose the spec** — break the approach into ordered, independent actions
|
|
@@ -12,6 +12,8 @@ model: inherit
|
|
|
12
12
|
|
|
13
13
|
You are the Plastic Spec Specialist. You own the Why-to-How boundary in the What->Why->How->Exec cycle.
|
|
14
14
|
|
|
15
|
+
When dispatched in auto mode you receive the standard Plastic spawn preamble (from `scripts/spawn-preamble`) prepended to your prompt: it states the active intent id, intent line, current stage, your role, and the instruction to emit valid lifecycle artifacts. Honor it as your live state; do not re-derive or contradict it.
|
|
16
|
+
|
|
15
17
|
## Your Responsibilities
|
|
16
18
|
|
|
17
19
|
1. **Consolidate the Why** — turn the enriched `## Context` and `### Decisions` into one spec
|
package/hooks/hooks.json
CHANGED
|
@@ -93,6 +93,16 @@
|
|
|
93
93
|
"statusMessage": "Checking auto mode..."
|
|
94
94
|
}
|
|
95
95
|
]
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"matcher": "",
|
|
99
|
+
"hooks": [
|
|
100
|
+
{
|
|
101
|
+
"type": "command",
|
|
102
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\" qmd-search",
|
|
103
|
+
"statusMessage": "Searching QMD..."
|
|
104
|
+
}
|
|
105
|
+
]
|
|
96
106
|
}
|
|
97
107
|
]
|
|
98
108
|
}
|
package/hooks/qmd-search
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# qmd-first search hook launcher (intent 66). No-op when there is no global store.
|
|
3
|
+
GLOBAL_INDEX="$HOME/.plastic/INDEX.md"
|
|
4
|
+
if [ ! -f "$GLOBAL_INDEX" ]; then
|
|
5
|
+
exit 0
|
|
6
|
+
fi
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
+
exec ruby "$SCRIPT_DIR/../scripts/hook-qmd-search" "$HOME/.plastic"
|
package/package.json
CHANGED
package/scripts/doctor.rb
CHANGED
package/scripts/hook-gate-check
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
require "json"
|
|
10
10
|
require_relative "lib/bridge"
|
|
11
|
+
require_relative "lib/intent_validator"
|
|
11
12
|
|
|
12
13
|
file_path = ARGV[0]
|
|
13
14
|
exit 0 unless file_path && !file_path.empty?
|
|
@@ -28,6 +29,24 @@ if intent_dir_abs
|
|
|
28
29
|
end
|
|
29
30
|
end
|
|
30
31
|
|
|
32
|
+
# --- Artifact-validity backstop (intent 4a1c1) ---
|
|
33
|
+
# When the written file IS the intent file itself (the `<id>--<slug>.md` directly
|
|
34
|
+
# inside `store/<id>--<slug>/`), run IntentValidator on it. This is headless-safe:
|
|
35
|
+
# it depends only on the file path and disk, never on a bridge or session. In the
|
|
36
|
+
# PostToolUse model the write has already happened, so we cannot prevent it; the
|
|
37
|
+
# loud non-zero exit + stderr is the rejection signal. NOT for spec.md/plan.md/
|
|
38
|
+
# checklist.md/outcome.md/savepoint.md — those are validated by the stage gates.
|
|
39
|
+
if intent_dir_abs && File.basename(file_path_abs) == File.basename(Bridge.intent_file(intent_dir_abs))
|
|
40
|
+
result = IntentValidator.validate(intent_dir_abs)
|
|
41
|
+
unless result[:ok]
|
|
42
|
+
warn "PLASTIC ARTIFACT INVALID — #{File.basename(file_path_abs)} is not born complete:"
|
|
43
|
+
result[:missing].each { |field| warn " missing required field: #{field}" }
|
|
44
|
+
result[:errors].each { |error| warn " #{error}" }
|
|
45
|
+
warn "Fix the frontmatter; the intent is not valid until every required field is present and sources/chain are well-formed."
|
|
46
|
+
exit 1
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
31
50
|
# --- Find bridge file ---
|
|
32
51
|
bridge_data = Bridge.discover_bridge(session: session, cwd: Dir.pwd)
|
|
33
52
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# qmd-first UserPromptSubmit hook (intent 66). Reads the prompt from stdin JSON,
|
|
6
|
+
# delegates the decision to QmdHook.run under a hard timeout, and prints
|
|
7
|
+
# hookSpecificOutput when there is something to inject/remind. Silent no-op (exit
|
|
8
|
+
# 0) when qmd is absent, the prompt is trivial, or anything goes wrong. ARGV[0]
|
|
9
|
+
# is the plastic_home (passed by the launcher); defaults to ~/.plastic.
|
|
10
|
+
require "json"
|
|
11
|
+
require "timeout"
|
|
12
|
+
require_relative "lib/qmd_hook"
|
|
13
|
+
|
|
14
|
+
raw = begin
|
|
15
|
+
STDIN.read
|
|
16
|
+
rescue StandardError
|
|
17
|
+
""
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
prompt = begin
|
|
21
|
+
parsed = JSON.parse(raw)
|
|
22
|
+
parsed.is_a?(Hash) ? parsed["user_prompt"].to_s : ""
|
|
23
|
+
rescue StandardError
|
|
24
|
+
""
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
plastic_home = (ARGV[0] && !ARGV[0].empty?) ? ARGV[0] : File.expand_path("~/.plastic")
|
|
28
|
+
|
|
29
|
+
context = begin
|
|
30
|
+
Timeout.timeout(2) do
|
|
31
|
+
QmdHook.run(prompt: prompt, cwd: Dir.pwd, plastic_home: plastic_home)
|
|
32
|
+
end
|
|
33
|
+
rescue Exception
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
exit 0 if context.nil? || context.strip.empty?
|
|
38
|
+
|
|
39
|
+
puts JSON.generate(
|
|
40
|
+
"hookSpecificOutput" => {
|
|
41
|
+
"hookEventName" => "UserPromptSubmit",
|
|
42
|
+
"additionalContext" => context,
|
|
43
|
+
}
|
|
44
|
+
)
|
|
@@ -199,6 +199,8 @@ class InstallerCore
|
|
|
199
199
|
"scripts/hook-continue" => "scripts/hook-continue",
|
|
200
200
|
"scripts/hook-future-intent-check" => "scripts/hook-future-intent-check",
|
|
201
201
|
"scripts/hook-gate-check" => "scripts/hook-gate-check",
|
|
202
|
+
"scripts/hook-qmd-search" => "scripts/hook-qmd-search",
|
|
203
|
+
"scripts/lib/qmd_hook.rb" => "scripts/lib/qmd_hook.rb",
|
|
202
204
|
"scripts/hook-code-gate" => "scripts/hook-code-gate",
|
|
203
205
|
"scripts/hook-bash-gate" => "scripts/hook-bash-gate",
|
|
204
206
|
"scripts/hook-auto-arm" => "scripts/hook-auto-arm",
|
|
@@ -208,6 +210,7 @@ class InstallerCore
|
|
|
208
210
|
"scripts/qmd-sync" => "scripts/qmd-sync",
|
|
209
211
|
"scripts/lib/intent_validator.rb" => "scripts/lib/intent_validator.rb",
|
|
210
212
|
"scripts/validate-intent" => "scripts/validate-intent",
|
|
213
|
+
"scripts/spawn-preamble" => "scripts/spawn-preamble",
|
|
211
214
|
"scripts/lib/store_provisioning.rb" => "scripts/lib/store_provisioning.rb",
|
|
212
215
|
"scripts/provision-project-store" => "scripts/provision-project-store",
|
|
213
216
|
"scripts/lib/installer_core.rb" => "scripts/lib/installer_core.rb",
|
|
@@ -542,6 +545,7 @@ class InstallerCore
|
|
|
542
545
|
{ "type" => "command", "command" => "#{hook_dir}/plastic-continue", "statusMessage" => "Checking for continue..." },
|
|
543
546
|
{ "type" => "command", "command" => "#{hook_dir}/plastic-future-intent-check", "statusMessage" => "Checking future intents..." },
|
|
544
547
|
{ "type" => "command", "command" => "#{hook_dir}/plastic-auto-arm", "statusMessage" => "Checking auto mode..." },
|
|
548
|
+
{ "type" => "command", "command" => "#{hook_dir}/plastic-qmd-search", "statusMessage" => "Searching QMD..." },
|
|
545
549
|
],
|
|
546
550
|
},
|
|
547
551
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "qmd_sync"
|
|
5
|
+
|
|
6
|
+
# QmdHook — decision logic for the qmd-first UserPromptSubmit hook (intent 66).
|
|
7
|
+
# Pure and dependency-injected: returns the additionalContext string to emit, or
|
|
8
|
+
# nil to emit nothing. The executable hook wires real deps and prints; this is
|
|
9
|
+
# unit-tested with a fake runner/detector (no real qmd, no network).
|
|
10
|
+
module QmdHook
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
MIN_PROMPT_LENGTH = 10
|
|
14
|
+
REMINDER = "qmd is available: query it (`qmd search` / `qmd query` over the " \
|
|
15
|
+
"`plastic-*` collections) before grep/Read when gathering intent " \
|
|
16
|
+
"context (sources/chain) or checking whether this work already " \
|
|
17
|
+
"exists as an intent."
|
|
18
|
+
|
|
19
|
+
def run(prompt:, cwd:, plastic_home:, runner: QmdSync.default_runner,
|
|
20
|
+
detector: QmdSync.method(:detect), limit: 3, min_score: 0.5)
|
|
21
|
+
return nil unless detector.call
|
|
22
|
+
p = prompt.to_s.strip
|
|
23
|
+
return nil if p.length < MIN_PROMPT_LENGTH
|
|
24
|
+
return nil if p.downcase == "continue"
|
|
25
|
+
|
|
26
|
+
collections = QmdSync.collections_for_cwd(cwd, plastic_home: plastic_home)
|
|
27
|
+
hits = QmdSync.search(p, collections: collections, limit: limit,
|
|
28
|
+
min_score: min_score, runner: runner, detector: detector)
|
|
29
|
+
|
|
30
|
+
parts = []
|
|
31
|
+
if hits.any?
|
|
32
|
+
parts << "Related / prior Plastic intents (qmd BM25, includes completed) — " \
|
|
33
|
+
"check before treating this as new work:"
|
|
34
|
+
hits.each do |h|
|
|
35
|
+
loc = h[:file].to_s.sub(%r{\Aqmd://}, "")
|
|
36
|
+
pct = (h[:score] * 100).round
|
|
37
|
+
parts << "- [#{pct}%] #{loc} — #{h[:title]}"
|
|
38
|
+
end
|
|
39
|
+
parts << ""
|
|
40
|
+
end
|
|
41
|
+
parts << REMINDER
|
|
42
|
+
parts.join("\n")
|
|
43
|
+
end
|
|
44
|
+
end
|
package/scripts/lib/qmd_sync.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "yaml"
|
|
5
|
+
require "json"
|
|
5
6
|
|
|
6
7
|
# QmdSync — the single place Plastic talks to QMD (intent 45a).
|
|
7
8
|
#
|
|
@@ -111,6 +112,55 @@ module QmdSync
|
|
|
111
112
|
missing: missing, all_registered: missing.empty? }
|
|
112
113
|
end
|
|
113
114
|
|
|
115
|
+
# Read-only BM25 search over one or more collections. Returns hits sorted by
|
|
116
|
+
# score (desc), filtered by min_score, capped at limit:
|
|
117
|
+
# [{ score: Float, file: String, line: Integer|nil, title: String, snippet: String }, ...]
|
|
118
|
+
# No-ops to [] when qmd is absent or the query is blank. Uses `qmd search --json`
|
|
119
|
+
# (BM25, no embeddings / no model downloads). Pure via the injected runner.
|
|
120
|
+
def search(query, collections:, limit: 3, min_score: 0.5, runner: default_runner, detector: method(:detect))
|
|
121
|
+
return [] unless detector.call
|
|
122
|
+
q = query.to_s.strip
|
|
123
|
+
return [] if q.empty? || Array(collections).empty?
|
|
124
|
+
|
|
125
|
+
args = ["search", q]
|
|
126
|
+
Array(collections).each { |c| args.concat(["-c", c]) }
|
|
127
|
+
args << "--json"
|
|
128
|
+
|
|
129
|
+
out, ok = runner.call(args)
|
|
130
|
+
return [] unless ok && out && !out.strip.empty?
|
|
131
|
+
|
|
132
|
+
parsed = begin
|
|
133
|
+
JSON.parse(out)
|
|
134
|
+
rescue StandardError
|
|
135
|
+
return []
|
|
136
|
+
end
|
|
137
|
+
return [] unless parsed.is_a?(Array)
|
|
138
|
+
|
|
139
|
+
parsed.filter_map do |h|
|
|
140
|
+
next unless h.is_a?(Hash)
|
|
141
|
+
score = h["score"].to_f
|
|
142
|
+
next if score < min_score
|
|
143
|
+
{ score: score, file: h["file"].to_s, line: h["line"],
|
|
144
|
+
title: h["title"].to_s, snippet: h["snippet"].to_s }
|
|
145
|
+
end.sort_by { |h| -h[:score] }.first(limit)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Which collections to search for a given working directory: the matched
|
|
149
|
+
# project's collection plus plastic-global, or just plastic-global when the
|
|
150
|
+
# CWD is not inside any registered project. Project match = CWD equals the
|
|
151
|
+
# registered path or is nested under it.
|
|
152
|
+
def collections_for_cwd(cwd, plastic_home:)
|
|
153
|
+
cwd = File.expand_path(cwd)
|
|
154
|
+
projects = load_projects(plastic_home)
|
|
155
|
+
slug, = projects.find do |_s, info|
|
|
156
|
+
path = info.is_a?(Hash) ? info["path"] : nil
|
|
157
|
+
next false unless path
|
|
158
|
+
root = File.expand_path(path)
|
|
159
|
+
cwd == root || cwd.start_with?(root + File::SEPARATOR)
|
|
160
|
+
end
|
|
161
|
+
slug ? ["plastic-#{slug}", "plastic-global"] : ["plastic-global"]
|
|
162
|
+
end
|
|
163
|
+
|
|
114
164
|
# --- internals ---
|
|
115
165
|
|
|
116
166
|
def skip_result
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# spawn-preamble — deterministic live-state injection for spawned agents
|
|
6
|
+
# (intent 4a1c1, agent harness foundation).
|
|
7
|
+
#
|
|
8
|
+
# Emits a preamble block built ONLY from filesystem state for a single intent
|
|
9
|
+
# directory. It is a PURE function of the intent dir: no network, no randomness,
|
|
10
|
+
# no wall-clock reads. Two runs over the same on-disk state produce byte-identical
|
|
11
|
+
# output. This is the authoritative L2 (live-state) mechanism for harnesses whose
|
|
12
|
+
# spawned sub-agents do not inherit a top-level session event (see
|
|
13
|
+
# docs/reference/harness-adapters.md).
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# spawn-preamble <intent_dir> [--role ROLE] [--step STEP]
|
|
17
|
+
#
|
|
18
|
+
# The preamble reports the active intent (id + intent line from frontmatter), the
|
|
19
|
+
# current lifecycle stage (last savepoint line if present, else derived from which
|
|
20
|
+
# lifecycle files exist), the cycle role/step (from --role/--step, else the
|
|
21
|
+
# derived stage), and an imperative honoring instruction.
|
|
22
|
+
#
|
|
23
|
+
# Exit codes: 0 (preamble emitted), 2 (usage).
|
|
24
|
+
|
|
25
|
+
require_relative "lib/bridge"
|
|
26
|
+
|
|
27
|
+
# Verbatim honoring instruction. Kept as one constant so the contract doc and the
|
|
28
|
+
# test assert against the exact same string.
|
|
29
|
+
HONOR_INSTRUCTION =
|
|
30
|
+
"You are operating inside Plastic. Use it as your operating scaffold. " \
|
|
31
|
+
"Emit VALID lifecycle artifacts; do not hallucinate intents or stages. " \
|
|
32
|
+
"Your output is a deliverable, not a message."
|
|
33
|
+
|
|
34
|
+
def parse_args(argv)
|
|
35
|
+
role = nil
|
|
36
|
+
step = nil
|
|
37
|
+
positional = []
|
|
38
|
+
i = 0
|
|
39
|
+
while i < argv.length
|
|
40
|
+
case argv[i]
|
|
41
|
+
when "--role"
|
|
42
|
+
role = argv[i + 1]
|
|
43
|
+
i += 2
|
|
44
|
+
when "--step"
|
|
45
|
+
step = argv[i + 1]
|
|
46
|
+
i += 2
|
|
47
|
+
else
|
|
48
|
+
positional << argv[i]
|
|
49
|
+
i += 1
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
[positional.first, role, step]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Read the intent file frontmatter via the same parser the validator uses, so the
|
|
56
|
+
# id/intent we report match what the rest of Plastic sees. Returns a Hash (possibly
|
|
57
|
+
# empty) — never raises.
|
|
58
|
+
def frontmatter_for(intent_dir)
|
|
59
|
+
ifile = Bridge.intent_file(intent_dir)
|
|
60
|
+
return {} unless File.exist?(ifile)
|
|
61
|
+
|
|
62
|
+
content = File.read(ifile)
|
|
63
|
+
return {} unless content.start_with?("---")
|
|
64
|
+
|
|
65
|
+
parts = content.split("---", 3)
|
|
66
|
+
return {} if parts.length < 3
|
|
67
|
+
|
|
68
|
+
require "yaml"
|
|
69
|
+
require "date"
|
|
70
|
+
require "time"
|
|
71
|
+
YAML.safe_load(parts[1], permitted_classes: [Date, Time]) || {}
|
|
72
|
+
rescue StandardError
|
|
73
|
+
{}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Current stage label. Prefer the last non-empty line of savepoint.md (the ledger
|
|
77
|
+
# already encodes the furthest-reached milestone). Fall back to the file-derived
|
|
78
|
+
# stage when there is no ledger.
|
|
79
|
+
STAGE_LABELS = {
|
|
80
|
+
"what" => "What", "why" => "Why", "how" => "How",
|
|
81
|
+
"exec" => "Exec", "done" => "Done"
|
|
82
|
+
}.freeze
|
|
83
|
+
|
|
84
|
+
def current_stage(intent_dir)
|
|
85
|
+
ledger = File.join(intent_dir, Bridge::SAVEPOINT_FILE)
|
|
86
|
+
if File.exist?(ledger)
|
|
87
|
+
last = File.read(ledger).each_line.map(&:strip).reject(&:empty?).last
|
|
88
|
+
return last if last
|
|
89
|
+
end
|
|
90
|
+
STAGE_LABELS.fetch(Bridge.derive_stage(intent_dir), Bridge.derive_stage(intent_dir))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
intent_dir_arg, role, step = parse_args(ARGV)
|
|
94
|
+
|
|
95
|
+
if intent_dir_arg.nil? || intent_dir_arg.empty?
|
|
96
|
+
warn "usage: spawn-preamble <intent_dir> [--role ROLE] [--step STEP]"
|
|
97
|
+
exit 2
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
intent_dir = File.expand_path(intent_dir_arg)
|
|
101
|
+
|
|
102
|
+
fm = frontmatter_for(intent_dir)
|
|
103
|
+
id = fm["id"].to_s.strip
|
|
104
|
+
intent_name = fm["intent"].to_s.strip
|
|
105
|
+
id = "(unknown)" if id.empty?
|
|
106
|
+
intent_name = "(unknown)" if intent_name.empty?
|
|
107
|
+
|
|
108
|
+
stage = current_stage(intent_dir)
|
|
109
|
+
cycle = role || step || stage
|
|
110
|
+
|
|
111
|
+
lines = []
|
|
112
|
+
lines << "=== Plastic spawn preamble ==="
|
|
113
|
+
lines << "Store: #{intent_dir}"
|
|
114
|
+
lines << "Active intent: #{id} - #{intent_name}"
|
|
115
|
+
lines << "Current stage: #{stage}"
|
|
116
|
+
lines << "Cycle step / role: #{cycle}"
|
|
117
|
+
lines << ""
|
|
118
|
+
lines << HONOR_INSTRUCTION
|
|
119
|
+
lines << "=== end preamble ==="
|
|
120
|
+
|
|
121
|
+
puts lines.join("\n")
|
package/skills/auto/SKILL.md
CHANGED
|
@@ -66,6 +66,8 @@ Roster (one role per cycle stage):
|
|
|
66
66
|
|
|
67
67
|
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.
|
|
68
68
|
|
|
69
|
+
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`).
|
|
70
|
+
|
|
69
71
|
Final-gate review: dispatch an independent reviewer subagent at the final gate only, not as a standing role.
|
|
70
72
|
|
|
71
73
|
Headless manual gate: when running headless or in the background, enforce gates manually and do not rely on hooks, because `CLAUDE_SESSION_ID` may be unset (this ties to the arm-gate fallback above).
|
|
@@ -48,6 +48,19 @@ The chain: intent `## Intent` / `## Context`, then enriched `## Context` plus
|
|
|
48
48
|
`### Decisions`, then `spec.md`, then `plan.md` plus `actions/` plus `checklist.md`,
|
|
49
49
|
then the code changes plus a checked-off checklist plus `## Insights`.
|
|
50
50
|
|
|
51
|
+
### Spawn Preamble (L2 live-state injection)
|
|
52
|
+
|
|
53
|
+
Every dispatched specialist is booted with a spawn preamble: the enforcer runs
|
|
54
|
+
`scripts/spawn-preamble <intent_dir> --role <role>` and prepends its output to the
|
|
55
|
+
specialist's prompt. The preamble is a pure function of the intent directory on disk
|
|
56
|
+
(no network, no clock, no randomness), so it is deterministic and rebuildable. It
|
|
57
|
+
carries the active intent id and intent line, the current lifecycle stage (the last
|
|
58
|
+
savepoint line, else stage derived from which lifecycle files exist), the cycle
|
|
59
|
+
role, and the honoring instruction that the agent must emit valid lifecycle artifacts
|
|
60
|
+
and not hallucinate intents or stages. This is the standard L2 live-state mechanism
|
|
61
|
+
for harnesses whose spawned sub-agents do not inherit the top-level session event. See
|
|
62
|
+
`docs/reference/harness-adapters.md` for how it slots into the per-harness contract.
|
|
63
|
+
|
|
51
64
|
### Gate Ownership
|
|
52
65
|
|
|
53
66
|
The enforcer arms and verifies the lifecycle gate, then gates every stage transition.
|