qualia-framework 5.8.0 → 5.9.1
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/plan-checker.md +8 -0
- package/agents/qa-browser.md +7 -0
- package/agents/roadmapper.md +8 -0
- package/agents/verifier.md +14 -1
- package/bin/cli.js +30 -1
- package/bin/erp-retry.js +289 -0
- package/bin/install.js +6 -0
- package/bin/state.js +10 -1
- package/docs/onboarding.html +3 -5
- package/docs/playwright-loop-pilot-results.md +7 -5
- package/docs/research/2026-05-11-deep-research.md +189 -0
- package/hooks/session-start.js +18 -0
- package/package.json +3 -2
- package/rules/speed.md +1 -2
- package/skills/qualia-discuss/SKILL.md +4 -2
- package/skills/qualia-new/SKILL.md +71 -43
- package/skills/qualia-report/SKILL.md +64 -2
- package/skills/qualia-verify/SKILL.md +16 -0
- package/templates/help.html +2 -3
- package/tests/bin.test.sh +23 -5
- package/tests/refs.test.sh +146 -0
|
@@ -168,6 +168,17 @@ REPORT_FILE=".planning/reports/report-{date}.md"
|
|
|
168
168
|
SUBMITTED_BY=$(git config user.name || echo "unknown")
|
|
169
169
|
SUBMITTED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
170
170
|
|
|
171
|
+
# Idempotency key — deterministic per (client_report_id, submitted_at). A retry
|
|
172
|
+
# of the same shift report carries the same key, so the ERP can dedupe at the
|
|
173
|
+
# header level in addition to the UPSERT on (project_id, client_report_id).
|
|
174
|
+
IDEMPOTENCY_KEY=$(node -e "
|
|
175
|
+
const c=require('crypto');
|
|
176
|
+
const seed='$CLIENT_REPORT_ID|$SUBMITTED_AT|'+require('path').basename(process.cwd());
|
|
177
|
+
// RFC 4122 v5-style: deterministic UUID from sha1 of the seed
|
|
178
|
+
const h=c.createHash('sha1').update(seed).digest('hex');
|
|
179
|
+
console.log([h.slice(0,8),h.slice(8,12),'5'+h.slice(13,16),'8'+h.slice(17,20),h.slice(20,32)].join('-'));
|
|
180
|
+
")
|
|
181
|
+
|
|
171
182
|
# Guard: API key required for upload (otherwise curl posts an empty bearer)
|
|
172
183
|
if [ "$ERP_ENABLED" = "true" ] && [ -z "$API_KEY" ] && [ "$DRY_RUN" != "true" ]; then
|
|
173
184
|
node ~/.claude/bin/qualia-ui.js warn "ERP API key missing (~/.claude/.erp-api-key). Run: qualia-framework set-erp-key <key>"
|
|
@@ -189,6 +200,17 @@ PAYLOAD=$(
|
|
|
189
200
|
const commits=[];try{const r=spawnSync('git',['log','--oneline','--since=8 hours ago','--format=%h'],{encoding:'utf8',timeout:3000});if(r.stdout)commits.push(...r.stdout.trim().split('\n').filter(Boolean));}catch{}
|
|
190
201
|
const gitRemote=t.git_remote||git(['config','--get','remote.origin.url']);
|
|
191
202
|
const projectKey=t.project_id||repoSlug(gitRemote)||require('path').basename(process.cwd());
|
|
203
|
+
// Session duration: minutes from session_started_at to submitted_at. The ERP's
|
|
204
|
+
// example payload (docs/erp-contract.md:93) includes this; without it the ERP
|
|
205
|
+
// can't compute shift-length analytics without parsing notes.
|
|
206
|
+
let sessionDurationMinutes=0;
|
|
207
|
+
if(t.session_started_at){
|
|
208
|
+
const startMs=Date.parse(t.session_started_at);
|
|
209
|
+
const endMs=Date.parse(process.env.SUBMITTED_AT)||Date.now();
|
|
210
|
+
if(!Number.isNaN(startMs)&&endMs>startMs){
|
|
211
|
+
sessionDurationMinutes=Math.round((endMs-startMs)/60000);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
192
214
|
console.log(JSON.stringify({
|
|
193
215
|
project:t.project||require('path').basename(process.cwd()),
|
|
194
216
|
project_id:projectKey,team_id:t.team_id||'qualia-solutions',git_remote:gitRemote,
|
|
@@ -200,6 +222,7 @@ PAYLOAD=$(
|
|
|
200
222
|
gap_cycles:(t.gap_cycles||{})[String(t.phase)]||0,build_count:t.build_count||0,
|
|
201
223
|
deploy_count:t.deploy_count||0,deployed_url:t.deployed_url||'',
|
|
202
224
|
session_started_at:t.session_started_at||'',last_pushed_at:t.last_pushed_at||'',
|
|
225
|
+
session_duration_minutes:sessionDurationMinutes,
|
|
203
226
|
lifetime:t.lifetime||{},commits:commits,notes:notes,
|
|
204
227
|
submitted_by:process.env.SUBMITTED_BY||'unknown',submitted_at:process.env.SUBMITTED_AT
|
|
205
228
|
}));
|
|
@@ -214,11 +237,15 @@ if [ "$DRY_RUN" = "true" ]; then
|
|
|
214
237
|
exit 0
|
|
215
238
|
fi
|
|
216
239
|
|
|
217
|
-
# Upload — 3 attempts with 1s/3s/9s backoff
|
|
240
|
+
# Upload — 3 attempts with 1s/3s/9s backoff.
|
|
241
|
+
# Idempotency-Key header carries a deterministic UUID per (client_report_id, submitted_at)
|
|
242
|
+
# so the ERP can dedupe at the request level in addition to the UPSERT key on the body.
|
|
243
|
+
# Documented in docs/erp-contract.md:42-49 with a 24h replay window.
|
|
218
244
|
if [ "$ERP_ENABLED" = "true" ]; then
|
|
219
245
|
for ATTEMPT in 1 2 3; do
|
|
220
246
|
RESPONSE=$(curl -sS -X POST "$ERP_URL/api/v1/reports" \
|
|
221
247
|
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
|
|
248
|
+
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
|
|
222
249
|
-d "$PAYLOAD" --max-time 10 -w "\n__HTTP__%{http_code}" 2>&1)
|
|
223
250
|
HTTP_CODE=$(echo "$RESPONSE" | grep -o "__HTTP__[0-9]*" | sed 's/__HTTP__//')
|
|
224
251
|
BODY=$(echo "$RESPONSE" | sed 's/__HTTP__[0-9]*//g')
|
|
@@ -235,7 +262,42 @@ if [ "$ERP_ENABLED" = "true" ]; then
|
|
|
235
262
|
fi
|
|
236
263
|
[ $ATTEMPT -lt 3 ] && { SLEEP=$((1 * 3 ** (ATTEMPT - 1))); node ~/.claude/bin/qualia-ui.js warn "Attempt $ATTEMPT failed (HTTP ${HTTP_CODE:-timeout}), retrying in ${SLEEP}s..."; sleep $SLEEP; }
|
|
237
264
|
done
|
|
238
|
-
|
|
265
|
+
|
|
266
|
+
# If all 3 in-process attempts failed, enqueue the report into the persistent
|
|
267
|
+
# retry queue (~/.claude/.erp-retry-queue.json). session-start.js drains it on
|
|
268
|
+
# the next Claude Code launch; `qualia-framework erp-flush` drains it on demand.
|
|
269
|
+
# This replaces the prior "will appear after retry" message which was a lie —
|
|
270
|
+
# no retry mechanism existed before v5.9.
|
|
271
|
+
if [ "$ATTEMPT" = "3" ] && [ "$HTTP_CODE" != "200" ]; then
|
|
272
|
+
LAST_ERR="HTTP ${HTTP_CODE:-timeout}"
|
|
273
|
+
if [ -n "$BODY" ]; then LAST_ERR="$LAST_ERR: $(echo "$BODY" | head -c 200)"; fi
|
|
274
|
+
PAYLOAD="$PAYLOAD" \
|
|
275
|
+
CLIENT_REPORT_ID="$CLIENT_REPORT_ID" \
|
|
276
|
+
IDEMPOTENCY_KEY="$IDEMPOTENCY_KEY" \
|
|
277
|
+
ERP_URL="$ERP_URL" \
|
|
278
|
+
LAST_ERR="$LAST_ERR" \
|
|
279
|
+
node -e "
|
|
280
|
+
try {
|
|
281
|
+
const {enqueue} = require(require('os').homedir() + '/.claude/bin/erp-retry.js');
|
|
282
|
+
enqueue({
|
|
283
|
+
client_report_id: process.env.CLIENT_REPORT_ID,
|
|
284
|
+
idempotency_key: process.env.IDEMPOTENCY_KEY,
|
|
285
|
+
url: process.env.ERP_URL + '/api/v1/reports',
|
|
286
|
+
payload: process.env.PAYLOAD,
|
|
287
|
+
last_error: process.env.LAST_ERR,
|
|
288
|
+
});
|
|
289
|
+
process.stdout.write('enqueued');
|
|
290
|
+
} catch (e) {
|
|
291
|
+
process.stderr.write('enqueue failed: ' + (e.message || e));
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
" 2>/dev/null && {
|
|
295
|
+
node ~/.claude/bin/qualia-ui.js warn "ERP upload failed after 3 attempts — $CLIENT_REPORT_ID enqueued for auto-retry on next session"
|
|
296
|
+
node ~/.claude/bin/qualia-ui.js info "Drain manually with: qualia-framework erp-flush"
|
|
297
|
+
} || {
|
|
298
|
+
node ~/.claude/bin/qualia-ui.js warn "ERP upload failed after 3 attempts AND queue enqueue failed. $CLIENT_REPORT_ID is committed locally — re-run /qualia-report later to retry."
|
|
299
|
+
}
|
|
300
|
+
fi
|
|
239
301
|
fi
|
|
240
302
|
|
|
241
303
|
[ "$ERP_ENABLED" != "true" ] && node ~/.claude/bin/qualia-ui.js info "ERP upload skipped (disabled). $CLIENT_REPORT_ID committed locally."
|
|
@@ -104,6 +104,22 @@ Append '## Adversarial Findings' to verification file. Empty section fine if not
|
|
|
104
104
|
|
|
105
105
|
Findings merge into main report. Union PASS/FAIL: either pass found CRITICAL/HIGH → phase FAIL.
|
|
106
106
|
|
|
107
|
+
### 2d. INSUFFICIENT EVIDENCE downgrade (mandatory)
|
|
108
|
+
|
|
109
|
+
The verifier marks criteria it could not check (budget exhaustion, missing context) as `INSUFFICIENT EVIDENCE`. The orchestrator treats those as silent PASS unless explicitly downgraded — that's the #1 false-pass vector. Grep the verification file before declaring PASS:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
IE_COUNT=$(grep -c "INSUFFICIENT EVIDENCE" .planning/phase-{N}-verification.md 2>/dev/null || echo 0)
|
|
113
|
+
if [ "$IE_COUNT" -gt 0 ]; then
|
|
114
|
+
node ~/.claude/bin/qualia-ui.js warn "${IE_COUNT} criteria marked INSUFFICIENT EVIDENCE — downgrading verdict to FAIL"
|
|
115
|
+
# Rewrite the verdict line in-place
|
|
116
|
+
sed -i 's/^result: PASS$/result: FAIL/' .planning/phase-{N}-verification.md
|
|
117
|
+
sed -i 's/^## Verdict$/## Verdict\n\n**Downgraded to FAIL:** '"${IE_COUNT}"' criteria left unchecked. Re-run with larger budget (`max(25, tasks*5)` already applied) or simplify the phase plan./' .planning/phase-{N}-verification.md
|
|
118
|
+
fi
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The same check runs after the adversarial pass if it executed.
|
|
122
|
+
|
|
107
123
|
### 3. Present Results
|
|
108
124
|
|
|
109
125
|
Read verification report. Present:
|
package/templates/help.html
CHANGED
|
@@ -374,9 +374,8 @@
|
|
|
374
374
|
<h3>Quick Paths</h3>
|
|
375
375
|
<p class="cmd-group-note">Lightweight alternatives when the full road is overkill.</p>
|
|
376
376
|
<div class="commands">
|
|
377
|
-
<div class="cmd"><span class="cmd-name">/qualia-
|
|
378
|
-
<div class="cmd"><span class="cmd-name">/qualia-
|
|
379
|
-
<div class="cmd"><span class="cmd-name">/qualia-design</span><span class="cmd-desc">One-shot design transformation — critiques, fixes, polishes, hardens, makes responsive. No reports, no choices, just makes it professional.</span></div>
|
|
377
|
+
<div class="cmd"><span class="cmd-name">/qualia-feature</span><span class="cmd-desc">Auto-scoped single-feature build. Inline for trivia (typo, config), fresh builder spawn for 1-5 file features. Refuses and routes to /qualia-plan for phase-sized work. Flags: --force-spawn, --force-inline.</span></div>
|
|
378
|
+
<div class="cmd"><span class="cmd-name">/qualia-polish</span><span class="cmd-desc">Design pass, scope-adaptive — component, route, full app, redesign, critique, quick. Add --loop for the autonomous screenshot → score → fix loop.</span></div>
|
|
380
379
|
</div>
|
|
381
380
|
</div>
|
|
382
381
|
|
package/tests/bin.test.sh
CHANGED
|
@@ -719,18 +719,36 @@ else
|
|
|
719
719
|
fail_case "research-synthesizer missing model frontmatter"
|
|
720
720
|
fi
|
|
721
721
|
|
|
722
|
-
# 64.
|
|
723
|
-
|
|
722
|
+
# 64. v5.9 model tiering: structured agents (verifier, plan-checker, roadmapper,
|
|
723
|
+
# qa-browser) use Sonnet. Real-reasoning agents (planner, builder, researcher,
|
|
724
|
+
# visual-evaluator) keep inherited Opus.
|
|
725
|
+
SONNET_AGENTS=("verifier.md" "plan-checker.md" "roadmapper.md" "qa-browser.md")
|
|
726
|
+
OPUS_AGENTS=("planner.md" "builder.md" "researcher.md" "visual-evaluator.md")
|
|
727
|
+
|
|
728
|
+
ALL_OK=1
|
|
729
|
+
for a in "${SONNET_AGENTS[@]}"; do
|
|
730
|
+
if ! grep -q '^model: sonnet$' "$TMP/.claude/agents/$a" 2>/dev/null; then
|
|
731
|
+
ALL_OK=0
|
|
732
|
+
echo " missing 'model: sonnet' in $a"
|
|
733
|
+
fi
|
|
734
|
+
done
|
|
735
|
+
if [ "$ALL_OK" = "1" ]; then
|
|
736
|
+
pass "structured agents (verifier/plan-checker/roadmapper/qa-browser) use sonnet (v5.9 tiering)"
|
|
737
|
+
else
|
|
738
|
+
fail_case "v5.9 sonnet-tier agent has wrong model frontmatter"
|
|
739
|
+
fi
|
|
740
|
+
|
|
724
741
|
ALL_OK=1
|
|
725
|
-
for a in "${
|
|
742
|
+
for a in "${OPUS_AGENTS[@]}"; do
|
|
726
743
|
if grep -q '^model: ' "$TMP/.claude/agents/$a" 2>/dev/null; then
|
|
727
744
|
ALL_OK=0
|
|
745
|
+
echo " unexpected 'model:' in $a (should inherit Opus)"
|
|
728
746
|
fi
|
|
729
747
|
done
|
|
730
748
|
if [ "$ALL_OK" = "1" ]; then
|
|
731
|
-
pass "
|
|
749
|
+
pass "reasoning agents (planner/builder/researcher/visual-evaluator) inherit Opus"
|
|
732
750
|
else
|
|
733
|
-
fail_case "
|
|
751
|
+
fail_case "Opus-tier agent has unexpected model frontmatter"
|
|
734
752
|
fi
|
|
735
753
|
|
|
736
754
|
echo ""
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Qualia Framework — surface-drift guard
|
|
3
|
+
# Greps every active surface for backtick-quoted /qualia-{name} command references.
|
|
4
|
+
# Asserts each name has a matching skills/qualia-{name}/SKILL.md.
|
|
5
|
+
#
|
|
6
|
+
# Why: v5.7 + v5.8 removed /qualia-quick, /qualia-task, /qualia-prd, /qualia-design,
|
|
7
|
+
# /qualia-polish-loop. Three user-facing files (rules/speed.md, templates/help.html,
|
|
8
|
+
# docs/onboarding.html) still pointed at the removed commands. New hires hit dead
|
|
9
|
+
# ends. This test fails when that happens again.
|
|
10
|
+
#
|
|
11
|
+
# Scoping rules:
|
|
12
|
+
# - Only matches references quoted in markdown backticks (`/qualia-foo`) or shown
|
|
13
|
+
# as a slash-command at line start. Bare path refs (/qualia-templates/,
|
|
14
|
+
# /qualia-ui.js) are excluded — they're directories or scripts, not commands.
|
|
15
|
+
# - Historical surfaces (docs/reviews/, docs/research/, CHANGELOG) are excluded —
|
|
16
|
+
# they intentionally describe past states.
|
|
17
|
+
# - Migration mentions on active surfaces (README/guide describing v5.7 removals)
|
|
18
|
+
# are excluded via a context prefix check.
|
|
19
|
+
#
|
|
20
|
+
# Run: bash tests/refs.test.sh
|
|
21
|
+
|
|
22
|
+
FRAMEWORK_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
23
|
+
SKILLS_DIR="$FRAMEWORK_ROOT/skills"
|
|
24
|
+
|
|
25
|
+
PASS=0
|
|
26
|
+
FAIL=0
|
|
27
|
+
|
|
28
|
+
pass() {
|
|
29
|
+
echo " ✓ $1"
|
|
30
|
+
PASS=$((PASS + 1))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fail_case() {
|
|
34
|
+
echo " ✗ $1"
|
|
35
|
+
echo " $2"
|
|
36
|
+
FAIL=$((FAIL + 1))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Files we never scan — historical records, backups, vendored deps.
|
|
40
|
+
EXCLUDE_REGEX='/docs/reviews/|/docs/research/|/docs/playwright-loop-pilot-results\.md$|/CHANGELOG\.md$|\.bak\.|\.git/|/node_modules/|/\.continue-here\.md$'
|
|
41
|
+
|
|
42
|
+
# When a `/qualia-foo` ref appears AFTER one of these context tokens on the same line,
|
|
43
|
+
# it's a migration-explainer ("Replaces /qualia-quick" / "deprecated in v5.7"), not
|
|
44
|
+
# a live command reference. Treat it as exempt.
|
|
45
|
+
MIGRATION_CONTEXT_REGEX='Replaces|Removed|removed in|consolidated|deprecated|renamed|former|previously|was the|now the|now\s+`?/qualia|absorbed|superseded|legacy|migrated|after\s+`?/qualia.*-(quick|task|prd|design|polish-loop)'
|
|
46
|
+
|
|
47
|
+
ACTIVE_DIRS=(
|
|
48
|
+
"$FRAMEWORK_ROOT/rules"
|
|
49
|
+
"$FRAMEWORK_ROOT/skills"
|
|
50
|
+
"$FRAMEWORK_ROOT/agents"
|
|
51
|
+
"$FRAMEWORK_ROOT/hooks"
|
|
52
|
+
"$FRAMEWORK_ROOT/templates"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
ACTIVE_FILES=(
|
|
56
|
+
"$FRAMEWORK_ROOT/README.md"
|
|
57
|
+
"$FRAMEWORK_ROOT/guide.md"
|
|
58
|
+
"$FRAMEWORK_ROOT/CLAUDE.md"
|
|
59
|
+
"$FRAMEWORK_ROOT/AGENTS.md"
|
|
60
|
+
"$FRAMEWORK_ROOT/docs/onboarding.html"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
echo "refs.test.sh — surface-drift guard (active /qualia-* references must point at shipped skills)"
|
|
64
|
+
echo ""
|
|
65
|
+
|
|
66
|
+
SCAN_FILES=$(
|
|
67
|
+
{
|
|
68
|
+
for d in "${ACTIVE_DIRS[@]}"; do
|
|
69
|
+
[ -d "$d" ] && find "$d" -type f \( -name "*.md" -o -name "*.html" \)
|
|
70
|
+
done
|
|
71
|
+
for f in "${ACTIVE_FILES[@]}"; do
|
|
72
|
+
[ -f "$f" ] && echo "$f"
|
|
73
|
+
done
|
|
74
|
+
} | grep -Ev "$EXCLUDE_REGEX" | sort -u
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Extract backtick-quoted command refs only. Two patterns:
|
|
78
|
+
# 1. `/qualia-foo` — backticked, the canonical command-doc style
|
|
79
|
+
# 2. <dt>/qualia-foo</dt> — HTML help/onboarding pages
|
|
80
|
+
# We capture name + file:line so we can show context per failure.
|
|
81
|
+
declare -A SEEN_REFS
|
|
82
|
+
declare -A REF_LOCATIONS
|
|
83
|
+
|
|
84
|
+
while IFS= read -r file; do
|
|
85
|
+
# Pattern A: backtick-quoted commands. Allow trailing flag/word but only capture base name.
|
|
86
|
+
while IFS=: read -r path lineno line; do
|
|
87
|
+
[ -z "$line" ] && continue
|
|
88
|
+
# Skip migration-context lines.
|
|
89
|
+
if echo "$line" | grep -qE "$MIGRATION_CONTEXT_REGEX"; then
|
|
90
|
+
continue
|
|
91
|
+
fi
|
|
92
|
+
# Extract every backticked /qualia-foo on this line.
|
|
93
|
+
matches=$(echo "$line" | grep -oE '`/qualia(-[a-z]+){0,3}`' | sed 's/^`//; s/`$//')
|
|
94
|
+
for ref in $matches; do
|
|
95
|
+
SEEN_REFS["$ref"]=1
|
|
96
|
+
REF_LOCATIONS["$ref"]="${REF_LOCATIONS[$ref]:+${REF_LOCATIONS[$ref]}, }$(basename "$path"):$lineno"
|
|
97
|
+
done
|
|
98
|
+
done < <(grep -nE '`/qualia(-[a-z]+){0,3}`' "$file" 2>/dev/null)
|
|
99
|
+
|
|
100
|
+
# Pattern B: HTML <dt>/qualia-foo</dt>.
|
|
101
|
+
while IFS=: read -r path lineno line; do
|
|
102
|
+
[ -z "$line" ] && continue
|
|
103
|
+
if echo "$line" | grep -qE "$MIGRATION_CONTEXT_REGEX"; then
|
|
104
|
+
continue
|
|
105
|
+
fi
|
|
106
|
+
matches=$(echo "$line" | grep -oE '<dt>/qualia(-[a-z]+){0,3}( [^<]*)?</dt>' | sed -E 's|<dt>(/qualia(-[a-z]+){0,3}).*|\1|')
|
|
107
|
+
for ref in $matches; do
|
|
108
|
+
SEEN_REFS["$ref"]=1
|
|
109
|
+
REF_LOCATIONS["$ref"]="${REF_LOCATIONS[$ref]:+${REF_LOCATIONS[$ref]}, }$(basename "$path"):$lineno"
|
|
110
|
+
done
|
|
111
|
+
done < <(grep -nE '<dt>/qualia' "$file" 2>/dev/null)
|
|
112
|
+
done <<<"$SCAN_FILES"
|
|
113
|
+
|
|
114
|
+
if [ ${#SEEN_REFS[@]} -eq 0 ]; then
|
|
115
|
+
fail_case "scan" "no backticked /qualia-* references found across active surfaces (scanner broken?)"
|
|
116
|
+
echo ""
|
|
117
|
+
echo "Results: $PASS passed, $FAIL failed"
|
|
118
|
+
exit 1
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# Sort refs for deterministic output.
|
|
122
|
+
for ref in $(printf '%s\n' "${!SEEN_REFS[@]}" | sort); do
|
|
123
|
+
name="${ref#/}"
|
|
124
|
+
skill_dir="$SKILLS_DIR/$name"
|
|
125
|
+
locations="${REF_LOCATIONS[$ref]}"
|
|
126
|
+
if [ -d "$skill_dir" ] && [ -f "$skill_dir/SKILL.md" ]; then
|
|
127
|
+
pass "$ref → skills/$name/SKILL.md"
|
|
128
|
+
continue
|
|
129
|
+
fi
|
|
130
|
+
fail_case "$ref" "no skills/$name/SKILL.md — referenced by: $locations"
|
|
131
|
+
done
|
|
132
|
+
|
|
133
|
+
echo ""
|
|
134
|
+
echo "Results: $PASS passed, $FAIL failed"
|
|
135
|
+
|
|
136
|
+
if [ "$FAIL" -gt 0 ]; then
|
|
137
|
+
echo ""
|
|
138
|
+
echo "Surface drift detected. To fix one of:"
|
|
139
|
+
echo " 1. Restore the missing skill at skills/<name>/SKILL.md"
|
|
140
|
+
echo " 2. Update the offending file to point at the replacement command"
|
|
141
|
+
echo " 3. Reframe the mention as a migration note (this test skips lines containing"
|
|
142
|
+
echo " 'Replaces', 'consolidated', 'deprecated', 'now', 'former', 'superseded', etc.)"
|
|
143
|
+
exit 1
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
exit 0
|