@zalom/plastic 1.0.0-beta.4 → 1.0.0-beta.5

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.
@@ -26,15 +26,17 @@ You are the Plastic Intent Curator. Your role is to maintain the health and navi
26
26
 
27
27
  ## How You Work
28
28
 
29
+ 0. QMD-first (when available): when you need to locate a specific intent (to reclassify, link, or cluster it) rather than rebuild the whole landscape, before scanning the store with grep/Read run `ruby ~/.plastic/scripts/qmd-sync search "<terms>"` to surface candidate or related intents, then open the authoritative intent file for any hit you act on. The command is a no-op when QMD is absent, so fall back to the full scan below. (This is discovery; the reindex step at completion is separate.)
29
30
  1. Scan `~/.plastic/store/*/ID--slug.md` (or project store) to understand the full intent landscape
30
31
  2. Read `~/.plastic/INDEX.md` (or project INDEX.md) to understand current organization
31
32
  3. Compare: are there intents not in any cluster? Missing from Active/Completed? Status mismatches?
32
33
  4. Make targeted edits to INDEX.md and intent frontmatter/links
33
- 5. Report what you changed
34
+ 5. On completion: whenever you move an intent to Completed, refresh the QMD index for that store so the new outcome is searchable. This is mandatory on completion and a no-op when QMD is absent, and it runs in the background so it never blocks: `ruby ~/.plastic/scripts/qmd-sync reindex --store <store-root> --async`
35
+ 6. Report what you changed
34
36
 
35
37
  ## Constraints
36
38
 
37
39
  - You only edit `~/.plastic/INDEX.md` (or project INDEX.md) and `~/.plastic/store/*/ID--slug.md` (or project store) files
38
40
  - You never create new intents — that's the creating-intent skill's job
39
41
  - You never modify `## Insights`, `## Context`, or `## Outcome` content sections — those belong to the worker
40
- - You use Read and grep/find for discovery, Edit for targeted changes
42
+ - For discovery, put QMD first when available (`qmd-sync search`), then fall back to Read and grep/find; use Edit for targeted changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalom/plastic",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.5",
4
4
  "description": "Intent-driven idea development system for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,76 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "qmd_sync"
5
+
6
+ # PowerTools — detect-then-degrade harness for Plastic's optional power-tools
7
+ # (intent 66b). It owns deterministic detection of each tool and builds an
8
+ # obligation ("mandate") string for whichever tools are present, so the agent is
9
+ # obliged (not merely reminded) to use them: QMD for finding intents, Serena for
10
+ # code navigation.
11
+ #
12
+ # Strictly detect-then-degrade: a tool that is absent contributes nothing, and
13
+ # `mandate` returns nil when no tool is present. Nothing here installs anything.
14
+ #
15
+ # Pure and dependency-injected: every detection runs through an injected callable
16
+ # or keyword probe (PATH scan / `.serena` marker walk), so the whole module is
17
+ # unit-testable with no real binaries, no network, and no global/ENV state.
18
+ module PowerTools
19
+ module_function
20
+
21
+ # True when QMD is present. Reuses QmdSync.detect (PATH probe), injectable.
22
+ def qmd?(detector: QmdSync.method(:detect))
23
+ !!detector.call
24
+ end
25
+
26
+ # True when Serena is present: a `.serena` directory exists in cwd or any
27
+ # ancestor, OR `serena` is resolvable on PATH. Both probes are injectable so
28
+ # tests do not depend on the host having Serena installed.
29
+ def serena?(cwd:, path_probe: method(:which_serena), marker_finder: method(:serena_marker?))
30
+ return true if marker_finder.call(cwd)
31
+ !!path_probe.call
32
+ end
33
+
34
+ # True when `serena` is an executable on PATH. Mirrors QmdSync.which_qmd.
35
+ def which_serena
36
+ ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? do |dir|
37
+ candidate = File.join(dir, "serena")
38
+ File.file?(candidate) && File.executable?(candidate)
39
+ end
40
+ end
41
+
42
+ # Walk up from cwd to the filesystem root, returning true if any level holds a
43
+ # `.serena` directory.
44
+ def serena_marker?(cwd)
45
+ dir = File.expand_path(cwd)
46
+ loop do
47
+ return true if Dir.exist?(File.join(dir, ".serena"))
48
+ parent = File.dirname(dir)
49
+ break if parent == dir
50
+ dir = parent
51
+ end
52
+ false
53
+ end
54
+
55
+ # Obligation text for whichever tools are present, joined by newlines, or nil
56
+ # when none are. One MANDATORY line per present tool.
57
+ def mandate(cwd:, qmd_detector: QmdSync.method(:detect), serena_detector: nil)
58
+ lines = []
59
+
60
+ if qmd?(detector: qmd_detector)
61
+ lines << "MANDATORY: you MUST use QMD (`qmd search` / `qmd query` over the " \
62
+ "`plastic-*` collections) to check for an existing or related intent " \
63
+ "before treating this as new work; do not grep/Read the store first."
64
+ end
65
+
66
+ serena_present = serena_detector ? !!serena_detector.call : serena?(cwd: cwd)
67
+ if serena_present
68
+ lines << "MANDATORY: you MUST use Serena's symbolic tools (find_symbol / " \
69
+ "get_symbols_overview / find_referencing_symbols) for code navigation " \
70
+ "before grep/Read."
71
+ end
72
+
73
+ return nil if lines.empty?
74
+ lines.join("\n")
75
+ end
76
+ end
@@ -2,43 +2,56 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require_relative "qmd_sync"
5
+ require_relative "power_tools"
5
6
 
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).
7
+ # QmdHook — decision logic for the power-tools UserPromptSubmit hook (intents 66,
8
+ # 66b). Pure and dependency-injected: returns the additionalContext string to
9
+ # emit, or nil to emit nothing. The executable hook wires real deps and prints;
10
+ # this is unit-tested with a fake runner/detector (no real qmd, no network).
11
+ #
12
+ # When qmd is present it still injects scored qmd hits (intent 66), then appends
13
+ # the PowerTools mandate (a MUST obligation per present tool: qmd for finding
14
+ # intents, serena for code navigation) instead of the old soft reminder.
10
15
  module QmdHook
