@zalom/plastic 1.0.0-beta.3 → 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.
- package/agents/plastic-intent-curator.md +4 -2
- package/hooks/statusline +126 -42
- package/package.json +1 -1
- package/scripts/lib/power_tools.rb +76 -0
- package/scripts/lib/qmd_hook.rb +38 -25
- package/scripts/lib/qmd_sync.rb +21 -0
- package/scripts/qmd-sync +50 -3
- package/skills/auto/SKILL.md +11 -3
- package/skills/auto/evals/evals.json +48 -0
- package/skills/brainstorming/SKILL.md +1 -0
- package/skills/brainstorming/evals/evals.json +22 -0
- package/skills/continuing/SKILL.md +8 -1
- package/skills/continuing/evals/evals.json +9 -0
- package/skills/creating-intent/SKILL.md +8 -0
- package/skills/creating-intent/evals/evals.json +16 -0
- package/skills/dashboard/SKILL.md +5 -0
- package/skills/dashboard/evals/evals.json +22 -0
- package/skills/executing-plan/SKILL.md +2 -2
- package/skills/intent-curator/SKILL.md +2 -0
- package/skills/intent-curator/evals/evals.json +22 -0
- package/skills/linking-intents/SKILL.md +6 -0
- package/skills/linking-intents/evals/evals.json +22 -0
- package/skills/managing-index/SKILL.md +6 -0
- package/skills/managing-index/evals/evals.json +22 -0
- package/skills/research/SKILL.md +8 -0
- package/skills/research/evals/evals.json +22 -0
|
@@ -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.
|
|
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
|
-
-
|
|
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/hooks/statusline
CHANGED
|
@@ -1,56 +1,140 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# plastic-hook-version:
|
|
3
|
-
# StatusLine hook
|
|
2
|
+
# plastic-hook-version: 3.0.0
|
|
3
|
+
# StatusLine hook - Plastic owns the full statusline (no chaining).
|
|
4
|
+
# Renders: {model} - Plastic {version}[ -> {next}] - {path} - [{project}: {id} {title}]
|
|
5
|
+
# Pure bash (macOS 3.2 compatible). No ruby, no jq. Reads stdin once; small file reads only.
|
|
4
6
|
|
|
5
7
|
INPUT=$(cat)
|
|
6
8
|
|
|
7
|
-
# ---
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
# --- Colors (ANSI-C quoting; bash 3.2 supports $'\033') ---
|
|
10
|
+
C_MODEL=$'\033[38;2;90;140;220m' # navy blue - model (lightened for legibility)
|
|
11
|
+
C_VER=$'\033[37m' # white - "Plastic {version}" and update arrow
|
|
12
|
+
C_NEXT=$'\033[33m' # yellow - next version
|
|
13
|
+
C_PATH=$'\033[38;2;128;134;144m' # gray - path (matches dim UI text)
|
|
14
|
+
C_BRACK=$'\033[33m' # yellow - work-unit brackets
|
|
15
|
+
C_PROJ=$'\033[38;2;249;140;30m' # orange - project name
|
|
16
|
+
C_ID=$'\033[38;2;45;212;191m' # teal - intent id (accent between orange + yellow)
|
|
17
|
+
C_INTENT=$'\033[33m' # yellow - intent title
|
|
18
|
+
C_SEP=$'\033[38;2;128;134;144m' # gray - separators
|
|
19
|
+
RST=$'\033[0m'
|
|
20
|
+
SEP="${C_SEP} "$'\302\267'" ${RST}" # " . " middot, dim
|
|
19
21
|
|
|
20
|
-
# ---
|
|
21
|
-
|
|
22
|
+
# --- Extract a flat JSON string field from stdin: json_str <key> ---
|
|
23
|
+
json_str() {
|
|
24
|
+
printf '%s' "$INPUT" \
|
|
25
|
+
| grep -o "\"$1\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" \
|
|
26
|
+
| head -1 \
|
|
27
|
+
| sed "s/.*\"$1\"[[:space:]]*:[[:space:]]*\"//;s/\"$//"
|
|
28
|
+
}
|
|
22
29
|
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
if [ -f "$INDEX" ]; then
|
|
26
|
-
ACTIVE=$(sed -n '/^## Active$/,/^## /{/^- \[/p;}' "$INDEX" | head -1)
|
|
27
|
-
if [ -n "$ACTIVE" ]; then
|
|
28
|
-
INTENT=$(echo "$ACTIVE" | sed 's/^- \[\([^]]*\)\].*/\1/')
|
|
29
|
-
PLASTIC_PARTS="plastic: ${INTENT}"
|
|
30
|
-
fi
|
|
31
|
-
fi
|
|
30
|
+
# --- Model (model.display_name) ---
|
|
31
|
+
MODEL=$(json_str "display_name")
|
|
32
32
|
|
|
33
|
-
#
|
|
33
|
+
# --- Path (workspace.current_dir, fallback top-level cwd) ---
|
|
34
|
+
REAL_CWD=$(json_str "current_dir")
|
|
35
|
+
[ -z "$REAL_CWD" ] && REAL_CWD=$(json_str "cwd")
|
|
36
|
+
# Abbreviate $HOME -> ~
|
|
37
|
+
CWD="$REAL_CWD"
|
|
38
|
+
case "$CWD" in
|
|
39
|
+
"$HOME") CWD="~" ;;
|
|
40
|
+
"$HOME"/*) CWD="~${CWD#"$HOME"}" ;;
|
|
41
|
+
esac
|
|
42
|
+
|
|
43
|
+
# --- Plastic version ---
|
|
44
|
+
VERSION=""
|
|
45
|
+
[ -f "$HOME/.plastic/VERSION" ] && VERSION=$(tr -d ' \t\n' < "$HOME/.plastic/VERSION")
|
|
46
|
+
|
|
47
|
+
# --- Update (next version when one is available) ---
|
|
48
|
+
NEXT=""
|
|
34
49
|
CACHE_FILE="$HOME/.plastic/.cache/update-check.json"
|
|
35
50
|
if [ -f "$CACHE_FILE" ]; then
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
PLASTIC_PARTS="$UPDATE_MSG"
|
|
51
|
+
if grep -q '"updateAvailable"[[:space:]]*:[[:space:]]*true' "$CACHE_FILE" 2>/dev/null; then
|
|
52
|
+
LATEST=$(grep -o '"latest"[[:space:]]*:[[:space:]]*"[^"]*"' "$CACHE_FILE" 2>/dev/null \
|
|
53
|
+
| head -1 | sed 's/.*"latest"[[:space:]]*:[[:space:]]*"//;s/"$//')
|
|
54
|
+
if [ -n "$LATEST" ]; then
|
|
55
|
+
case "$LATEST" in
|
|
56
|
+
*-*) NEXT="${LATEST##*-}" ;; # 1.0.0-beta.3 -> beta.3
|
|
57
|
+
*) NEXT="$LATEST" ;;
|
|
58
|
+
esac
|
|
45
59
|
fi
|
|
46
60
|
fi
|
|
47
61
|
fi
|
|
48
62
|
|
|
49
|
-
# ---
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
# --- Scope resolution: cwd -> registered project store, else global ---
|
|
64
|
+
# Match the longest registered project path that prefixes the real cwd.
|
|
65
|
+
PROJECTS_YML="$HOME/.plastic/projects.yml"
|
|
66
|
+
SLUG=""
|
|
67
|
+
if [ -n "$REAL_CWD" ] && [ -f "$PROJECTS_YML" ]; then
|
|
68
|
+
SLUG=$(awk -v cwd="$REAL_CWD" '
|
|
69
|
+
/^ [A-Za-z0-9_.-]+:[[:space:]]*$/ { s=$1; sub(/:$/,"",s); next }
|
|
70
|
+
/^ path:/ {
|
|
71
|
+
p=$0; sub(/^[[:space:]]*path:[[:space:]]*/,"",p); gsub(/"/,"",p);
|
|
72
|
+
if (length(p)>0 && (cwd==p || index(cwd, p"/")==1) && length(p)>best) { best=length(p); win=s }
|
|
73
|
+
}
|
|
74
|
+
END { if (win!="") print win }
|
|
75
|
+
' "$PROJECTS_YML")
|
|
56
76
|
fi
|
|
77
|
+
|
|
78
|
+
if [ -n "$SLUG" ]; then
|
|
79
|
+
INDEX="$HOME/.plastic/projects/$SLUG/INDEX.md"
|
|
80
|
+
PROJ_LABEL="$SLUG"
|
|
81
|
+
else
|
|
82
|
+
INDEX="$HOME/.plastic/INDEX.md"
|
|
83
|
+
PROJ_LABEL="global"
|
|
84
|
+
fi
|
|
85
|
+
INDEX_DIR=$(dirname "$INDEX")
|
|
86
|
+
|
|
87
|
+
# --- Most-recently-accessed active intent (mirror dashboard: last savepoint ts) ---
|
|
88
|
+
WORK=""
|
|
89
|
+
if [ -f "$INDEX" ]; then
|
|
90
|
+
BEST_TS=""
|
|
91
|
+
while IFS= read -r line; do
|
|
92
|
+
[ -z "$line" ] && continue
|
|
93
|
+
inside=${line#*[}; inside=${inside%%]*} # "{id} - {title}"
|
|
94
|
+
dir=${line#*](store/}; dir=${dir%%/*} # intent directory name
|
|
95
|
+
[ -z "$dir" ] && continue
|
|
96
|
+
sp="$INDEX_DIR/store/$dir/savepoint.md"
|
|
97
|
+
ts=""
|
|
98
|
+
if [ -f "$sp" ]; then
|
|
99
|
+
ts=$(grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z' "$sp" 2>/dev/null | tail -1)
|
|
100
|
+
fi
|
|
101
|
+
# ISO8601 sorts lexicographically; keep the newest. Empty ts sorts lowest.
|
|
102
|
+
if [ -z "$BEST_TS" ] || { [ -n "$ts" ] && [ "$ts" \> "$BEST_TS" ]; }; then
|
|
103
|
+
BEST_TS="$ts"
|
|
104
|
+
WORK="$inside"
|
|
105
|
+
fi
|
|
106
|
+
done <<EOF
|
|
107
|
+
$(sed -n '/^## Active$/,/^## /{/^- \[/p;}' "$INDEX")
|
|
108
|
+
EOF
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# --- Build the work-unit "[{project}: {id} {title}]" with title truncation ---
|
|
112
|
+
WORK_SEG=""
|
|
113
|
+
if [ -n "$WORK" ]; then
|
|
114
|
+
# Split "{id} - {title}" on the em-dash (U+2014).
|
|
115
|
+
EMDASH=$'\342\200\224'
|
|
116
|
+
id=${WORK%% $EMDASH *}
|
|
117
|
+
title=${WORK#* $EMDASH }
|
|
118
|
+
# Fallback if the em-dash form is absent: split on first space.
|
|
119
|
+
if [ "$id" = "$WORK" ]; then id=${WORK%% *}; title=${WORK#* }; fi
|
|
120
|
+
# Truncate long titles (~40 chars).
|
|
121
|
+
if [ ${#title} -gt 40 ]; then title="$(printf '%.40s' "$title")..."; fi
|
|
122
|
+
WORK_SEG="${C_BRACK}[${RST}${C_PROJ}${PROJ_LABEL}${RST}${C_INTENT}: ${RST}${C_ID}${id}${RST}${C_INTENT} ${title}${RST}${C_BRACK}]${RST}"
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# --- Assemble segments in order, joined by " . " ---
|
|
126
|
+
PARTS=""
|
|
127
|
+
add() { [ -z "$1" ] && return; if [ -z "$PARTS" ]; then PARTS="$1"; else PARTS="$PARTS$SEP$1"; fi; }
|
|
128
|
+
|
|
129
|
+
# Order: stable values first (model, work-unit), then the volatile-length
|
|
130
|
+
# values last (version, path) so the line does not visually jump as they change.
|
|
131
|
+
[ -n "$MODEL" ] && add "${C_MODEL}${MODEL}${RST}"
|
|
132
|
+
[ -n "$WORK_SEG" ] && add "$WORK_SEG"
|
|
133
|
+
if [ -n "$VERSION" ]; then
|
|
134
|
+
vseg="${C_VER}Plastic ${VERSION}${RST}"
|
|
135
|
+
[ -n "$NEXT" ] && vseg="${vseg}${C_VER} "$'\342\206\222'" ${RST}${C_NEXT}${NEXT}${RST}"
|
|
136
|
+
add "$vseg"
|
|
137
|
+
fi
|
|
138
|
+
[ -n "$CWD" ] && add "${C_PATH}${CWD}${RST}"
|
|
139
|
+
|
|
140
|
+
printf '%s\n' "$PARTS"
|
package/package.json
CHANGED
|
@@ -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
|
package/scripts/lib/qmd_hook.rb
CHANGED
|
@@ -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
|
|
7
|
-
# Pure and dependency-injected: returns the additionalContext string to
|
|
8
|
-
# nil to emit nothing. The executable hook wires real deps and prints;
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
package/scripts/lib/qmd_sync.rb
CHANGED
|
@@ -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
|
-
|
|
75
|
-
|
|
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
|
package/skills/auto/SKILL.md
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
}
|
package/skills/research/SKILL.md
CHANGED
|
@@ -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
|
+
}
|