qualia-framework 5.3.0 → 5.5.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/README.md +54 -30
- package/agents/builder.md +33 -8
- package/agents/plan-checker.md +60 -3
- package/agents/planner.md +26 -2
- package/agents/qa-browser.md +10 -0
- package/agents/research-synthesizer.md +10 -0
- package/agents/researcher.md +38 -2
- package/agents/roadmapper.md +10 -0
- package/agents/verifier.md +15 -3
- package/agents/visual-evaluator.md +1 -1
- package/bin/install.js +44 -2
- package/bin/plan-contract.js +32 -1
- package/bin/state.js +155 -133
- package/docs/archive/v4.0.0-review.md +288 -0
- package/docs/erp-contract.md +11 -0
- package/guide.md +14 -7
- package/hooks/session-start.js +1 -1
- package/package.json +5 -2
- package/rules/architecture.md +125 -0
- package/rules/infrastructure.md +1 -2
- package/rules/speed.md +55 -0
- package/skills/qualia-discuss/SKILL.md +17 -3
- package/skills/qualia-help/SKILL.md +1 -1
- package/skills/qualia-map/SKILL.md +1 -1
- package/skills/qualia-milestone/SKILL.md +1 -1
- package/skills/qualia-new/SKILL.md +2 -2
- package/skills/qualia-optimize/REFERENCE.md +2 -2
- package/skills/qualia-optimize/SKILL.md +1 -1
- package/skills/qualia-polish/SKILL.md +3 -3
- package/skills/qualia-polish-loop/REFERENCE.md +1 -1
- package/skills/qualia-polish-loop/SKILL.md +3 -3
- package/skills/qualia-polish-loop/fixtures/broken.html +2 -2
- package/skills/qualia-polish-loop/scripts/score.mjs +1 -1
- package/skills/qualia-postmortem/SKILL.md +1 -1
- package/skills/qualia-quick/SKILL.md +1 -1
- package/skills/qualia-report/SKILL.md +8 -6
- package/skills/qualia-research/SKILL.md +5 -3
- package/skills/qualia-road/SKILL.md +15 -5
- package/skills/qualia-task/SKILL.md +1 -1
- package/templates/CONTEXT.md +3 -2
- package/templates/PRODUCT.md +1 -1
- package/templates/help.html +1 -1
- package/templates/phase-context.md +5 -4
- package/tests/bin.test.sh +33 -3
- package/tests/lib.test.sh +21 -0
- package/tests/skills.test.sh +143 -0
- package/tests/slop-detect.test.sh +160 -0
- package/docs/install-redesign-builder-prompt.md +0 -290
- package/docs/install-redesign-pilot.md +0 -234
- package/docs/journey-demo.html +0 -1008
- package/docs/playwright-loop-builder-prompt.md +0 -185
- package/docs/playwright-loop-design-notes.md +0 -108
- package/docs/playwright-loop-tester-prompt.md +0 -213
- package/docs/polish-loop-supervised-run.md +0 -111
- /package/{rules → qualia-design}/design-brand.md +0 -0
- /package/{rules → qualia-design}/design-laws.md +0 -0
- /package/{rules → qualia-design}/design-product.md +0 -0
- /package/{rules → qualia-design}/design-reference.md +0 -0
- /package/{rules → qualia-design}/design-rubric.md +0 -0
- /package/{rules → qualia-design}/frontend.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: qualia-road
|
|
3
|
-
description: "
|
|
3
|
+
description: "TERMINAL workflow map — Project → Journey → Milestones → Phases → Tasks. Use this in headless/SSH/no-browser sessions or when the user asks for the road in chat. For the HTML reference (default when a browser is available), use /qualia-help. Triggers: 'how does Qualia work', 'what's the workflow', 'show me the road', 'what command does X', 'how do projects flow', 'in terminal please', SSH context."
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
allowed-tools:
|
|
6
6
|
- Read
|
|
@@ -45,14 +45,24 @@ Every road agent loads `PRODUCT.md + DESIGN.md + design-laws.md` substrate. Buil
|
|
|
45
45
|
/qualia-polish --quick ~1m gates only
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
## /qualia-polish-loop -- autonomous visual QA (v5.1
|
|
48
|
+
## /qualia-polish-loop -- autonomous visual QA (v5.1+, hardened in v5.2)
|
|
49
49
|
```
|
|
50
|
-
/qualia-polish-loop http://localhost:3000
|
|
51
|
-
/qualia-polish-loop {url} --max 4
|
|
52
|
-
/qualia-polish-loop {url} --ref design.png
|
|
50
|
+
/qualia-polish-loop http://localhost:3000 screenshot + eval + fix loop
|
|
51
|
+
/qualia-polish-loop {url} --max 4 cap iterations
|
|
52
|
+
/qualia-polish-loop {url} --ref design.png anchor to reference image
|
|
53
|
+
/qualia-polish-loop {url} --reduced-motion force prefers-reduced-motion (v5.2+)
|
|
54
|
+
/qualia-polish-loop --routes /a,/b,/c multi-route sweep (v5.2+)
|
|
53
55
|
```
|
|
54
56
|
Screenshots at 3 viewports (375/768/1440), scores 8 design dimensions using vision, fixes issues, re-screenshots, loops until all dims >= 3 or kill-switch triggers. Per-iteration git commits for clean revert.
|
|
55
57
|
|
|
58
|
+
## v5.3+ skills (Matt Pocock gaps closed)
|
|
59
|
+
```
|
|
60
|
+
/qualia-prd synthesize current conversation → .planning/PRD-{slug}.md (durable feature spec)
|
|
61
|
+
/qualia-hook-gen convert a CLAUDE.md/rules instruction into a deterministic pre-tool-use hook
|
|
62
|
+
/qualia-optimize --deepen now spawns 3 parallel interface-design variants per candidate (Step 5b)
|
|
63
|
+
```
|
|
64
|
+
`/qualia-prd` pairs with `/qualia-issues` to form the PRD → vertical-slice → execute loop. `/qualia-hook-gen` reduces lifetime token cost (each migrated rule frees ~50-200 tokens per request). `/qualia-optimize --deepen` produces dramatically better refactor RFCs because 3 radically-different interfaces are surfaced and the human picks/hybridizes.
|
|
65
|
+
|
|
56
66
|
## Alignment substrate (v5.0+)
|
|
57
67
|
Before high-stakes phases, run alignment skills against `.planning/CONTEXT.md` (domain glossary) and `.planning/decisions/` (ADRs):
|
|
58
68
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: qualia-task
|
|
3
|
-
description: "
|
|
3
|
+
description: "Single focused task with a FRESH builder subagent spawn — 1-3 hours, 1-5 files, atomic commit, validation contract. Heavier than /qualia-quick (which runs inline with no spawn) but lighter than /qualia-build (which needs a phase plan). Use when the user says 'build this one thing', 'add a component', 'implement this feature', 'qualia-task', or for any 1-5 file feature outside a full phase."
|
|
4
4
|
allowed-tools:
|
|
5
5
|
- Bash
|
|
6
6
|
- Read
|
package/templates/CONTEXT.md
CHANGED
|
@@ -18,14 +18,15 @@ A unit of work inside a milestone. 2–5 tasks. Ends in a verification gate.
|
|
|
18
18
|
**Avoid:** epic, story, ticket, sprint.
|
|
19
19
|
|
|
20
20
|
### Task
|
|
21
|
-
A
|
|
22
|
-
**Avoid:**
|
|
21
|
+
A framework-internal execution unit: one commit-sized work item with one verification contract.
|
|
22
|
+
**Avoid:** using "task" as an ERP assignment or employee performance label unless the product domain explicitly needs it.
|
|
23
23
|
|
|
24
24
|
## Relationships
|
|
25
25
|
- Project holds many Milestones
|
|
26
26
|
- Milestone holds many Phases
|
|
27
27
|
- Phase holds many Tasks
|
|
28
28
|
- Task carries one Verification Contract
|
|
29
|
+
- ERP tracks project deadlines, milestone deadlines, and employee shift submissions; framework tasks stay internal.
|
|
29
30
|
- {{add domain-specific relationships, e.g. "Customer holds many Orders"}}
|
|
30
31
|
|
|
31
32
|
## Flagged ambiguities
|
package/templates/PRODUCT.md
CHANGED
|
@@ -49,7 +49,7 @@ Sites the project should NOT look like. Anti-references pin down what the design
|
|
|
49
49
|
- {URL or descriptor} — ...
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
For Brand register, these are usually saturated aesthetic lanes (see `
|
|
52
|
+
For Brand register, these are usually saturated aesthetic lanes (see `qualia-design/design-brand.md`).
|
|
53
53
|
For Product register, these are usually patterns we don't want to inherit (e.g., "Salesforce Lightning — too dense, too many panels").
|
|
54
54
|
|
|
55
55
|
## Positive references (optional, ≤3)
|
package/templates/help.html
CHANGED
|
@@ -479,7 +479,7 @@
|
|
|
479
479
|
<li><span class="rule-icon">1</span> Feature branches by default — OWNER overrides must be explicit</li>
|
|
480
480
|
<li><span class="rule-icon">2</span> Read before write — understand files before editing</li>
|
|
481
481
|
<li><span class="rule-icon">3</span> MVP first — build what's asked, nothing extra</li>
|
|
482
|
-
<li><span class="rule-icon">4</span> /qualia-report before clock-out — mandatory
|
|
482
|
+
<li><span class="rule-icon">4</span> /qualia-report before clock-out — mandatory shift submission in ERP</li>
|
|
483
483
|
<li><span class="rule-icon">5</span> Secrets through approved flows — use set-erp-key or ask Fawzi</li>
|
|
484
484
|
<li><span class="rule-icon">6</span> Stuck 30+ minutes? Ask Fawzi</li>
|
|
485
485
|
</ul>
|
|
@@ -13,11 +13,12 @@ Captured during `/qualia-discuss {N}` — decisions, trade-offs, and constraints
|
|
|
13
13
|
|
|
14
14
|
## Locked Decisions
|
|
15
15
|
|
|
16
|
-
Non-negotiable choices. Planner must honor these exactly.
|
|
16
|
+
Non-negotiable choices. Planner must honor these exactly. Every row has a stable ID (`D-NN`) — the planner's Decision Coverage Audit checks each is implemented; the plan-checker BLOCKS if any is missing.
|
|
17
17
|
|
|
18
|
-
| Decision | Rationale | Source |
|
|
19
|
-
|
|
20
|
-
| {e.g., "Use Supabase RLS for authorization, not middleware"} | {e.g., "Client compliance requires database-level checks"} | {who/when} |
|
|
18
|
+
| ID | Decision | Rationale | Source |
|
|
19
|
+
|----|----------|-----------|--------|
|
|
20
|
+
| D-01 | {e.g., "Use Supabase RLS for authorization, not middleware"} | {e.g., "Client compliance requires database-level checks"} | {who/when} |
|
|
21
|
+
| D-02 | {next decision} | {rationale} | {source} |
|
|
21
22
|
|
|
22
23
|
## Discretion (Planner Chooses)
|
|
23
24
|
|
package/tests/bin.test.sh
CHANGED
|
@@ -1201,7 +1201,7 @@ else
|
|
|
1201
1201
|
fi
|
|
1202
1202
|
|
|
1203
1203
|
# 108. package.json version is 5.x (5.1+ accepted; v5.1 / v5.2 share the v5 line)
|
|
1204
|
-
if grep -qE '"5\.[
|
|
1204
|
+
if grep -qE '"5\.[1234]\.' "$FRAMEWORK_DIR/package.json"; then
|
|
1205
1205
|
pass "package.json version is 5.x"
|
|
1206
1206
|
else
|
|
1207
1207
|
fail_case "package.json version not 5.x"
|
|
@@ -1430,7 +1430,7 @@ fi
|
|
|
1430
1430
|
|
|
1431
1431
|
# 128. package.json bumped to 5.x (5.1+ accepted; 5.2 is the v5.2 release)
|
|
1432
1432
|
PKG_V=$($NODE -e 'console.log(require("'"$FRAMEWORK_DIR"'/package.json").version)')
|
|
1433
|
-
if echo "$PKG_V" | grep -qE "^5\.[
|
|
1433
|
+
if echo "$PKG_V" | grep -qE "^5\.[1234]\."; then
|
|
1434
1434
|
pass "package.json version bumped to 5.x ($PKG_V)"
|
|
1435
1435
|
else
|
|
1436
1436
|
fail_case "package.json version not 5.x" "got=$PKG_V"
|
|
@@ -1577,12 +1577,42 @@ fi
|
|
|
1577
1577
|
|
|
1578
1578
|
# 143. package.json version is 5.x (5.1+ accepted; v5.3 is the v5.3 release)
|
|
1579
1579
|
PKG_V=$($NODE -e 'console.log(require("'"$FRAMEWORK_DIR"'/package.json").version)')
|
|
1580
|
-
if echo "$PKG_V" | grep -qE "^5\.[
|
|
1580
|
+
if echo "$PKG_V" | grep -qE "^5\.[1234]\."; then
|
|
1581
1581
|
pass "package.json version is 5.x ($PKG_V) — v5.3 accepted"
|
|
1582
1582
|
else
|
|
1583
1583
|
fail_case "package.json version not 5.x" "got=$PKG_V"
|
|
1584
1584
|
fi
|
|
1585
1585
|
|
|
1586
|
+
echo ""
|
|
1587
|
+
echo "--- ERP shift-report contract ---"
|
|
1588
|
+
|
|
1589
|
+
# 144. qualia-report describes clock-out as truthful shift submission, not task completion
|
|
1590
|
+
if grep -qi "daily shift report" "$TMP/.claude/skills/qualia-report/SKILL.md" \
|
|
1591
|
+
&& grep -qi "not a task-completion ceremony" "$TMP/.claude/skills/qualia-report/SKILL.md" \
|
|
1592
|
+
&& grep -qi "What happened during your shift" "$TMP/.claude/skills/qualia-report/SKILL.md"; then
|
|
1593
|
+
pass "qualia-report frames clock-out as shift submission, not task completion"
|
|
1594
|
+
else
|
|
1595
|
+
fail_case "qualia-report missing shift-submission contract"
|
|
1596
|
+
fi
|
|
1597
|
+
|
|
1598
|
+
# 145. ERP contract documents the date model: project, milestone, employee submission
|
|
1599
|
+
if grep -q "Project deadline" "$FRAMEWORK_DIR/docs/erp-contract.md" \
|
|
1600
|
+
&& grep -q "Milestone deadline" "$FRAMEWORK_DIR/docs/erp-contract.md" \
|
|
1601
|
+
&& grep -q "Employee submission date" "$FRAMEWORK_DIR/docs/erp-contract.md" \
|
|
1602
|
+
&& grep -q "Phase and task counters remain framework telemetry" "$FRAMEWORK_DIR/docs/erp-contract.md"; then
|
|
1603
|
+
pass "ERP contract documents project/milestone/submission date model"
|
|
1604
|
+
else
|
|
1605
|
+
fail_case "ERP contract missing date-model clarification"
|
|
1606
|
+
fi
|
|
1607
|
+
|
|
1608
|
+
# 146. Project glossary keeps framework tasks internal to agent execution
|
|
1609
|
+
if grep -q "framework-internal execution unit" "$FRAMEWORK_DIR/templates/CONTEXT.md" \
|
|
1610
|
+
&& grep -q "ERP tracks project deadlines, milestone deadlines, and employee shift submissions" "$FRAMEWORK_DIR/templates/CONTEXT.md"; then
|
|
1611
|
+
pass "CONTEXT template distinguishes internal tasks from ERP workflow"
|
|
1612
|
+
else
|
|
1613
|
+
fail_case "CONTEXT template missing framework-task vs ERP-workflow distinction"
|
|
1614
|
+
fi
|
|
1615
|
+
|
|
1586
1616
|
echo ""
|
|
1587
1617
|
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
1588
1618
|
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
package/tests/lib.test.sh
CHANGED
|
@@ -57,6 +57,27 @@ console.log(errs.length > 0 ? "REJECTED" : "ACCEPTED");
|
|
|
57
57
|
')
|
|
58
58
|
[ "$OUT" = "REJECTED" ] && ok "rejects malformed contract" || fail "malformed accepted"
|
|
59
59
|
|
|
60
|
+
OUT=$($NODE -e '
|
|
61
|
+
const pc = require("'"$PC"'");
|
|
62
|
+
const slop = {
|
|
63
|
+
version: 1, phase: 1, goal: "x", why: "y",
|
|
64
|
+
generated_at: "t", generated_by: "planner", source_plan_hash: "h",
|
|
65
|
+
success_criteria: ["sc"],
|
|
66
|
+
tasks: [{
|
|
67
|
+
id: "T1", title: "t", wave: 1, depends_on: [],
|
|
68
|
+
files_modify: [], files_create: [], files_delete: [],
|
|
69
|
+
acceptance_criteria: ["minimal implementation of login"],
|
|
70
|
+
action: "Add hardcoded for now placeholder, will be wired later",
|
|
71
|
+
context_files: [],
|
|
72
|
+
verification: [{ type: "file-exists", path: "a.ts" }]
|
|
73
|
+
}]
|
|
74
|
+
};
|
|
75
|
+
const errs = pc.validate(slop);
|
|
76
|
+
const hits = errs.filter(e => /scope-reduction/.test(e));
|
|
77
|
+
console.log(hits.length >= 2 ? "DETECTED" : "MISSED:" + errs.join(";"));
|
|
78
|
+
')
|
|
79
|
+
[ "$OUT" = "DETECTED" ] && ok "detects scope-reduction phrases in action + acceptance_criteria" || fail "scope-reduction missed: $OUT"
|
|
80
|
+
|
|
60
81
|
OUT=$($NODE -e '
|
|
61
82
|
const pc = require("'"$PC"'");
|
|
62
83
|
const c = {
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Qualia Framework — skill smoke tests
|
|
3
|
+
# Verifies every skills/*/SKILL.md is well-formed:
|
|
4
|
+
# - YAML frontmatter present and parseable
|
|
5
|
+
# - name field matches folder name
|
|
6
|
+
# - description present and substantive
|
|
7
|
+
# - description has trigger phrases (or skill is disable-model-invocation)
|
|
8
|
+
# - body has at least one h1 heading and 2+ sections
|
|
9
|
+
#
|
|
10
|
+
# Run: bash tests/skills.test.sh
|
|
11
|
+
|
|
12
|
+
PASS=0
|
|
13
|
+
FAIL=0
|
|
14
|
+
SKILLS_DIR="$(cd "$(dirname "$0")/../skills" && pwd)"
|
|
15
|
+
|
|
16
|
+
# Skills allowed to ship without trigger phrases — disable-model-invocation
|
|
17
|
+
# skills only fire on explicit slash command, so triggers are optional.
|
|
18
|
+
SKIP_TRIGGER_CHECK=("qualia-road" "qualia-handoff")
|
|
19
|
+
|
|
20
|
+
pass() {
|
|
21
|
+
echo " ✓ $1"
|
|
22
|
+
PASS=$((PASS + 1))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fail_case() {
|
|
26
|
+
echo " ✗ $1"
|
|
27
|
+
echo " $2"
|
|
28
|
+
FAIL=$((FAIL + 1))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
is_in_skip_list() {
|
|
32
|
+
local needle="$1"
|
|
33
|
+
for x in "${SKIP_TRIGGER_CHECK[@]}"; do
|
|
34
|
+
[ "$x" = "$needle" ] && return 0
|
|
35
|
+
done
|
|
36
|
+
return 1
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
echo "skills.test.sh — smoke tests for every skills/*/SKILL.md"
|
|
40
|
+
echo ""
|
|
41
|
+
|
|
42
|
+
for skill_dir in "$SKILLS_DIR"/*/; do
|
|
43
|
+
name=$(basename "$skill_dir")
|
|
44
|
+
skill_md="$skill_dir/SKILL.md"
|
|
45
|
+
|
|
46
|
+
# Existence
|
|
47
|
+
if [ ! -f "$skill_md" ]; then
|
|
48
|
+
fail_case "$name" "SKILL.md not found at $skill_md"
|
|
49
|
+
continue
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Frontmatter present
|
|
53
|
+
if ! head -1 "$skill_md" | grep -q "^---$"; then
|
|
54
|
+
fail_case "$name: frontmatter" "first line is not '---'"
|
|
55
|
+
continue
|
|
56
|
+
fi
|
|
57
|
+
if ! sed -n '2,30p' "$skill_md" | grep -q "^---$"; then
|
|
58
|
+
fail_case "$name: frontmatter" "no closing --- within first 30 lines"
|
|
59
|
+
continue
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# name field matches folder
|
|
63
|
+
fm_name=$(grep "^name:" "$skill_md" | head -1 | sed 's/^name:[[:space:]]*//' | tr -d '"')
|
|
64
|
+
if [ "$fm_name" != "$name" ]; then
|
|
65
|
+
fail_case "$name: name field" "frontmatter says name=\"$fm_name\", folder is \"$name\""
|
|
66
|
+
continue
|
|
67
|
+
fi
|
|
68
|
+
pass "$name: frontmatter name matches folder"
|
|
69
|
+
|
|
70
|
+
# description field present + substantive
|
|
71
|
+
fm_desc=$(grep "^description:" "$skill_md" | head -1 | sed 's/^description:[[:space:]]*//')
|
|
72
|
+
desc_len=${#fm_desc}
|
|
73
|
+
if [ "$desc_len" -lt 50 ]; then
|
|
74
|
+
fail_case "$name: description" "description is $desc_len chars, expected >= 50"
|
|
75
|
+
continue
|
|
76
|
+
fi
|
|
77
|
+
pass "$name: description present (${desc_len} chars)"
|
|
78
|
+
|
|
79
|
+
# Trigger phrases (unless disable-model-invocation or in transitional skip list)
|
|
80
|
+
if ! is_in_skip_list "$name"; then
|
|
81
|
+
has_disable=$(grep -c "disable-model-invocation:[[:space:]]*true" "$skill_md")
|
|
82
|
+
if [ "$has_disable" = "0" ]; then
|
|
83
|
+
if echo "$fm_desc" | grep -qiE "trigger|when user|use when|invoke|says|use this|fire on|user types"; then
|
|
84
|
+
pass "$name: description has trigger guidance"
|
|
85
|
+
else
|
|
86
|
+
fail_case "$name: triggers" "description lacks trigger phrases (Trigger:/Use when:/'says'/etc.) and skill is not disable-model-invocation"
|
|
87
|
+
fi
|
|
88
|
+
fi
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# Body has an h1 heading
|
|
92
|
+
h1_count=$(grep -cE "^# " "$skill_md")
|
|
93
|
+
if [ "$h1_count" -ge 1 ]; then
|
|
94
|
+
pass "$name: body has h1 heading"
|
|
95
|
+
else
|
|
96
|
+
fail_case "$name: body" "no h1 heading (^# ) in body"
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# Body has at least one section (any ## heading)
|
|
100
|
+
h2_count=$(grep -cE "^## " "$skill_md")
|
|
101
|
+
if [ "$h2_count" -ge 1 ]; then
|
|
102
|
+
pass "$name: body has section heading (${h2_count} found)"
|
|
103
|
+
else
|
|
104
|
+
fail_case "$name: body" "no '## ' section heading; every skill needs at least one"
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# Cache-aware spawn audit (per rules/grounding.md):
|
|
108
|
+
# Every spawn to a CUSTOM (qualia-*) agent must anchor the prompt with
|
|
109
|
+
# `@~/.claude/agents/{name}.md` (either `Role: @...` or `Read your role:
|
|
110
|
+
# @...` — both forms accepted). The role file is session-stable; placing
|
|
111
|
+
# it first lets Anthropic's prompt cache reuse the prefix across spawns
|
|
112
|
+
# (documented 81-90% cost reduction). If task-specific content lands
|
|
113
|
+
# before the role anchor, the entire prefix recomputes on every spawn.
|
|
114
|
+
#
|
|
115
|
+
# Built-in subagent types (Explore, general-purpose, Plan, etc.) have
|
|
116
|
+
# stable system-prompt baselines on Anthropic's side; no Role anchor
|
|
117
|
+
# required. We count only `subagent_type="qualia-*"` spawns.
|
|
118
|
+
#
|
|
119
|
+
# Some skills follow progressive-disclosure discipline (e.g.
|
|
120
|
+
# qualia-polish-loop) and put the literal spawn template in REFERENCE.md
|
|
121
|
+
# while SKILL.md mentions the spawn in prose. We scan both.
|
|
122
|
+
custom_spawn_count=$(grep -c 'subagent_type="qualia-' "$skill_md")
|
|
123
|
+
ref_md="$skill_dir/REFERENCE.md"
|
|
124
|
+
if [ -f "$ref_md" ]; then
|
|
125
|
+
custom_spawn_count=$((custom_spawn_count + $(grep -c 'subagent_type="qualia-' "$ref_md")))
|
|
126
|
+
fi
|
|
127
|
+
if [ "${custom_spawn_count:-0}" -gt 0 ]; then
|
|
128
|
+
role_count=$(grep -cE '@~/\.claude/agents/' "$skill_md")
|
|
129
|
+
if [ -f "$ref_md" ]; then
|
|
130
|
+
role_count=$((role_count + $(grep -cE '@~/\.claude/agents/' "$ref_md")))
|
|
131
|
+
fi
|
|
132
|
+
if [ "${role_count:-0}" -ge "$custom_spawn_count" ]; then
|
|
133
|
+
pass "$name: spawn audit ($custom_spawn_count custom spawn(s), all role-anchored for cache)"
|
|
134
|
+
else
|
|
135
|
+
fail_case "$name: spawn audit" "$custom_spawn_count custom spawn(s) but only ${role_count:-0} '@~/.claude/agents/' anchors — prompt cache will miss"
|
|
136
|
+
fi
|
|
137
|
+
fi
|
|
138
|
+
done
|
|
139
|
+
|
|
140
|
+
echo ""
|
|
141
|
+
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
142
|
+
|
|
143
|
+
[ "$FAIL" = "0" ]
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Qualia Framework — bin/slop-detect.mjs behavior tests
|
|
3
|
+
# Verifies the AI-tells gatekeeper actually catches what it claims to catch.
|
|
4
|
+
#
|
|
5
|
+
# Run: bash tests/slop-detect.test.sh
|
|
6
|
+
|
|
7
|
+
PASS=0
|
|
8
|
+
FAIL=0
|
|
9
|
+
SLOP_DETECT="$(cd "$(dirname "$0")/../bin" && pwd)/slop-detect.mjs"
|
|
10
|
+
NODE="${NODE:-node}"
|
|
11
|
+
|
|
12
|
+
TMP_DIRS=()
|
|
13
|
+
cleanup() {
|
|
14
|
+
for d in "${TMP_DIRS[@]}"; do
|
|
15
|
+
[ -d "$d" ] && rm -rf "$d"
|
|
16
|
+
done
|
|
17
|
+
}
|
|
18
|
+
trap cleanup EXIT
|
|
19
|
+
|
|
20
|
+
mktmp() {
|
|
21
|
+
local TMP
|
|
22
|
+
TMP=$(mktemp -d)
|
|
23
|
+
TMP_DIRS+=("$TMP")
|
|
24
|
+
echo "$TMP"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
pass() {
|
|
28
|
+
echo " ✓ $1"
|
|
29
|
+
PASS=$((PASS + 1))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fail_case() {
|
|
33
|
+
echo " ✗ $1"
|
|
34
|
+
echo " $2"
|
|
35
|
+
FAIL=$((FAIL + 1))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
echo "slop-detect.test.sh — bin/slop-detect.mjs behavioral tests"
|
|
39
|
+
echo ""
|
|
40
|
+
|
|
41
|
+
# ── Sanity: file exists and parses ────────────────────────────────────
|
|
42
|
+
if [ ! -f "$SLOP_DETECT" ]; then
|
|
43
|
+
fail_case "slop-detect exists" "$SLOP_DETECT not found"
|
|
44
|
+
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
pass "slop-detect.mjs exists at expected path"
|
|
48
|
+
|
|
49
|
+
if ! $NODE --check "$SLOP_DETECT" 2>&1 | head -1; then
|
|
50
|
+
pass "slop-detect.mjs parses as valid JS"
|
|
51
|
+
fi
|
|
52
|
+
# Above is a heredoc test — node --check succeeds silently on valid JS.
|
|
53
|
+
# Re-test explicitly so a parse error fails the suite.
|
|
54
|
+
if $NODE --check "$SLOP_DETECT" 2>/dev/null; then
|
|
55
|
+
pass "slop-detect.mjs syntax is valid"
|
|
56
|
+
else
|
|
57
|
+
fail_case "syntax check" "node --check failed on $SLOP_DETECT"
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# ── Clean file: should exit 0 ─────────────────────────────────────────
|
|
61
|
+
TMP=$(mktmp)
|
|
62
|
+
cat > "$TMP/clean.tsx" <<'EOF'
|
|
63
|
+
import { Button } from '@/components/ui/button';
|
|
64
|
+
|
|
65
|
+
export default function Page() {
|
|
66
|
+
return (
|
|
67
|
+
<div className="bg-surface text-foreground">
|
|
68
|
+
<h1 className="text-display">Welcome</h1>
|
|
69
|
+
<Button variant="primary">Continue setup</Button>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
EOF
|
|
74
|
+
if $NODE "$SLOP_DETECT" "$TMP/clean.tsx" >/dev/null 2>&1; then
|
|
75
|
+
pass "exits 0 on a clean .tsx file"
|
|
76
|
+
else
|
|
77
|
+
fail_case "clean file" "exit non-zero on a deliberately clean file"
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# ── Em-dash detection (HIGH severity — reported, doesn't block) ──────
|
|
81
|
+
# Em-dash is HIGH not CRITICAL, so default exit is 0; we verify the
|
|
82
|
+
# FINDING is reported to stdout/stderr, not the exit code.
|
|
83
|
+
TMP2=$(mktmp)
|
|
84
|
+
cat > "$TMP2/emdash.tsx" <<'EOF'
|
|
85
|
+
export default function Page() {
|
|
86
|
+
return <p>Welcome — to our amazing platform</p>;
|
|
87
|
+
}
|
|
88
|
+
EOF
|
|
89
|
+
OUT=$($NODE "$SLOP_DETECT" "$TMP2/emdash.tsx" 2>&1 || true)
|
|
90
|
+
if echo "$OUT" | grep -qiE "em.?dash|—"; then
|
|
91
|
+
pass "reports em-dash finding (HIGH severity, non-blocking)"
|
|
92
|
+
else
|
|
93
|
+
fail_case "em-dash detection" "no em-dash mention in output: $(echo "$OUT" | head -c 120)"
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# ── Banned-font detection ─────────────────────────────────────────────
|
|
97
|
+
TMP3=$(mktmp)
|
|
98
|
+
cat > "$TMP3/font.css" <<'EOF'
|
|
99
|
+
body { font-family: "Inter", sans-serif; }
|
|
100
|
+
EOF
|
|
101
|
+
EXIT_CODE=0
|
|
102
|
+
$NODE "$SLOP_DETECT" "$TMP3/font.css" >/dev/null 2>&1 || EXIT_CODE=$?
|
|
103
|
+
if [ "$EXIT_CODE" = "1" ]; then
|
|
104
|
+
pass "exits 1 on banned font (Inter) in CSS"
|
|
105
|
+
else
|
|
106
|
+
fail_case "banned-font detection" "expected exit 1, got $EXIT_CODE"
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# ── Purple-blue gradient detection ────────────────────────────────────
|
|
110
|
+
TMP4=$(mktmp)
|
|
111
|
+
cat > "$TMP4/gradient.tsx" <<'EOF'
|
|
112
|
+
export default function Hero() {
|
|
113
|
+
return <div className="bg-gradient-to-r from-blue-500 to-purple-600">Hi</div>;
|
|
114
|
+
}
|
|
115
|
+
EOF
|
|
116
|
+
EXIT_CODE=0
|
|
117
|
+
$NODE "$SLOP_DETECT" "$TMP4/gradient.tsx" >/dev/null 2>&1 || EXIT_CODE=$?
|
|
118
|
+
if [ "$EXIT_CODE" = "1" ]; then
|
|
119
|
+
pass "exits 1 on purple-blue gradient (the #1 AI-design tell)"
|
|
120
|
+
else
|
|
121
|
+
fail_case "gradient detection" "expected exit 1, got $EXIT_CODE"
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# ── Existing fixture: skills/qualia-polish-loop/fixtures/broken.html ──
|
|
125
|
+
FIXTURE="$(cd "$(dirname "$0")/.." && pwd)/skills/qualia-polish-loop/fixtures/broken.html"
|
|
126
|
+
if [ -f "$FIXTURE" ]; then
|
|
127
|
+
EXIT_CODE=0
|
|
128
|
+
$NODE "$SLOP_DETECT" "$FIXTURE" >/dev/null 2>&1 || EXIT_CODE=$?
|
|
129
|
+
if [ "$EXIT_CODE" = "1" ]; then
|
|
130
|
+
pass "exits 1 on the broken.html fixture (designed to hit critical bans)"
|
|
131
|
+
else
|
|
132
|
+
fail_case "fixture detection" "broken.html fixture exited $EXIT_CODE; expected 1"
|
|
133
|
+
fi
|
|
134
|
+
else
|
|
135
|
+
echo " - broken.html fixture not present, skipping"
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
# ── --json flag produces JSON output ─────────────────────────────────
|
|
139
|
+
TMP5=$(mktmp)
|
|
140
|
+
cp "$TMP3/font.css" "$TMP5/font.css"
|
|
141
|
+
JSON_OUT=$($NODE "$SLOP_DETECT" --json "$TMP5/font.css" 2>/dev/null || true)
|
|
142
|
+
if echo "$JSON_OUT" | head -1 | grep -qE "^[\{\[]"; then
|
|
143
|
+
pass "--json flag produces JSON-shaped output"
|
|
144
|
+
else
|
|
145
|
+
fail_case "--json output" "first line is not JSON-shaped: '$(echo "$JSON_OUT" | head -c 80)'"
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# ── Invocation error: no path provided AND no default repo ───────────
|
|
149
|
+
EXIT_CODE=0
|
|
150
|
+
$NODE "$SLOP_DETECT" /nonexistent/path/that/cannot/exist >/dev/null 2>&1 || EXIT_CODE=$?
|
|
151
|
+
if [ "$EXIT_CODE" = "2" ] || [ "$EXIT_CODE" = "0" ]; then
|
|
152
|
+
pass "handles missing path gracefully (exit=$EXIT_CODE — 0=skip, 2=invocation error)"
|
|
153
|
+
else
|
|
154
|
+
fail_case "missing path" "unexpected exit $EXIT_CODE on /nonexistent path"
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
echo ""
|
|
158
|
+
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
159
|
+
|
|
160
|
+
[ "$FAIL" = "0" ]
|