11
16
  module_function
12
17
 
13
18
  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
 
19
20
  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"
21
+ detector: QmdSync.method(:detect), limit: 3, min_score: 0.5,
22
+ serena_detector: nil)
23
+ serena_detector ||= -> { PowerTools.serena?(cwd: cwd) }
24
+ qmd_present = !!detector.call
25
+ serena_present = !!serena_detector.call
26
+ return nil unless qmd_present || serena_present
25
27
 
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)
28
+ p = prompt.to_s.strip
29
+ # The hit SEARCH is the only expensive step and the only one gated by prompt
30
+ # triviality: skip it for short or bare-"continue" prompts (and when qmd is
31
+ # absent). The mandate itself is always-on for whichever tools are present.
32
+ search_ok = qmd_present && p.length >= MIN_PROMPT_LENGTH && p.downcase != "continue"
29
33
 
30
34
  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]}"
35
+ if search_ok
36
+ collections = QmdSync.collections_for_cwd(cwd, plastic_home: plastic_home)
37
+ hits = QmdSync.search(p, collections: collections, limit: limit,
38
+ min_score: min_score, runner: runner, detector: detector)
39
+ if hits.any?
40
+ parts << "Related / prior Plastic intents (qmd BM25, includes completed) — " \
41
+ "check before treating this as new work:"
42
+ hits.each do |h|
43
+ loc = h[:file].to_s.sub(%r{\Aqmd://}, "")
44
+ pct = (h[:score] * 100).round
45
+ parts << "- [#{pct}%] #{loc} — #{h[:title]}"
46
+ end
47
+ parts << ""
38
48
  end
39
- parts << ""
40
49
  end
41
- parts << REMINDER
50
+
51
+ mandate = PowerTools.mandate(cwd: cwd, qmd_detector: -> { qmd_present },
52
+ serena_detector: -> { serena_present })
53
+ parts << mandate if mandate
54
+ return nil if parts.empty?
42
55
  parts.join("\n")
43
56
  end
44
57
  end
@@ -99,6 +99,27 @@ module QmdSync
99
99
  { ran: true, ok: (ok1 && ok2) }
100
100
  end
101
101
 
102
+ # Non-blocking reindex for the completion path. QMD has no incremental reindex
103
+ # (`qmd update` is a full rescan, `qmd embed -c` re-embeds the whole collection,
104
+ # ~2 min), so running it inline would block the agent's turn; spawning detached
105
+ # returns immediately. No-op when qmd absent. Spawner/detector are injected so
106
+ # tests assert behavior with no real qmd and no real spawned process.
107
+ def reindex_async(collection:, detector: method(:detect), spawner: method(:default_async_spawner))
108
+ return skip_result unless detector.call
109
+ pid = spawner.call(collection)
110
+ { ran: true, async: true, pid: pid }
111
+ end
112
+
113
+ # Default spawner: launch `qmd update && qmd embed -c <collection>` detached and
114
+ # non-blocking, discarding output, then detach so it never blocks the turn.
115
+ def default_async_spawner(collection)
116
+ require "shellwords"
117
+ cmd = "qmd update && qmd embed -c #{Shellwords.escape(collection)}"
118
+ pid = Process.spawn(cmd, out: File::NULL, err: File::NULL, pgroup: true)
119
+ Process.detach(pid)
120
+ pid
121
+ end
122
+
102
123
  # Read-only status used by doctor and the session-start report line.
103
124
  # Returns a structured hash; never mutates the index.
104
125
  def status(plastic_home:, runner: default_runner, detector: method(:detect))
package/scripts/qmd-sync CHANGED
@@ -13,7 +13,10 @@
13
13
  # qmd-sync register --store <dir> # register one store as a collection
14
14
  # qmd-sync register --all # register the global store + all projects
15
15
  # qmd-sync reindex --store <dir> # update + embed that store's collection
16
+ # qmd-sync reindex --store <dir> --async # same, detached/non-blocking
16
17
  # qmd-sync status [--format json] # read-only status
18
+ # qmd-sync search "<terms>" [--store <dir>] [--limit N] [--min-score F]
19
+ # # ranked store search; scope by --store or CWD
17
20
  #
18
21
  # --home <path> overrides the Plastic home (default: ~/.plastic).
19
22
 
@@ -71,8 +74,13 @@ when "register"
71
74
  when "reindex"
72
75
  dir = opt(ARGV, "--store") or (warn("reindex: pass --store <dir>"); exit 2)
73
76
  collection = QmdSync.collection_name(dir, plastic_home: home)
74
- res = QmdSync.reindex(collection: collection)
75
- puts "reindexed #{collection} (#{res[:ok] ? "ok" : "warn"})"
77
+ if ARGV.include?("--async")
78
+ QmdSync.reindex_async(collection: collection)
79
+ puts "reindex (async) #{collection} (started)"
80
+ else
81
+ res = QmdSync.reindex(collection: collection)
82
+ puts "reindexed #{collection} (#{res[:ok] ? "ok" : "warn"})"
83
+ end
76
84
  exit 0
77
85
 
78
86
  when "status"
@@ -86,7 +94,46 @@ when "status"
86
94
  end
87
95
  exit 0
88
96
 
97
+ when "search"
98
+ # The query is the first bareword that is NOT the value of a known value-taking
99
+ # flag. Walk ARGV, skipping each such flag and the token right after it, then
100
+ # take the first remaining token that does not start with "--".
101
+ value_flags = %w[--store --limit --min-score --home]
102
+ query = nil
103
+ i = 0
104
+ while i < ARGV.length
105
+ tok = ARGV[i]
106
+ if value_flags.include?(tok)
107
+ i += 2
108
+ next
109
+ end
110
+ unless tok.start_with?("--")
111
+ query = tok
112
+ break
113
+ end
114
+ i += 1
115
+ end
116
+ collections =
117
+ if (dir = opt(ARGV, "--store"))
118
+ [QmdSync.collection_name(dir, plastic_home: home), "plastic-global"].uniq
119
+ else
120
+ QmdSync.collections_for_cwd(Dir.pwd, plastic_home: home)
121
+ end
122
+ limit = (opt(ARGV, "--limit") || "5").to_i
123
+ min_score = (opt(ARGV, "--min-score") || "0.5").to_f
124
+ hits = QmdSync.search(query, collections: collections, limit: limit, min_score: min_score)
125
+ if hits.empty?
126
+ puts "no qmd hits"
127
+ else
128
+ hits.each do |h|
129
+ pct = (h[:score] * 100).round
130
+ path = h[:file].to_s.sub(%r{\Aqmd://}, "")
131
+ puts "[#{pct}%] #{path} - #{h[:title]}"
132
+ end
133
+ end
134
+ exit 0
135
+
89
136
  else
90
- warn "qmd-sync: unknown verb #{verb.inspect}. Use detect|register|reindex|status."
137
+ warn "qmd-sync: unknown verb #{verb.inspect}. Use detect|register|reindex|status|search."
91
138
  exit 2
92
139
  end
@@ -28,6 +28,13 @@ Work `dispatchable_queue` in `rank` order (these are `defer`/`research` disposit
28
28
  safe to deliver autonomously). Leave `human_only` and `next_big_thing` for the user — those
29
29
  are `drive`/`triage` items the human should lead. See the `plastic-dashboard` skill.
30
30
 
31
+ QMD-first (when available): when the user describes the work to deliver rather than naming an
32
+ intent, before scanning the store with grep/Read run
33
+ `ruby ~/.plastic/scripts/qmd-sync search "<terms>"` to surface candidate, prior, or duplicate
34
+ intents, then open the authoritative intent file for the hit you take over. The command is a no-op
35
+ when QMD is absent, so fall back to the existing INDEX.md / file scan. (This is discovery; the
36
+ reindex step under Completion is separate.)
37
+
31
38
  ## Arm the Lifecycle Gate (do this FIRST)
32
39
 
33
40
  Immediately after selecting the intent — before any other work — arm auto mode. This
@@ -186,11 +193,12 @@ During initial project creation, all decisions are non-destructive by definition
186
193
  - Update `chain` in the current intent's frontmatter
187
194
  6. Move intent from `## Active` to `## Completed` in INDEX.md (with today's date)
188
195
  7. Auto-commit: `cd <store-root> && git add . && git commit -m "feat: deliver intent <ID> — <name>"`
189
- 8. Refresh the QMD search index for this store (optional, no-op when QMD is absent):
196
+ 8. On completion, ALWAYS refresh the QMD search index for this store (no-op when QMD is absent).
197
+ It runs in the background so it never blocks the turn:
190
198
  ```bash
191
- ruby ~/.plastic/scripts/qmd-sync reindex --store <store-root>
199
+ ruby ~/.plastic/scripts/qmd-sync reindex --store <store-root> --async
192
200
  ```
193
- Delivery is the lifecycle event that keeps the search index fresh. `<store-root>` is the
201
+ Completion is the lifecycle event that keeps the search index fresh. `<store-root>` is the
194
202
  store that holds this intent (the global store or the project store).
195
203
  9. Disarm the lifecycle gate (auto delivery is finished):
196
204
  ```bash
@@ -202,6 +202,54 @@
202
202
  "result": "pass"
203
203
  }
204
204
  ]
205
+ },
206
+ {
207
+ "id": 12,
208
+ "scope": "behavior",
209
+ "set": "validation",
210
+ "prompt": "A power-tool is present (qmd on PATH, or a .serena marker / serena on PATH). A substantive prompt arrives in auto mode.",
211
+ "expected_output": "The UserPromptSubmit power-tools hook appends a MANDATORY obligation per present tool: a MUST-use-QMD line when qmd is present (to check for an existing or related intent before treating work as new), and a MUST-use-Serena line when serena is present (symbolic tools before grep/Read). QMD hits are still injected when above threshold.",
212
+ "files": [],
213
+ "assertions": [
214
+ {
215
+ "type": "code",
216
+ "check": "PowerTools.mandate returns MUST/MANDATORY lines for each present tool; QmdHook.run appends the mandate",
217
+ "observed": "power_tools_test.rb + qmd_hook_test.rb assert MUST wording; serena line gated on the serena detector",
218
+ "result": "pass"
219
+ }
220
+ ]
221
+ },
222
+ {
223
+ "id": 13,
224
+ "scope": "behavior",
225
+ "set": "validation",
226
+ "prompt": "Neither qmd nor serena is present (no qmd on PATH, no .serena marker, no serena on PATH). A substantive prompt arrives.",
227
+ "expected_output": "Detect-then-degrade: the hook emits nothing (silent no-op, exit 0). No mandate text appears. Nothing is required to install.",
228
+ "files": [],
229
+ "assertions": [
230
+ {
231
+ "type": "code",
232
+ "check": "PowerTools.mandate returns nil and QmdHook.run returns nil when neither tool is present",
233
+ "observed": "power_tools_test.rb test_mandate_neither_is_nil + qmd_hook_test.rb test_nil_when_neither_tool_present",
234
+ "result": "pass"
235
+ }
236
+ ]
237
+ },
238
+ {
239
+ "id": 14,
240
+ "scope": "behavior",
241
+ "set": "validation",
242
+ "prompt": "QMD is present. In auto mode the user says: deliver the work on the uploader retry policy (no intent id given).",
243
+ "expected_output": "Before scanning the store with grep/Read to find the matching intent, runs `ruby ~/.plastic/scripts/qmd-sync search \"uploader retry policy\"` to surface the candidate intent, then opens the authoritative intent file for the hit it takes over. This discovery step is distinct from the completion-time reindex step. No-op fallback to INDEX.md / file scan when QMD is absent.",
244
+ "files": [],
245
+ "assertions": [
246
+ {
247
+ "type": "human",
248
+ "check": "qmd-sync search is run before grep/Read during discovery; authoritative file opened for the hit; reindex step stays separate",
249
+ "observed": "SKILL.md (or agent file) carries the QMD-first step: run qmd-sync search before grep/Read, then open the authoritative file; no-op fallback when QMD is absent",
250
+ "result": "pass"
251
+ }
252
+ ]
205
253
  }
206
254
  ]
207
255
  }
@@ -74,6 +74,7 @@ digraph brainstorming {
74
74
  ## The Process
75
75
 
76
76
  **Understanding the idea:**
77
+ - QMD-first (when available): before scanning the store with grep/Read for prior decisions, specs, or outcomes, run `ruby ~/.plastic/scripts/qmd-sync search "<terms>"` to surface candidate, prior, or related intents, then open the authoritative intent file for any hit you act on. The command is a no-op when QMD is absent, so fall back to the existing INDEX.md / file scan.
77
78
  - Check out the current project state first (files, docs, recent commits)
78
79
  - Before asking detailed questions, assess scope: if the request describes multiple independent subsystems (e.g., "build a platform with chat, file storage, billing, and analytics"), flag this immediately. Don't spend questions refining details of a project that needs to be decomposed first.
79
80
  - If the project is too large for a single spec, help the user decompose into sub-projects: what are the independent pieces, how do they relate, what order should they be built? Then brainstorm the first sub-project through the normal design flow. Each sub-project gets its own spec → plan → implementation cycle.
@@ -0,0 +1,22 @@
1
+ {
2
+ "skill_name": "plastic-brainstorming",
3
+ "notes": "Intent 66a. Spec for the QMD-first step in the Why/explore-context phase (surface prior decisions/specs/outcomes before grep/Read). Runner is intent 76; spec only.",
4
+ "evals": [
5
+ {
6
+ "id": 1,
7
+ "scope": "behavior",
8
+ "set": "validation",
9
+ "prompt": "QMD is present. Brainstorming the active intent during the Why phase, the agent needs prior decisions and specs on caching.",
10
+ "expected_output": "In the explore-project-context (Why) step, before scanning the store with grep/Read, runs `ruby ~/.plastic/scripts/qmd-sync search \"caching decisions\"` to surface prior decisions, specs, or outcomes, then opens the authoritative intent file for any hit it acts on. No-op fallback to INDEX.md / file scan when QMD is absent.",
11
+ "files": [],
12
+ "assertions": [
13
+ {
14
+ "type": "human",
15
+ "check": "qmd-sync search is run during Why before grep/Read; authoritative file opened for any hit",
16
+ "observed": "SKILL.md (or agent file) carries the QMD-first step: run qmd-sync search before grep/Read, then open the authoritative file; no-op fallback when QMD is absent",
17
+ "result": "pass"
18
+ }
19
+ ]
20
+ }
21
+ ]
22
+ }
@@ -54,7 +54,14 @@ intents. Do not start executing work. The branches below are the only follow-ups
54
54
  ## Conditional Ledger-Resume
55
55
 
56
56
  Fires ONLY when the user explicitly asks to continue a SPECIFIC intent, or an agent is
57
- instructed to continue one. It is not part of every boot. For that intent's directory:
57
+ instructed to continue one. It is not part of every boot.
58
+
59
+ QMD-first (when available): when the user names the intent by description rather than id, before
60
+ scanning the store with grep/Read run `ruby ~/.plastic/scripts/qmd-sync search "<terms>"` to
61
+ surface the candidate intent, then open the authoritative intent file for the hit you resume. The
62
+ command is a no-op when QMD is absent, so fall back to the existing INDEX.md / file scan.
63
+
64
+ For that intent's directory:
58
65
 
59
66
  1. **Read `savepoint.md`.** It is a deterministic, append-only stage ledger (one line per
60
67
  milestone, newest at the bottom): `{utc-iso8601} {Stage} {milestone}`. The **last line =
@@ -131,6 +131,15 @@
131
131
  "assertions": [
132
132
  { "type": "convention", "check": "Determine Store step 2 documents local/project store detection via projects.yml + CWD match", "observed": "filled", "result": "pass" }
133
133
  ]
134
+ },
135
+ {
136
+ "id": 15, "scope": "behavior", "set": "validation",
137
+ "prompt": "QMD is present. The user says: continue the work on the statusline coloring (names the intent by description, not id).",
138
+ "expected_output": "Before scanning the store with grep/Read to locate the named intent, runs `ruby ~/.plastic/scripts/qmd-sync search \"statusline coloring\"` to surface the candidate intent, then opens the authoritative intent file for the hit it resumes. No-op fallback to INDEX.md / file scan when QMD is absent.",
139
+ "files": ["skills/continuing/SKILL.md"],
140
+ "assertions": [
141
+ { "type": "human", "check": "qmd-sync search is run before grep/Read when the intent is named by description; authoritative file opened for the hit", "observed": "Conditional Ledger-Resume carries the QMD-first step: run qmd-sync search before grep/Read, then open the authoritative file; no-op fallback when QMD is absent", "result": "pass" }
142
+ ]
134
143
  }
135
144
  ]
136
145
  }
