@zalom/plastic 1.0.0-beta.9 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PLASTIC-reference.md +133 -0
- package/PLASTIC.md +398 -82
- package/README.md +99 -82
- package/agents/plastic-brainstorming.md +12 -9
- package/agents/plastic-enforcer.md +52 -12
- package/agents/plastic-executor.md +10 -10
- package/agents/plastic-future-intent-researcher.md +4 -8
- package/agents/plastic-intent-curator.md +12 -14
- package/agents/plastic-intent-discovery.md +35 -0
- package/agents/plastic-planner.md +15 -10
- package/agents/plastic-spec-specialist.md +15 -10
- package/bin/plastic.js +7 -3
- package/hooks/code-gate +10 -1
- package/hooks/hooks.json +28 -3
- package/hooks/lock-gate +21 -0
- package/hooks/retrieval-gate +10 -0
- package/hooks/savepoint-pre +10 -0
- package/hooks/statusline +48 -6
- package/package.json +2 -1
- package/scripts/agent-report +21 -0
- package/scripts/dashboard.rb +98 -23
- package/scripts/doctor.rb +232 -5
- package/scripts/hook-auto-arm +1 -1
- package/scripts/hook-bash-gate +22 -4
- package/scripts/hook-code-gate +11 -6
- package/scripts/hook-continue +17 -0
- package/scripts/hook-create-gate +45 -9
- package/scripts/hook-gate-check +24 -23
- package/scripts/hook-lock-gate +83 -0
- package/scripts/hook-retrieval-gate +148 -0
- package/scripts/hook-savepoint-pre +32 -0
- package/scripts/hook-session-start +1 -1
- package/scripts/insight-append +51 -0
- package/scripts/install.rb +44 -7
- package/scripts/lib/agent_models.rb +43 -0
- package/scripts/lib/bridge.rb +924 -84
- package/scripts/lib/dashboard_banner.rb +42 -0
- package/scripts/lib/hook_registry.rb +95 -0
- package/scripts/lib/insights.rb +86 -0
- package/scripts/lib/installer_core.rb +100 -60
- package/scripts/lib/link_suggestions.rb +319 -0
- package/scripts/lib/lock.rb +375 -0
- package/scripts/lib/power_tools.rb +22 -21
- package/scripts/lib/preflight.rb +79 -0
- package/scripts/lib/qmd_sync.rb +15 -0
- package/scripts/lib/retrieval_gate.rb +211 -0
- package/scripts/lib/worktree.rb +384 -0
- package/scripts/link-suggest +213 -0
- package/scripts/new-intent +9 -1
- package/scripts/plastic-lock +164 -0
- package/scripts/read-config +4 -0
- package/scripts/spawn-preamble +11 -3
- package/scripts/update.rb +16 -7
- package/skills/auto/SKILL.md +126 -19
- package/skills/auto/references/agent-architecture.md +7 -4
- package/skills/auto/references/agent-report-contract.md +36 -1
- package/skills/auto/references/end-tail.md +56 -0
- package/skills/auto/references/human-report-contract.md +55 -0
- package/skills/auto/references/tiers.md +77 -0
- package/skills/brainstorming/SKILL.md +7 -34
- package/skills/brainstorming/references/design-principles.md +49 -0
- package/skills/continuing/SKILL.md +26 -7
- package/skills/creating-intent/SKILL.md +13 -28
- package/skills/creating-project/SKILL.md +11 -74
- package/skills/creating-project/references/project-scaffolding.md +97 -0
- package/skills/creating-skills/SKILL.md +65 -0
- package/skills/creating-skills/evals/evals.json +108 -0
- package/skills/creating-skills/references/agents.md +168 -0
- package/skills/creating-skills/references/evals.md +41 -0
- package/skills/creating-skills/references/hooks.md +248 -0
- package/skills/creating-skills/references/progressive-disclosure.md +176 -0
- package/skills/creating-skills/references/scripts.md +166 -0
- package/skills/creating-skills/references/skills.md +165 -0
- package/skills/creating-skills/scripts/scaffold.rb +313 -0
- package/skills/dashboard/SKILL.md +13 -11
- package/skills/dashboard/references/classification.md +22 -0
- package/skills/doctor/SKILL.md +10 -7
- package/skills/executing-plan/SKILL.md +4 -4
- package/skills/humanizer/SKILL.md +39 -0
- package/skills/humanizer/evals/evals.json +70 -0
- package/skills/humanizer/references/always-on-snippet.md +9 -0
- package/skills/humanizer/references/examples.md +48 -0
- package/skills/install/SKILL.md +75 -84
- package/skills/intent-curator/SKILL.md +5 -1
- package/skills/intent-discovery/SKILL.md +47 -0
- package/skills/intent-starting/SKILL.md +130 -0
- package/skills/intent-starting/evals/evals.json +117 -0
- package/skills/intent-starting/references/boarding-matrix.md +35 -0
- package/skills/linking-intents/SKILL.md +48 -12
- package/skills/lock/SKILL.md +41 -0
- package/skills/managing-index/SKILL.md +6 -0
- package/skills/releasing/SKILL.md +44 -29
- package/skills/releasing/references/promotion-and-tagging.md +60 -0
- package/skills/roadmap/SKILL.md +51 -0
- package/skills/roadmap/references/file-format.md +91 -0
- package/skills/roadmap/references/operations.md +78 -0
- package/skills/uninstall/SKILL.md +29 -11
- package/skills/update/SKILL.md +34 -23
- package/skills/versions/SKILL.md +27 -12
- package/skills/writing-plans/SKILL.md +10 -88
- package/skills/writing-plans/references/plan-format.md +102 -0
- package/templates/config.yml +8 -0
- package/templates/outcome.md +3 -0
- package/templates/revisions.md +58 -0
- package/templates/roadmap.md +30 -0
- package/skills/writing-instructions/SKILL.md +0 -159
- package/skills/writing-instructions/references/agentskills-spec.md +0 -135
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
require_relative "qmd_sync"
|
|
5
5
|
|
|
6
6
|
# PowerTools — detect-then-degrade harness for Plastic's optional power-tools
|
|
7
|
-
# (intent 66b
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# code navigation.
|
|
7
|
+
# (intent 66b; demoted to recommendations in intent 108, D8). It owns
|
|
8
|
+
# deterministic detection of each tool and builds a RECOMMENDATION string for
|
|
9
|
+
# whichever tools are present, so the agent is reminded (not obliged) to prefer
|
|
10
|
+
# them: QMD for finding intents, Serena for code navigation.
|
|
11
11
|
#
|
|
12
12
|
# Strictly detect-then-degrade: a tool that is absent contributes nothing, and
|
|
13
13
|
# `mandate` returns nil when no tool is present. Nothing here installs anything.
|
|
@@ -52,25 +52,26 @@ module PowerTools
|
|
|
52
52
|
false
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
55
|
+
QMD_OBLIGATION = "prefer `qmd search` / `qmd query` over the `plastic-*` " \
|
|
56
|
+
"collections to check for existing or related intents before " \
|
|
57
|
+
"treating work as new"
|
|
58
|
+
SERENA_OBLIGATION = "prefer its symbolic tools (find_symbol / get_symbols_overview / " \
|
|
59
|
+
"find_referencing_symbols) for code navigation"
|
|
65
60
|
|
|
61
|
+
# Recommendation text for whichever tools are present, or nil when none are.
|
|
62
|
+
# Both present collapse to ONE combined line naming both obligations (no
|
|
63
|
+
# embedded newline); one present returns that tool's own line; neither
|
|
64
|
+
# returns nil.
|
|
65
|
+
def mandate(cwd:, qmd_detector: QmdSync.method(:detect), serena_detector: nil)
|
|
66
|
+
qmd_present = qmd?(detector: qmd_detector)
|
|
66
67
|
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
68
|
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
if qmd_present && serena_present
|
|
70
|
+
"QMD and Serena are available: #{QMD_OBLIGATION}, and #{SERENA_OBLIGATION}."
|
|
71
|
+
elsif qmd_present
|
|
72
|
+
"QMD is available: #{QMD_OBLIGATION}."
|
|
73
|
+
elsif serena_present
|
|
74
|
+
"Serena is available: #{SERENA_OBLIGATION}."
|
|
75
|
+
end
|
|
75
76
|
end
|
|
76
77
|
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "rubygems"
|
|
5
|
+
|
|
6
|
+
# Pure, dependency-injected pre-flight checks for Plastic's runtime dependencies
|
|
7
|
+
# (intent 38). Takes injected probes (ruby version, node version, git presence,
|
|
8
|
+
# mise presence) and returns a plain decision: ok / fatal plus branded messages.
|
|
9
|
+
#
|
|
10
|
+
# No I/O, no shelling out, no ENV reads here. Callers (scripts/install.rb,
|
|
11
|
+
# bin/plastic.js) own the impure probing and the printing, so this module stays
|
|
12
|
+
# hermetically testable. Voice matches boot_banner.rb (understated, "Plastic ..."
|
|
13
|
+
# prefix); no em-dash, no en-dash in any message.
|
|
14
|
+
module Preflight
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
RUBY_FLOOR = "3.0.0"
|
|
18
|
+
NODE_FLOOR = 18
|
|
19
|
+
RUBY_PIN = "3.3"
|
|
20
|
+
|
|
21
|
+
def check(ruby_version:, node_version:, git_present:, mise_present:)
|
|
22
|
+
messages = []
|
|
23
|
+
|
|
24
|
+
ruby_message = ruby_issue(ruby_version, mise_present)
|
|
25
|
+
fatal = !ruby_message.nil?
|
|
26
|
+
messages << ruby_message if ruby_message
|
|
27
|
+
|
|
28
|
+
node_message = node_issue(node_version)
|
|
29
|
+
messages << node_message if node_message
|
|
30
|
+
|
|
31
|
+
git_message = git_issue(git_present)
|
|
32
|
+
messages << git_message if git_message
|
|
33
|
+
|
|
34
|
+
{ ok: messages.empty?, fatal: fatal, messages: messages }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def ruby_issue(ruby_version, mise_present)
|
|
38
|
+
parsed = safe_version(ruby_version)
|
|
39
|
+
return nil if parsed && parsed >= safe_version(RUBY_FLOOR)
|
|
40
|
+
|
|
41
|
+
lines = []
|
|
42
|
+
lines << "Plastic needs Ruby #{RUBY_FLOOR} or newer to run its scripts (found #{found(ruby_version)})."
|
|
43
|
+
lines << "Install a pinned Ruby with mise:"
|
|
44
|
+
lines << " curl https://mise.run | sh # only if mise is not installed yet" unless mise_present
|
|
45
|
+
lines << " mise use --global ruby@#{RUBY_PIN}"
|
|
46
|
+
lines << "Then re-run the Plastic installer."
|
|
47
|
+
lines.join("\n")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def node_issue(node_version)
|
|
51
|
+
parsed = safe_version(strip_leading_v(node_version))
|
|
52
|
+
return nil if parsed && parsed >= safe_version(NODE_FLOOR.to_s)
|
|
53
|
+
|
|
54
|
+
"Plastic works best on Node #{NODE_FLOOR} or newer (found #{found(node_version)}). " \
|
|
55
|
+
"Pin it with mise: mise use --global node@25"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def git_issue(git_present)
|
|
59
|
+
return nil if git_present
|
|
60
|
+
|
|
61
|
+
"Plastic uses git for its store and worktrees (git was not found). " \
|
|
62
|
+
"Install git, e.g. macOS: xcode-select --install"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def safe_version(str)
|
|
66
|
+
Gem::Version.new(str.to_s)
|
|
67
|
+
rescue ArgumentError
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def strip_leading_v(str)
|
|
72
|
+
str.to_s.strip.sub(/\Av/, "")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def found(value)
|
|
76
|
+
text = value.to_s.strip
|
|
77
|
+
text.empty? ? "not found" : text
|
|
78
|
+
end
|
|
79
|
+
end
|
package/scripts/lib/qmd_sync.rb
CHANGED
|
@@ -120,6 +120,21 @@ module QmdSync
|
|
|
120
120
|
pid
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
+
# True when the QMD index has no pending (unembedded) documents. Binary
|
|
124
|
+
# freshness signal for the retrieval gate (intent 84, Lever 2). `qmd status` is
|
|
125
|
+
# plain text (no --json); it prints a line like "Pending: N need embedding".
|
|
126
|
+
# No pending line found -> treat as fresh (conservative: a parse miss must not
|
|
127
|
+
# block reads). Runner failure -> false (cannot confirm freshness). The caller
|
|
128
|
+
# gates on `detect` first, so absence is handled upstream; this only answers
|
|
129
|
+
# "is the present index fresh?". Pure via the injected runner.
|
|
130
|
+
def self.fresh?(runner: default_runner)
|
|
131
|
+
out, ok = runner.call(["status"])
|
|
132
|
+
return false unless ok && out
|
|
133
|
+
m = out[/^\s*Pending:\s*(\d+)\b/i, 1]
|
|
134
|
+
pending = m ? m.to_i : 0
|
|
135
|
+
pending.zero?
|
|
136
|
+
end
|
|
137
|
+
|
|
123
138
|
# Read-only status used by doctor and the session-start report line.
|
|
124
139
|
# Returns a structured hash; never mutates the index.
|
|
125
140
|
def status(plastic_home:, runner: default_runner, detector: method(:detect))
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "bridge"
|
|
5
|
+
|
|
6
|
+
# RetrievalGate — the single, pure decision for Lever 2 of intent 84, redesigned
|
|
7
|
+
# operation-based in intent 89a.
|
|
8
|
+
#
|
|
9
|
+
# Given an agent tool call (Bash/Read/Grep/Glob) and injected capability signals,
|
|
10
|
+
# it decides whether to BLOCK the call (returning a redirect-to-QMD reason String)
|
|
11
|
+
# or ALLOW it (returning nil). All capability/freshness signals are injected by the
|
|
12
|
+
# caller (the hook); this module shells out to nothing, reads no globals, and runs
|
|
13
|
+
# no binaries. Mirrors bridge.rb's decision-fn convention (reason String to block,
|
|
14
|
+
# nil to allow).
|
|
15
|
+
#
|
|
16
|
+
# Operation-based policy (intent 89, ## Redesign):
|
|
17
|
+
# - The gate distinguishes DISCOVERY (content search) from READING a known target.
|
|
18
|
+
# - Only CONTENT SEARCH over store markdown is hard-gated -> QMD.
|
|
19
|
+
# - Reading a known target (Read, cat/head/tail) and structural discovery (Glob,
|
|
20
|
+
# find, ls) are ALWAYS allowed, including over the store.
|
|
21
|
+
# - Code navigation is a soft prompt MANDATE (PowerTools / UserPromptSubmit), not a
|
|
22
|
+
# hard gate here. Content grep over code is allowed (Serena cannot grep strings).
|
|
23
|
+
#
|
|
24
|
+
# Content-search vectors (the only ones that can be gated):
|
|
25
|
+
# - the Grep tool (its `path` search root)
|
|
26
|
+
# - bash `grep`/`rg`/`ag` (their path args; the first bareword is the PATTERN)
|
|
27
|
+
#
|
|
28
|
+
# QMD enforcement is BINARY (no advisory tier):
|
|
29
|
+
# - store-md content search: QMD detected+fresh -> BLOCK; detected+stale -> fire
|
|
30
|
+
# reindex, ALLOW this turn; absent/broken -> ALLOW (the hook warns on broken).
|
|
31
|
+
#
|
|
32
|
+
# Bypass: a TRAILING `# qmd-ok` shell comment on a Bash command (not a substring; a
|
|
33
|
+
# quoted/echoed occurrence does not bypass). It is the auditable seam for "I tried
|
|
34
|
+
# discovery and it did not serve me" (empty, low, or wrongly-scored results).
|
|
35
|
+
#
|
|
36
|
+
# Scope: only the agent's own tool calls. Ruby `File.read` inside scripts is invisible
|
|
37
|
+
# to a PreToolUse hook and is explicitly out of scope (no exemptions).
|
|
38
|
+
module RetrievalGate
|
|
39
|
+
module_function
|
|
40
|
+
|
|
41
|
+
# A `# qmd-ok` token that is a real TRAILING shell comment, after stripping a
|
|
42
|
+
# trailing newline. The token must be preceded by whitespace (or start the
|
|
43
|
+
# command) and run to end-of-string. `echo "# qmd-ok"` does NOT match: the token
|
|
44
|
+
# there is followed by a closing quote, not end-of-string.
|
|
45
|
+
BYPASS_RE = /(?:\A|\s)#\s*qmd-ok\s*\z/.freeze
|
|
46
|
+
|
|
47
|
+
# Bash utilities that perform CONTENT SEARCH (scan file CONTENT for a pattern).
|
|
48
|
+
# These are the only bash read-vectors that can be gated; readers (cat/head/tail)
|
|
49
|
+
# and structural tools (find/ls) are never gated.
|
|
50
|
+
CONTENT_SEARCH_UTILS = %w[grep rg ag].freeze
|
|
51
|
+
|
|
52
|
+
# Decide. Returns nil to ALLOW, or a reason String to BLOCK.
|
|
53
|
+
# capabilities: { qmd:, qmd_fresh: } (booleans).
|
|
54
|
+
# reindex: no-arg callable fired once when a QMD-class target is STALE.
|
|
55
|
+
# When bypassed, returns nil and (if given) yields :bypass to the optional block
|
|
56
|
+
# so the caller can log it.
|
|
57
|
+
def decision(tool_name:, tool_input:, plastic_home:, cwd:,
|
|
58
|
+
capabilities:, reindex: -> {})
|
|
59
|
+
targets = extract_targets(tool_name, tool_input, cwd: cwd)
|
|
60
|
+
return nil if targets.empty?
|
|
61
|
+
|
|
62
|
+
if bypass?(tool_name, tool_input)
|
|
63
|
+
yield(:bypass) if block_given?
|
|
64
|
+
return nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
stale_seen = false
|
|
68
|
+
targets.each do |path|
|
|
69
|
+
next unless classify(path, plastic_home: plastic_home) == :qmd
|
|
70
|
+
|
|
71
|
+
if capabilities[:qmd] && capabilities[:qmd_fresh]
|
|
72
|
+
return qmd_reason(path)
|
|
73
|
+
elsif capabilities[:qmd] # present but stale
|
|
74
|
+
stale_seen = true
|
|
75
|
+
end
|
|
76
|
+
# absent/broken -> allow this target
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
reindex.call if stale_seen
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# --- classification ---
|
|
84
|
+
|
|
85
|
+
# Operation-based: the store tree is the only gated class (content search whose
|
|
86
|
+
# target is at/under a store routes to QMD). Everything else is allowed.
|
|
87
|
+
def classify(path, plastic_home:)
|
|
88
|
+
return :allow if path.nil? || path.empty?
|
|
89
|
+
store_path?(path, plastic_home: plastic_home) ? :qmd : :allow
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# A path AT or UNDER the global store or a project store. We gate the whole store
|
|
93
|
+
# tree (not just `*.md`) because a content search root is usually a directory:
|
|
94
|
+
# grepping the store scans its markdown, which is exactly what QMD should serve.
|
|
95
|
+
def store_path?(path, plastic_home:)
|
|
96
|
+
abs = absolutize(path)
|
|
97
|
+
home = File.expand_path(plastic_home)
|
|
98
|
+
global = File.join(home, "store")
|
|
99
|
+
return true if abs == global || abs.start_with?("#{global}/")
|
|
100
|
+
|
|
101
|
+
projects = File.join(home, "projects")
|
|
102
|
+
return false unless abs.start_with?("#{projects}/")
|
|
103
|
+
tail = abs[(projects.length + 1)..].to_s.split(File::SEPARATOR)
|
|
104
|
+
tail.length >= 2 && tail[1] == "store"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def absolutize(path)
|
|
108
|
+
File.absolute_path?(path) ? path : File.expand_path(path)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# --- bypass ---
|
|
112
|
+
|
|
113
|
+
# Only Bash commands carry a trailing `# qmd-ok` comment. The token must be a real
|
|
114
|
+
# trailing comment (BYPASS_RE), so a quoted/echoed occurrence does not bypass.
|
|
115
|
+
def bypass?(tool_name, tool_input)
|
|
116
|
+
return false unless tool_name.to_s == "Bash"
|
|
117
|
+
cmd = tool_input.is_a?(Hash) ? tool_input["command"].to_s : ""
|
|
118
|
+
BYPASS_RE.match?(cmd.chomp)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# --- target extraction ---
|
|
122
|
+
|
|
123
|
+
# Paths a CONTENT-SEARCH operation scans. Reads (Read, cat/head/tail) and
|
|
124
|
+
# structural discovery (Glob, find, ls) are NOT content search -> no targets ->
|
|
125
|
+
# always allowed. Only the Grep tool and bash grep/rg/ag can be gated. Read
|
|
126
|
+
# vectors only (this is a READ gate); write vectors are bridge.rb's job.
|
|
127
|
+
def extract_targets(tool_name, tool_input, cwd:)
|
|
128
|
+
input = tool_input.is_a?(Hash) ? tool_input : {}
|
|
129
|
+
case tool_name.to_s
|
|
130
|
+
when "Grep"
|
|
131
|
+
# The search root is the target; the query text is not a path.
|
|
132
|
+
[input["path"]].compact.reject { |s| s.to_s.empty? }
|
|
133
|
+
when "Bash"
|
|
134
|
+
bash_search_targets(input["command"].to_s)
|
|
135
|
+
else
|
|
136
|
+
# Read, Glob, and every other tool: read / structural op -> never gated.
|
|
137
|
+
[]
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# CONTENT-SEARCH path args across a compound command. Conservative: missing an
|
|
142
|
+
# exotic form is fine; never flag /dev/null or pure pipes.
|
|
143
|
+
def bash_search_targets(command)
|
|
144
|
+
return [] unless command.is_a?(String) && !command.empty?
|
|
145
|
+
targets = []
|
|
146
|
+
command.split(/[;\n]|&&|\|\||\|/).each do |segment|
|
|
147
|
+
targets.concat(segment_search_targets(segment))
|
|
148
|
+
end
|
|
149
|
+
targets.reject { |t| t.nil? || t.empty? || dev_path?(t) }.uniq
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def segment_search_targets(segment)
|
|
153
|
+
tokens = tokenize(segment)
|
|
154
|
+
return [] if tokens.empty?
|
|
155
|
+
|
|
156
|
+
# Skip leading env-style assignments (FOO=bar cmd ...).
|
|
157
|
+
idx = 0
|
|
158
|
+
idx += 1 while tokens[idx] && tokens[idx].include?("=") && tokens[idx] !~ /\A-/
|
|
159
|
+
util = File.basename(tokens[idx].to_s)
|
|
160
|
+
return [] unless CONTENT_SEARCH_UTILS.include?(util)
|
|
161
|
+
|
|
162
|
+
args = tokens[(idx + 1)..] || []
|
|
163
|
+
path_args_for(args)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Collect path-shaped arguments for a content-search util. Flags are skipped; the
|
|
167
|
+
# first non-flag bareword is the PATTERN, not a path.
|
|
168
|
+
def path_args_for(args)
|
|
169
|
+
paths = []
|
|
170
|
+
pattern_consumed = false
|
|
171
|
+
args.each do |a|
|
|
172
|
+
next if a.start_with?("-")
|
|
173
|
+
unless pattern_consumed
|
|
174
|
+
pattern_consumed = true
|
|
175
|
+
next
|
|
176
|
+
end
|
|
177
|
+
paths << a
|
|
178
|
+
end
|
|
179
|
+
paths
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Minimal tokenizer: split on whitespace, strip surrounding matching quotes off
|
|
183
|
+
# each token. Good enough for the conservative read-vector parse.
|
|
184
|
+
def tokenize(segment)
|
|
185
|
+
segment.to_s.strip.split(/\s+/).map { |t| strip_quotes(t) }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def strip_quotes(token)
|
|
189
|
+
if (token.start_with?('"') && token.end_with?('"')) ||
|
|
190
|
+
(token.start_with?("'") && token.end_with?("'"))
|
|
191
|
+
token[1..-2].to_s
|
|
192
|
+
else
|
|
193
|
+
token
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def dev_path?(path)
|
|
198
|
+
path == "/dev/null" || path.start_with?("/dev/")
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# --- reasons ---
|
|
202
|
+
|
|
203
|
+
def qmd_reason(path)
|
|
204
|
+
"retrieval gate: search the store via QMD, not a raw content scan. Reading a " \
|
|
205
|
+
"known file and listing/globbing the store are fine; only CONTENT SEARCH over " \
|
|
206
|
+
"store markdown routes through QMD. Use `qmd search`/`qmd query` over the " \
|
|
207
|
+
"`plastic-*` collections (or `scripts/qmd-sync search`) instead of scanning " \
|
|
208
|
+
"#{path}. If QMD's results do not answer your need (your reading of the " \
|
|
209
|
+
"snippets, not their score), append a trailing `# qmd-ok` to a Bash command."
|
|
210
|
+
end
|
|
211
|
+
end
|