@zalom/plastic 1.0.0-alpha.26 → 1.0.0-alpha.28
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/package.json +1 -1
- package/scripts/doctor.rb +45 -0
- package/scripts/hook-session-start +20 -0
- package/scripts/lib/installer_core.rb +2 -0
- package/scripts/lib/qmd_sync.rb +159 -0
- package/scripts/qmd-sync +92 -0
- package/skills/auto/SKILL.md +8 -2
- package/skills/creating-project/SKILL.md +12 -1
- package/skills/executing-plan/SKILL.md +2 -0
- package/skills/install/SKILL.md +14 -1
package/package.json
CHANGED
package/scripts/doctor.rb
CHANGED
|
@@ -15,6 +15,8 @@ require "time"
|
|
|
15
15
|
require "date"
|
|
16
16
|
require "digest"
|
|
17
17
|
|
|
18
|
+
require_relative "lib/qmd_sync"
|
|
19
|
+
|
|
18
20
|
# Diagnostic engine, instantiable with an injected store/agent map so tests can
|
|
19
21
|
# run it hermetically (no eval, no global-constant rewriting).
|
|
20
22
|
class Doctor
|
|
@@ -1005,6 +1007,48 @@ class Doctor
|
|
|
1005
1007
|
a_pre <=> b_pre
|
|
1006
1008
|
end
|
|
1007
1009
|
|
|
1010
|
+
# --- Check category: QMD integration (read-only, optional) ---
|
|
1011
|
+
#
|
|
1012
|
+
# QMD is an optional integration. When `qmd` is not on PATH we emit a single
|
|
1013
|
+
# passing check and never fail — its absence is not a Plastic health problem.
|
|
1014
|
+
# When present, we report whether every Plastic store is registered as a QMD
|
|
1015
|
+
# collection. Everything here is read-only; we never invoke a mutating qmd
|
|
1016
|
+
# subcommand. detector/runner are injectable so tests stay hermetic.
|
|
1017
|
+
def check_qmd(detector: QmdSync.method(:detect), runner: QmdSync.default_runner)
|
|
1018
|
+
return [absent_qmd_check] unless detector.call
|
|
1019
|
+
|
|
1020
|
+
checks = [check(
|
|
1021
|
+
category: "qmd", name: "present", status: "pass",
|
|
1022
|
+
message: "QMD installed"
|
|
1023
|
+
)]
|
|
1024
|
+
|
|
1025
|
+
status = QmdSync.status(plastic_home: plastic_home, runner: runner, detector: detector)
|
|
1026
|
+
missing = status[:missing] || []
|
|
1027
|
+
|
|
1028
|
+
if status[:all_registered]
|
|
1029
|
+
checks << check(
|
|
1030
|
+
category: "qmd", name: "collections", status: "pass",
|
|
1031
|
+
message: "All #{status[:expected].size} Plastic store(s) registered as QMD collections"
|
|
1032
|
+
)
|
|
1033
|
+
else
|
|
1034
|
+
checks << check(
|
|
1035
|
+
category: "qmd", name: "collections", status: "warn",
|
|
1036
|
+
message: "#{missing.size} Plastic store(s) not registered as QMD collections",
|
|
1037
|
+
details: missing,
|
|
1038
|
+
fixable: true, fix_hint: "Run: qmd-sync register --all"
|
|
1039
|
+
)
|
|
1040
|
+
end
|
|
1041
|
+
|
|
1042
|
+
checks
|
|
1043
|
+
end
|
|
1044
|
+
|
|
1045
|
+
def absent_qmd_check
|
|
1046
|
+
check(
|
|
1047
|
+
category: "qmd", name: "present", status: "pass",
|
|
1048
|
+
message: "QMD not installed (optional integration)"
|
|
1049
|
+
)
|
|
1050
|
+
end
|
|
1051
|
+
|
|
1008
1052
|
# --- Run all checks ---
|
|
1009
1053
|
|
|
1010
1054
|
def run_checks(agent_key)
|
|
@@ -1015,6 +1059,7 @@ class Doctor
|
|
|
1015
1059
|
all_checks += check_core_files(agent_key)
|
|
1016
1060
|
all_checks += check_project_stores
|
|
1017
1061
|
all_checks += check_deprecations
|
|
1062
|
+
all_checks += check_qmd
|
|
1018
1063
|
|
|
1019
1064
|
summarize(all_checks, agent_key)
|
|
1020
1065
|
end
|
|
@@ -8,6 +8,7 @@ require "date"
|
|
|
8
8
|
require "yaml"
|
|
9
9
|
require_relative "lib/bridge"
|
|
10
10
|
require_relative "lib/boot_banner"
|
|
11
|
+
require_relative "lib/qmd_sync"
|
|
11
12
|
require_relative "doctor"
|
|
12
13
|
|
|
13
14
|
index_path, store_root, mode, plugin_root = ARGV
|
|
@@ -204,6 +205,25 @@ parts = []
|
|
|
204
205
|
parts << core_banner
|
|
205
206
|
parts << ""
|
|
206
207
|
|
|
208
|
+
# --- QMD search status (intent 45a, READ-ONLY) ---
|
|
209
|
+
# Report-only line for the model (additionalContext), never systemMessage and
|
|
210
|
+
# never the exit code. QmdSync.status shells out to `qmd collection list`, so the
|
|
211
|
+
# whole block is guarded: a 2s timeout caps any hang, and rescue-all guarantees a
|
|
212
|
+
# slow/broken/missing qmd appends nothing and the hook continues cleanly.
|
|
213
|
+
begin
|
|
214
|
+
require "timeout"
|
|
215
|
+
qmd_status = Timeout.timeout(2) { QmdSync.status(plastic_home: store_root) }
|
|
216
|
+
if qmd_status[:present]
|
|
217
|
+
if qmd_status[:all_registered]
|
|
218
|
+
parts << "QMD: #{qmd_status[:registered].size} Plastic collections indexed (search with the qmd skill)."
|
|
219
|
+
else
|
|
220
|
+
parts << "QMD detected — run `qmd-sync register --all` to index your Plastic stores for search."
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
rescue Exception
|
|
224
|
+
# Any failure (timeout, missing binary, parse error) — stay silent, never crash.
|
|
225
|
+
end
|
|
226
|
+
|
|
207
227
|
if plastic_md
|
|
208
228
|
# Conventions always loaded first
|
|
209
229
|
parts << plastic_md
|
|
@@ -204,6 +204,8 @@ class InstallerCore
|
|
|
204
204
|
"scripts/hook-auto-arm" => "scripts/hook-auto-arm",
|
|
205
205
|
"scripts/lib/bridge.rb" => "scripts/lib/bridge.rb",
|
|
206
206
|
"scripts/lib/boot_banner.rb" => "scripts/lib/boot_banner.rb",
|
|
207
|
+
"scripts/lib/qmd_sync.rb" => "scripts/lib/qmd_sync.rb",
|
|
208
|
+
"scripts/qmd-sync" => "scripts/qmd-sync",
|
|
207
209
|
"scripts/lib/installer_core.rb" => "scripts/lib/installer_core.rb",
|
|
208
210
|
"scripts/install.rb" => "scripts/install.rb",
|
|
209
211
|
"scripts/update.rb" => "scripts/update.rb",
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
# QmdSync — the single place Plastic talks to QMD (intent 45a).
|
|
7
|
+
#
|
|
8
|
+
# Plastic computes only the Plastic-specific inputs (which store directory maps to
|
|
9
|
+
# which `plastic-`prefixed collection, derived from projects.yml, plus a short
|
|
10
|
+
# context description). The actual indexing is delegated to the `qmd` CLI; this
|
|
11
|
+
# module never reimplements QMD's logic. QMD is optional: every public entry
|
|
12
|
+
# no-ops cleanly when `qmd` is not on PATH.
|
|
13
|
+
#
|
|
14
|
+
# Pure and dependency-injected: all shelling-out goes through an injected
|
|
15
|
+
# `runner` callable, so the whole module is unit-testable with no real binary,
|
|
16
|
+
# no network, and no model downloads. The default runner shells out to `qmd`.
|
|
17
|
+
module QmdSync
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# A runner is `->(args_array) { [stdout_string, success_boolean] }`.
|
|
21
|
+
# The default invokes the real `qmd` binary.
|
|
22
|
+
def default_runner
|
|
23
|
+
lambda do |args|
|
|
24
|
+
require "open3"
|
|
25
|
+
out, _err, status = Open3.capture3("qmd", *args)
|
|
26
|
+
[out, status.success?]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# True when `qmd` is resolvable on PATH. The probe is injectable so tests do
|
|
31
|
+
# not depend on the host having qmd installed.
|
|
32
|
+
def detect(path_probe: method(:which_qmd))
|
|
33
|
+
!!path_probe.call
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def which_qmd
|
|
37
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? do |dir|
|
|
38
|
+
candidate = File.join(dir, "qmd")
|
|
39
|
+
File.file?(candidate) && File.executable?(candidate)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Collection name for a store directory.
|
|
44
|
+
# global store (<plastic_home>/store) -> "plastic-global"
|
|
45
|
+
# project store (<.../projects/<slug>/store) -> "plastic-<slug>"
|
|
46
|
+
# Slug is resolved from projects.yml by matching the project path; falls back
|
|
47
|
+
# to the directory's parent name when no registry match exists.
|
|
48
|
+
def collection_name(store_dir, plastic_home:)
|
|
49
|
+
store_dir = File.expand_path(store_dir)
|
|
50
|
+
global_store = File.expand_path(File.join(plastic_home, "store"))
|
|
51
|
+
return "plastic-global" if store_dir == global_store
|
|
52
|
+
|
|
53
|
+
slug = slug_for_store(store_dir, plastic_home: plastic_home)
|
|
54
|
+
"plastic-#{slug}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Every store Plastic knows about: the global store plus each registered
|
|
58
|
+
# project store. Returns [{collection:, dir:}, ...].
|
|
59
|
+
def enumerate_stores(plastic_home:)
|
|
60
|
+
stores = [{
|
|
61
|
+
collection: "plastic-global",
|
|
62
|
+
dir: File.expand_path(File.join(plastic_home, "store")),
|
|
63
|
+
}]
|
|
64
|
+
|
|
65
|
+
projects = load_projects(plastic_home)
|
|
66
|
+
projects.each do |slug, info|
|
|
67
|
+
path = info.is_a?(Hash) ? info["path"] : nil
|
|
68
|
+
next unless path
|
|
69
|
+
project_store = File.join(File.expand_path(path), "store")
|
|
70
|
+
# Project stores live under ~/.plastic/projects/<slug>/store as the mirror;
|
|
71
|
+
# registry `path` is the project code dir, so the tactical store is the
|
|
72
|
+
# plastic_home projects mirror.
|
|
73
|
+
mirror_store = File.expand_path(File.join(plastic_home, "projects", slug.to_s, "store"))
|
|
74
|
+
dir = Dir.exist?(mirror_store) ? mirror_store : project_store
|
|
75
|
+
stores << { collection: "plastic-#{slug}", dir: dir }
|
|
76
|
+
end
|
|
77
|
+
stores
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Register a store directory as a collection. Idempotent: re-running is safe.
|
|
81
|
+
# `qmd collection add` exits non-zero when the collection already exists, so we
|
|
82
|
+
# check the collection list first and treat an existing collection as success.
|
|
83
|
+
# No-op when qmd is absent. (`qmd context add` attaches a summary to a document
|
|
84
|
+
# path, not a collection-level description, so it is not used here.)
|
|
85
|
+
def register(collection:, dir:, runner: default_runner, detector: method(:detect))
|
|
86
|
+
return skip_result unless detector.call
|
|
87
|
+
return { ran: true, ok: true, output: "exists" } if list_collections(runner).include?(collection)
|
|
88
|
+
out, ok = runner.call(["collection", "add", dir, "--name", collection])
|
|
89
|
+
{ ran: true, ok: ok, output: out.to_s.strip }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Re-index a single collection: refresh the corpus then its embeddings.
|
|
93
|
+
# Scoped embed (-c) keeps delivery-time reindex fast. No-op when qmd absent.
|
|
94
|
+
def reindex(collection:, runner: default_runner, detector: method(:detect))
|
|
95
|
+
return skip_result unless detector.call
|
|
96
|
+
_o1, ok1 = runner.call(["update"])
|
|
97
|
+
_o2, ok2 = runner.call(["embed", "-c", collection])
|
|
98
|
+
{ ran: true, ok: (ok1 && ok2) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Read-only status used by doctor and the session-start report line.
|
|
102
|
+
# Returns a structured hash; never mutates the index.
|
|
103
|
+
def status(plastic_home:, runner: default_runner, detector: method(:detect))
|
|
104
|
+
return { present: false } unless detector.call
|
|
105
|
+
|
|
106
|
+
expected = enumerate_stores(plastic_home: plastic_home).map { |s| s[:collection] }
|
|
107
|
+
listed = list_collections(runner)
|
|
108
|
+
missing = expected - listed
|
|
109
|
+
|
|
110
|
+
{ present: true, expected: expected, registered: listed,
|
|
111
|
+
missing: missing, all_registered: missing.empty? }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# --- internals ---
|
|
115
|
+
|
|
116
|
+
def skip_result
|
|
117
|
+
{ ran: false, ok: true, skipped: true }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def list_collections(runner)
|
|
121
|
+
out, ok = runner.call(["collection", "list"])
|
|
122
|
+
return [] unless ok && out
|
|
123
|
+
# qmd prints lines like "plastic-global (qmd://plastic-global/)"; pull the
|
|
124
|
+
# leading collection token off each non-indented line.
|
|
125
|
+
out.lines.filter_map do |line|
|
|
126
|
+
next if line.start_with?(" ", "\t")
|
|
127
|
+
m = line.strip.match(/\A([A-Za-z0-9][\w.-]*)\b/)
|
|
128
|
+
m && m[1]
|
|
129
|
+
end.reject { |t| %w[Collections No].include?(t) }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def load_projects(plastic_home)
|
|
133
|
+
path = File.join(plastic_home, "projects.yml")
|
|
134
|
+
return {} unless File.exist?(path)
|
|
135
|
+
data = begin
|
|
136
|
+
YAML.safe_load(File.read(path)) || {}
|
|
137
|
+
rescue StandardError
|
|
138
|
+
{}
|
|
139
|
+
end
|
|
140
|
+
projects = data.is_a?(Hash) ? data["projects"] : nil
|
|
141
|
+
projects.is_a?(Hash) ? projects : {}
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def slug_for_store(store_dir, plastic_home:)
|
|
145
|
+
projects = load_projects(plastic_home)
|
|
146
|
+
projects.each do |slug, info|
|
|
147
|
+
path = info.is_a?(Hash) ? info["path"] : nil
|
|
148
|
+
next unless path
|
|
149
|
+
project_root = File.expand_path(path)
|
|
150
|
+
mirror = File.expand_path(File.join(plastic_home, "projects", slug.to_s, "store"))
|
|
151
|
+
return slug.to_s if store_dir == File.join(project_root, "store") || store_dir == mirror
|
|
152
|
+
end
|
|
153
|
+
# Fallback: <...>/projects/<slug>/store -> slug, else parent dir name.
|
|
154
|
+
parts = store_dir.split(File::SEPARATOR)
|
|
155
|
+
idx = parts.rindex("projects")
|
|
156
|
+
return parts[idx + 1] if idx && parts[idx + 1] && parts[idx + 2] == "store"
|
|
157
|
+
File.basename(File.dirname(store_dir))
|
|
158
|
+
end
|
|
159
|
+
end
|
package/scripts/qmd-sync
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# qmd-sync — deterministic CLI over QmdSync (intent 45a).
|
|
6
|
+
#
|
|
7
|
+
# Plastic skills and hooks call these verbs instead of assembling `qmd` commands
|
|
8
|
+
# themselves. QMD is optional: when `qmd` is absent every verb exits 0 and prints
|
|
9
|
+
# a skip notice, so callers can invoke unconditionally.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# qmd-sync detect # exit 0 if qmd present, 1 if absent
|
|
13
|
+
# qmd-sync register --store <dir> # register one store as a collection
|
|
14
|
+
# qmd-sync register --all # register the global store + all projects
|
|
15
|
+
# qmd-sync reindex --store <dir> # update + embed that store's collection
|
|
16
|
+
# qmd-sync status [--format json] # read-only status
|
|
17
|
+
#
|
|
18
|
+
# --home <path> overrides the Plastic home (default: ~/.plastic).
|
|
19
|
+
|
|
20
|
+
require "json"
|
|
21
|
+
require_relative "lib/qmd_sync"
|
|
22
|
+
|
|
23
|
+
def plastic_home(args)
|
|
24
|
+
if (i = args.index("--home")) && args[i + 1]
|
|
25
|
+
File.expand_path(args[i + 1])
|
|
26
|
+
else
|
|
27
|
+
File.expand_path(ENV["PLASTIC_HOME"] || "~/.plastic")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def opt(args, name)
|
|
32
|
+
(i = args.index(name)) && args[i + 1]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
verb = ARGV.shift
|
|
36
|
+
home = plastic_home(ARGV)
|
|
37
|
+
|
|
38
|
+
unless QmdSync.detect
|
|
39
|
+
case verb
|
|
40
|
+
when "detect"
|
|
41
|
+
warn "QMD not detected on PATH."
|
|
42
|
+
exit 1
|
|
43
|
+
else
|
|
44
|
+
puts "QMD not detected; skipped (#{verb})."
|
|
45
|
+
exit 0
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
case verb
|
|
50
|
+
when "detect"
|
|
51
|
+
puts "QMD detected."
|
|
52
|
+
exit 0
|
|
53
|
+
|
|
54
|
+
when "register"
|
|
55
|
+
stores =
|
|
56
|
+
if ARGV.include?("--all")
|
|
57
|
+
QmdSync.enumerate_stores(plastic_home: home)
|
|
58
|
+
elsif (dir = opt(ARGV, "--store"))
|
|
59
|
+
[{ collection: QmdSync.collection_name(dir, plastic_home: home), dir: File.expand_path(dir) }]
|
|
60
|
+
else
|
|
61
|
+
warn "register: pass --store <dir> or --all"
|
|
62
|
+
exit 2
|
|
63
|
+
end
|
|
64
|
+
stores.each do |s|
|
|
65
|
+
next unless Dir.exist?(s[:dir])
|
|
66
|
+
res = QmdSync.register(collection: s[:collection], dir: s[:dir])
|
|
67
|
+
puts "registered #{s[:collection]} -> #{s[:dir]} (#{res[:ok] ? "ok" : "warn"})"
|
|
68
|
+
end
|
|
69
|
+
exit 0
|
|
70
|
+
|
|
71
|
+
when "reindex"
|
|
72
|
+
dir = opt(ARGV, "--store") or (warn("reindex: pass --store <dir>"); exit 2)
|
|
73
|
+
collection = QmdSync.collection_name(dir, plastic_home: home)
|
|
74
|
+
res = QmdSync.reindex(collection: collection)
|
|
75
|
+
puts "reindexed #{collection} (#{res[:ok] ? "ok" : "warn"})"
|
|
76
|
+
exit 0
|
|
77
|
+
|
|
78
|
+
when "status"
|
|
79
|
+
st = QmdSync.status(plastic_home: home)
|
|
80
|
+
if opt(ARGV, "--format") == "json"
|
|
81
|
+
puts JSON.generate(st)
|
|
82
|
+
else
|
|
83
|
+
puts "QMD present: #{st[:present]}"
|
|
84
|
+
puts "registered: #{Array(st[:registered]).join(", ")}"
|
|
85
|
+
puts "missing: #{Array(st[:missing]).join(", ")}" unless Array(st[:missing]).empty?
|
|
86
|
+
end
|
|
87
|
+
exit 0
|
|
88
|
+
|
|
89
|
+
else
|
|
90
|
+
warn "qmd-sync: unknown verb #{verb.inspect}. Use detect|register|reindex|status."
|
|
91
|
+
exit 2
|
|
92
|
+
end
|
package/skills/auto/SKILL.md
CHANGED
|
@@ -161,11 +161,17 @@ During initial project creation, all decisions are non-destructive by definition
|
|
|
161
161
|
- Update `chain` in the current intent's frontmatter
|
|
162
162
|
6. Move intent from `## Active` to `## Completed` in INDEX.md (with today's date)
|
|
163
163
|
7. Auto-commit: `cd <store-root> && git add . && git commit -m "feat: deliver intent <ID> — <name>"`
|
|
164
|
-
8.
|
|
164
|
+
8. Refresh the QMD search index for this store (optional, no-op when QMD is absent):
|
|
165
|
+
```bash
|
|
166
|
+
ruby ~/.plastic/scripts/qmd-sync reindex --store <store-root>
|
|
167
|
+
```
|
|
168
|
+
Delivery is the lifecycle event that keeps the search index fresh. `<store-root>` is the
|
|
169
|
+
store that holds this intent (the global store or the project store).
|
|
170
|
+
9. Disarm the lifecycle gate (auto delivery is finished):
|
|
165
171
|
```bash
|
|
166
172
|
ruby -r ~/.plastic/scripts/lib/bridge -e 'Bridge.disarm_auto(ENV["CLAUDE_SESSION_ID"])'
|
|
167
173
|
```
|
|
168
|
-
|
|
174
|
+
10. Notify user: "Intent [ID] — [name] delivered. [1-2 sentence summary]. See outcome.md for details."
|
|
169
175
|
|
|
170
176
|
## Error Handling
|
|
171
177
|
|
|
@@ -157,7 +157,18 @@ cd ~/.plastic && git add . && git commit -m "feat: spawn project <slug> from int
|
|
|
157
157
|
cd <project> && git add . && git commit -m "feat: initialize project from intent <ID>"
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
-
### 10.
|
|
160
|
+
### 10. Register the project store with QMD (optional)
|
|
161
|
+
|
|
162
|
+
If QMD is installed, register the new project's store as a search collection:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
ruby ~/.plastic/scripts/qmd-sync register --store ~/.plastic/projects/<slug>/store
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`qmd-sync` no-ops when QMD is absent, so run it unconditionally. This adds the
|
|
169
|
+
`plastic-<slug>` collection and indexes it.
|
|
170
|
+
|
|
171
|
+
### 11. Announce
|
|
161
172
|
|
|
162
173
|
Log in `## Insights` of each founding intent:
|
|
163
174
|
> "Project `<slug>` created at `<path>`. Tactical mirror: `project-<slug>:1` (autonomous)"
|
|
@@ -75,6 +75,7 @@ Capture observations in `## Insights`. When ALL checklist items are checked:
|
|
|
75
75
|
3. Move intent from `## Active` to `## Completed` in INDEX.md (with today's date)
|
|
76
76
|
4. Update cluster entries to show `_(completed)_`
|
|
77
77
|
5. Auto-commit: `cd <store-root> && git add . && git commit -m "feat: complete intent <ID> — <name>"`
|
|
78
|
+
6. Refresh the QMD search index for this store (optional, no-op when QMD is absent): `ruby ~/.plastic/scripts/qmd-sync reindex --store <store-root>`. Delivery is the lifecycle event that keeps the search index fresh.
|
|
78
79
|
|
|
79
80
|
**This is NOT optional.** An intent with all checklist items done but no Outcome is a broken state. Complete the intent immediately — do not leave it for later.
|
|
80
81
|
|
|
@@ -100,6 +101,7 @@ Capture observations in `## Insights`. When ALL checklist items are checked:
|
|
|
100
101
|
3. Move intent from `## Active` to `## Completed` in INDEX.md (with today's date)
|
|
101
102
|
4. Update cluster entries to show `_(completed)_`
|
|
102
103
|
5. Auto-commit: `cd <store-root> && git add . && git commit -m "feat: complete intent <ID> — <name>"`
|
|
104
|
+
6. Refresh the QMD search index for this store (optional, no-op when QMD is absent): `ruby ~/.plastic/scripts/qmd-sync reindex --store <store-root>`. Delivery is the lifecycle event that keeps the search index fresh.
|
|
103
105
|
|
|
104
106
|
**This is NOT optional.** Complete the intent immediately when work is done.
|
|
105
107
|
|
package/skills/install/SKILL.md
CHANGED
|
@@ -153,7 +153,20 @@ Auto-commit the config change.
|
|
|
153
153
|
Run `/plastic-doctor` and report the result. Resolve any fixable findings before
|
|
154
154
|
announcing success.
|
|
155
155
|
|
|
156
|
-
**Step 5:
|
|
156
|
+
**Step 5: Register stores with QMD (optional)**
|
|
157
|
+
|
|
158
|
+
QMD is an optional search layer. If it is installed, register the Plastic stores so
|
|
159
|
+
they are searchable:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
ruby ~/.plastic/scripts/qmd-sync detect && ruby ~/.plastic/scripts/qmd-sync register --all
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`qmd-sync` no-ops cleanly when QMD is absent, so this is safe to run unconditionally.
|
|
166
|
+
It registers `plastic-global` and every project store from `projects.yml`, then indexes
|
|
167
|
+
them. Report what was registered, or that QMD was not detected and the step was skipped.
|
|
168
|
+
|
|
169
|
+
**Step 6: Announce**
|
|
157
170
|
|
|
158
171
|
> "Plastic installed globally at ~/.plastic/. Health check: [doctor summary].
|
|
159
172
|
> Create your first intent with `/plastic-creating-intent`."
|