@@ -41,6 +41,14 @@ When creating a tactical intent in a project store:
41
41
  - **Global:** `~/.plastic/store/`
42
42
  - **Project:** `~/.plastic/projects/{slug}/store/`
43
43
 
44
+ ### 1b. Check for a Duplicate or Predecessor (QMD-first)
45
+
46
+ QMD-first (when available): before scanning the store with grep/Read, run
47
+ `ruby ~/.plastic/scripts/qmd-sync search "<terms>"` to surface candidate, prior, or duplicate
48
+ intents, then open the authoritative intent file for any hit you act on. The command is a no-op
49
+ when QMD is absent, so fall back to the existing INDEX.md / file scan. Do this before allocating
50
+ the id so a near-duplicate can be reused and a true predecessor can be set in `--sources`.
51
+
44
52
  ### 2. Decide Branch vs Root
45
53
 
46
54
  Decide this BEFORE scaffolding, because it sets whether you pass `--parent`.
@@ -51,6 +51,22 @@
51
51
  "result": "pass"
52
52
  }
53
53
  ]
54
+ },
55
+ {
56
+ "id": 3,
57
+ "scope": "behavior",
58
+ "set": "validation",
59
+ "prompt": "QMD is present. The user says: create an intent to add retry to the uploader.",
60
+ "expected_output": "Before allocating the id / scaffolding, runs `ruby ~/.plastic/scripts/qmd-sync search \"add retry to the uploader\"` to surface a near-duplicate or true predecessor, then opens the authoritative intent file for any hit (reusing a near-duplicate or setting a real predecessor in --sources). No-op fallback to INDEX.md / file scan when QMD is absent.",
61
+ "files": [],
62
+ "assertions": [
63
+ {
64
+ "type": "human",
65
+ "check": "qmd-sync search is run before id allocation; authoritative file opened for any hit; informs reuse / --sources",
66
+ "observed": "SKILL.md (or agent file) carries the QMD-first step: run qmd-sync search before grep/Read, then open the authoritative file; no-op fallback when QMD is absent",
67
+ "result": "pass"
68
+ }
69
+ ]
54
70
  }
55
71
  ]
56
72
  }
@@ -67,6 +67,11 @@ instead of showing it.
67
67
 
68
68
  ### Step 4 — Entry flow (the board is the menu)
69
69
 
70
+ QMD-first (when available): when the user navigates by describing an intent rather than giving its
71
+ id, before scanning the store with grep/Read run `ruby ~/.plastic/scripts/qmd-sync search "<terms>"`
72
+ to surface the candidate intent, then open the authoritative intent file for the hit. The command is
73
+ a no-op when QMD is absent, so fall back to the existing INDEX.md / file scan.
74
+
70
75
  The board lists everything; the user navigates by free prose (no capped picker):
71
76
  - On the **global** board, the user replies with an **intent id** (work it), a **project
72
77
  name** (re-run `project <slug> --data` and present that board), or **"new"** (start a new
@@ -0,0 +1,22 @@
1
+ {
2
+ "skill_name": "plastic-dashboard",
3
+ "notes": "Intent 66a. Spec for the QMD-first step in the entry flow (resolve a free-prose intent reference before grep/Read). Runner is intent 76; spec only.",
4
+ "evals": [
5
+ {
6
+ "id": 1,
7
+ "scope": "behavior",
8
+ "set": "validation",
9
+ "prompt": "QMD is present. On the board, the user navigates by describing an intent (the search verb work) instead of giving its id.",
10
+ "expected_output": "Before scanning the store with grep/Read to resolve the free-prose reference, runs `ruby ~/.plastic/scripts/qmd-sync search \"search verb\"` to surface the candidate intent, then opens the authoritative intent file for the hit. No-op fallback to INDEX.md / file scan when QMD is absent.",
11
+ "files": [],
12
+ "assertions": [
13
+ {
14
+ "type": "human",
15
+ "check": "qmd-sync search is run before grep/Read when the user navigates by description; authoritative file opened for the hit",
16
+ "observed": "SKILL.md (or agent file) carries the QMD-first step: run qmd-sync search before grep/Read, then open the authoritative file; no-op fallback when QMD is absent",
17
+ "result": "pass"
18
+ }
19
+ ]
20
+ }
21
+ ]
22
+ }
@@ -75,7 +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
+ 6. On completion, ALWAYS refresh the QMD search index for this store (no-op when QMD is absent), running in the background so it never blocks the turn: `ruby ~/.plastic/scripts/qmd-sync reindex --store <store-root> --async`. Completion is the lifecycle event that keeps the search index fresh.
79
79
 
80
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.
81
81
 
@@ -101,7 +101,7 @@ Capture observations in `## Insights`. When ALL checklist items are checked:
101
101
  3. Move intent from `## Active` to `## Completed` in INDEX.md (with today's date)
102
102
  4. Update cluster entries to show `_(completed)_`
103
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.
104
+ 6. On completion, ALWAYS refresh the QMD search index for this store (no-op when QMD is absent), running in the background so it never blocks the turn: `ruby ~/.plastic/scripts/qmd-sync reindex --store <store-root> --async`. Completion is the lifecycle event that keeps the search index fresh.
105
105
 
106
106
  **This is NOT optional.** Complete the intent immediately when work is done.
107
107
 
@@ -38,4 +38,6 @@ The agent handles:
38
38
  - Cluster management (create, merge, rename)
39
39
  - Orphan detection
40
40
 
41
+ When an intent is moved to Completed, refresh the QMD index for the affected store (no-op when QMD absent), running in the background so it never blocks: `ruby ~/.plastic/scripts/qmd-sync reindex --store <store-root> --async`.
42
+
41
43
  After the agent completes, report what changed.
@@ -0,0 +1,22 @@
1
+ {
2
+ "skill_name": "plastic-intent-curator",
3
+ "notes": "Intent 66a. Spec for the QMD-first discovery step in agents/plastic-intent-curator.md (locate a specific intent before grep/find; distinct from the completion-time reindex step). Runner is intent 76; spec only.",
4
+ "evals": [
5
+ {
6
+ "id": 1,
7
+ "scope": "behavior",
8
+ "set": "validation",
9
+ "prompt": "QMD is present. The user says: find and reclassify the intent about orphan detection.",
10
+ "expected_output": "During discovery (How You Work), before scanning the store with grep/find to locate the intent, runs `ruby ~/.plastic/scripts/qmd-sync search \"orphan detection\"` to surface the candidate or related intents, then opens the authoritative intent file for the hit it acts on. This discovery step is distinct from the completion-time reindex step. No-op fallback to the full scan when QMD is absent.",
11
+ "files": [],
12
+ "assertions": [
13
+ {
14
+ "type": "human",
15
+ "check": "qmd-sync search is run before grep/find during discovery; authoritative file opened for the hit; reindex step stays separate",
16
+ "observed": "SKILL.md (or agent file) carries the QMD-first step: run qmd-sync search before grep/Read, then open the authoritative file; no-op fallback when QMD is absent",
17
+ "result": "pass"
18
+ }
19
+ ]
20
+ }
21
+ ]
22
+ }
@@ -41,6 +41,12 @@ tags: [plastic, project-reddit-kb]
41
41
  ## Workflow
42
42
 
43
43
  ### 1. Identify Intents to Connect
44
+
45
+ QMD-first (when available): before scanning the store with grep/Read, run
46
+ `ruby ~/.plastic/scripts/qmd-sync search "<terms>"` to surface candidate, prior, or related
47
+ intents to propose as sources/chain, then open the authoritative intent file for any hit you act
48
+ on. The command is a no-op when QMD is absent, so fall back to the directory scan below.
49
+
44
50
  Show existing intents by scanning the store's directory for intent files:
45
51
  ```bash
46
52
  for dir in $STORE_ROOT/store/*/; do
@@ -0,0 +1,22 @@
1
+ {
2
+ "skill_name": "plastic-linking-intents",
3
+ "notes": "Intent 66a. Spec for the QMD-first discovery step (surface candidate/related intents to propose as sources/chain before grep/Read). Runner is intent 76; this case is a spec, not executed.",
4
+ "evals": [
5
+ {
6
+ "id": 1,
7
+ "scope": "behavior",
8
+ "set": "validation",
9
+ "prompt": "QMD is present. The user says: link this intent to the related upload-pipeline work.",
10
+ "expected_output": "Before scanning the store directory with grep/Read to identify related intents, runs `ruby ~/.plastic/scripts/qmd-sync search \"upload pipeline\"` to surface candidate or related intents to propose as sources/chain, then opens the authoritative intent file for any hit it links. No-op fallback to the directory scan when QMD is absent.",
11
+ "files": [],
12
+ "assertions": [
13
+ {
14
+ "type": "human",
15
+ "check": "qmd-sync search is run before the directory scan; authoritative file opened for any hit before proposing sources/chain",
16
+ "observed": "SKILL.md (or agent file) carries the QMD-first step: run qmd-sync search before grep/Read, then open the authoritative file; no-op fallback when QMD is absent",
17
+ "result": "pass"
18
+ }
19
+ ]
20
+ }
21
+ ]
22
+ }
@@ -43,6 +43,12 @@ All completed intents with dates. Links preserved, never deleted.
43
43
 
44
44
  ## Workflow
45
45
 
46
+ QMD-first (when available): when you need to locate a specific intent (to reclassify, flag, or
47
+ cluster it) rather than rebuild every section, before scanning the store with grep/Read run
48
+ `ruby ~/.plastic/scripts/qmd-sync search "<terms>"` to surface candidate or related intents, then
49
+ open the authoritative intent file for any hit you act on. The command is a no-op when QMD is
50
+ absent, so fall back to the directory scan below.
51
+
46
52
  ### Rebuild Sections
47
53
  Scan the active store's `store/` directory for intent files and rebuild each section:
48
54
 
@@ -0,0 +1,22 @@
1
+ {
2
+ "skill_name": "plastic-managing-index",
3
+ "notes": "Intent 66a. Spec for the QMD-first step when locating a specific intent to reclassify/flag/cluster before grep/Read. Runner is intent 76; spec only.",
4
+ "evals": [
5
+ {
6
+ "id": 1,
7
+ "scope": "behavior",
8
+ "set": "validation",
9
+ "prompt": "QMD is present. The user says: move the intent about the statusline into the Completed section.",
10
+ "expected_output": "When locating one specific intent rather than rebuilding every section, before scanning the store with grep/Read runs `ruby ~/.plastic/scripts/qmd-sync search \"statusline\"` to surface the candidate intent, then opens the authoritative intent file for the hit it reclassifies. No-op fallback to the directory scan when QMD is absent.",
11
+ "files": [],
12
+ "assertions": [
13
+ {
14
+ "type": "human",
15
+ "check": "qmd-sync search is run before the directory scan when locating a specific intent; authoritative file opened for the hit",
16
+ "observed": "SKILL.md (or agent file) carries the QMD-first step: run qmd-sync search before grep/Read, then open the authoritative file; no-op fallback when QMD is absent",
17
+ "result": "pass"
18
+ }
19
+ ]
20
+ }
21
+ ]
22
+ }
@@ -17,6 +17,14 @@ Before proceeding, resolve the active intent:
17
17
  2. **Find active intent:** Read `INDEX.md` from the detected store. Look under `## Active`. If exactly one → use it. If multiple → ask which. If none → refuse: "No active intent. Create one first with /plastic-creating-intent"
18
18
  3. **Resolve intent directory:** `{store}/store/{id}--{slug}/`
19
19
 
20
+ ## Check Prior Work First
21
+
22
+ QMD-first (when available): before scanning the store with grep/Read or searching the web, run
23
+ `ruby ~/.plastic/scripts/qmd-sync search "<terms>"` to surface candidate, prior, or related intents
24
+ and existing research, then open the authoritative intent file for any hit you act on. Reusing a
25
+ prior report beats re-deriving it. The command is a no-op when QMD is absent, so fall back to the
26
+ existing INDEX.md / file scan.
27
+
20
28
  ## Depth Decision
21
29
 
22
30
  Before starting research, assess the question against these criteria:
@@ -0,0 +1,22 @@
1
+ {
2
+ "skill_name": "plastic-research",
3
+ "notes": "Intent 66a. Spec for the QMD-first step before researching (reuse prior intents/reports before grep/Read or web search). Runner is intent 76; spec only.",
4
+ "evals": [
5
+ {
6
+ "id": 1,
7
+ "scope": "behavior",
8
+ "set": "validation",
9
+ "prompt": "QMD is present. The user asks the agent to research installer patterns for the active intent.",
10
+ "expected_output": "Before scanning the store with grep/Read or searching the web, runs `ruby ~/.plastic/scripts/qmd-sync search \"installer patterns\"` to surface candidate, prior, or related intents and existing research, then opens the authoritative intent file for any hit it reuses. No-op fallback to INDEX.md / file scan when QMD is absent.",
11
+ "files": [],
12
+ "assertions": [
13
+ {
14
+ "type": "human",
15
+ "check": "qmd-sync search is run before grep/Read and web search; authoritative file opened for any prior report reused",
16
+ "observed": "SKILL.md (or agent file) carries the QMD-first step: run qmd-sync search before grep/Read, then open the authoritative file; no-op fallback when QMD is absent",
17
+ "result": "pass"
18
+ }
19
+ ]
20
+ }
21
+ ]
22
+ }