qualia-framework 7.1.0 → 7.2.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/CHANGELOG.md +54 -0
- package/FLAGS.md +10 -0
- package/README.md +8 -8
- package/bin/cli.js +48 -0
- package/bin/host-adapters.js +5 -1
- package/bin/install.js +125 -1
- package/bin/knowledge-flush.js +123 -2
- package/bin/qualia-doctor.js +249 -0
- package/bin/qualia-ui.js +186 -1
- package/bin/runtime-manifest.js +1 -0
- package/bin/statusline.js +7 -1
- package/guide.md +6 -1
- package/hooks/session-start.js +1 -0
- package/package.json +1 -1
- package/rules/infrastructure.md +4 -2
- package/skills/qualia-doctor/SKILL.md +25 -1
- package/skills/qualia-polish/SKILL.md +1 -1
- package/skills/qualia-report/SKILL.md +11 -0
- package/skills/qualia-scope/SKILL.md +2 -2
- package/skills/qualia-secure/SKILL.md +1 -1
- package/skills/qualia-ship/SKILL.md +15 -0
- package/skills/zoho-workflow/SKILL.md +8 -0
- package/tests/fixtures/r6-golden-fail-panel.json +23 -0
- package/tests/journey-spine.test.sh +171 -0
- package/tests/lib.test.sh +2 -2
- package/tests/memory-loop.test.sh +136 -0
- package/tests/r6-golden.test.sh +66 -0
- package/tests/refs.test.sh +33 -0
- package/tests/run-all.sh +3 -0
|
@@ -222,6 +222,21 @@ fi
|
|
|
222
222
|
```
|
|
223
223
|
Do NOT manually edit STATE.md or tracking.json — state.js handles both.
|
|
224
224
|
|
|
225
|
+
```bash
|
|
226
|
+
# Per-phase celebration card. Reads the freshly-updated state so the phase
|
|
227
|
+
# number, name, and milestone progress bar are accurate. Streak = phases this
|
|
228
|
+
# employee shipped today (from the trace log); omitted if unavailable. Every
|
|
229
|
+
# metric self-omits when missing, so this never fabricates.
|
|
230
|
+
PHASE_NUM=$(node ${QUALIA_BIN}/state.js check 2>/dev/null | node -pe 'try{JSON.parse(require("fs").readFileSync(0)).phase||""}catch{""}')
|
|
231
|
+
PHASE_NAME=$(node ${QUALIA_BIN}/state.js check 2>/dev/null | node -pe 'try{JSON.parse(require("fs").readFileSync(0)).phase_name||""}catch{""}')
|
|
232
|
+
TOTAL_PHASES=$(node ${QUALIA_BIN}/state.js check 2>/dev/null | node -pe 'try{JSON.parse(require("fs").readFileSync(0)).total_phases||""}catch{""}')
|
|
233
|
+
node ${QUALIA_BIN}/qualia-ui.js phase-complete "${PHASE_NUM:-?}" "$PHASE_NAME" \
|
|
234
|
+
${TASKS_DONE:+tasks=$TASKS_DONE} \
|
|
235
|
+
${PHASE_NUM:+mdone=$PHASE_NUM} ${TOTAL_PHASES:+mtotal=$TOTAL_PHASES} \
|
|
236
|
+
${STREAK:+streak=$STREAK} \
|
|
237
|
+
next=/qualia-handoff
|
|
238
|
+
```
|
|
239
|
+
|
|
225
240
|
```bash
|
|
226
241
|
node ${QUALIA_BIN}/qualia-ui.js end "SHIPPED" "/qualia-handoff"
|
|
227
242
|
```
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
name: zoho-workflow
|
|
3
3
|
description: "Zoho Invoice + Mail ops via ERP-first routing. Invoices from templates, cover emails, contacts, inbox, payment reminders. Triggers: 'invoice this client', 'send an email', 'check inbox', 'create a Zoho contact', 'payment reminder'."
|
|
4
4
|
tags: [zoho, invoice, email, billing, crm]
|
|
5
|
+
# Money-moving + client-comms skill — least-privilege scope. ERP-first routing
|
|
6
|
+
# (mcp__qualia-erp__*) is preferred; raw Zoho Books/Mail are the fallback surface.
|
|
7
|
+
allowed-tools:
|
|
8
|
+
- Bash
|
|
9
|
+
- Read
|
|
10
|
+
- mcp__qualia-erp__*
|
|
11
|
+
- mcp__claude_ai_Zoho_Books__*
|
|
12
|
+
- mcp__claude_ai_Mail__*
|
|
5
13
|
---
|
|
6
14
|
|
|
7
15
|
# Zoho Workflow
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "R6 golden verifier fixture — a SEEDED STUB the verifier MUST score FAIL. This proves the deterministic verifier (bin/verify-panel.js) cannot default-pass. The fixture contains (a) a surviving CRITICAL finding voted real by majority, and (b) an unvoted HIGH (INSUFFICIENT-EVIDENCE-style: unverified != disproven). Both survive the skeptic round, so the verdict MUST be FAIL (exit 1). If verify-panel ever regresses to a default-3/PASS, r6-golden.test.sh turns red.",
|
|
3
|
+
"phase": 99,
|
|
4
|
+
"lenses": ["security", "correctness"],
|
|
5
|
+
"findings": [
|
|
6
|
+
{
|
|
7
|
+
"lens": "security",
|
|
8
|
+
"file": "lib/supabase/client.ts",
|
|
9
|
+
"line": 12,
|
|
10
|
+
"severity": "CRITICAL",
|
|
11
|
+
"title": "service_role key imported into a client component (auth bypass)",
|
|
12
|
+
"votes": { "real": 3, "notReal": 0 }
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"lens": "correctness",
|
|
16
|
+
"file": "app/api/checkout/route.ts",
|
|
17
|
+
"line": 47,
|
|
18
|
+
"severity": "HIGH",
|
|
19
|
+
"title": "INSUFFICIENT EVIDENCE — payment idempotency unverified, no test backs the happy path",
|
|
20
|
+
"votes": { "real": 0, "notReal": 0 }
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# journey-spine.test.sh — lifecycle cosmetic layer (bin/qualia-ui.js spine/onboard/
|
|
3
|
+
# phase-complete/clockout + barTicks + statusline progress bar).
|
|
4
|
+
# Run: bash tests/journey-spine.test.sh
|
|
5
|
+
|
|
6
|
+
PASS=0
|
|
7
|
+
FAIL=0
|
|
8
|
+
BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
|
|
9
|
+
NODE="${NODE:-node}"
|
|
10
|
+
UI="$BIN_DIR/qualia-ui.js"
|
|
11
|
+
STATE="$BIN_DIR/state.js"
|
|
12
|
+
STATUSLINE="$BIN_DIR/statusline.js"
|
|
13
|
+
|
|
14
|
+
# strip ANSI for column-accurate assertions
|
|
15
|
+
strip() { sed 's/\x1b\[[0-9;]*m//g'; }
|
|
16
|
+
|
|
17
|
+
assert_contains() { if echo "$2" | strip | grep -qF -- "$3"; then echo " ✓ $1"; PASS=$((PASS+1)); else echo " ✗ $1 (missing '$3')"; FAIL=$((FAIL+1)); fi; }
|
|
18
|
+
assert_not_contains() { if echo "$2" | strip | grep -qF -- "$3"; then echo " ✗ $1 (should not contain '$3')"; FAIL=$((FAIL+1)); else echo " ✓ $1"; PASS=$((PASS+1)); fi; }
|
|
19
|
+
|
|
20
|
+
echo "journey-spine.test.sh — lifecycle cosmetic layer"
|
|
21
|
+
echo ""
|
|
22
|
+
|
|
23
|
+
$NODE -c "$UI" 2>/dev/null && { echo " ✓ qualia-ui syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ qualia-ui syntax invalid"; FAIL=$((FAIL+1)); }
|
|
24
|
+
|
|
25
|
+
# ── barTicks: scaling + clamping ──
|
|
26
|
+
TICKS=$($NODE -e 'const u=require(process.argv[1]); console.log(["a"+u.barTicks(4,5),"b"+u.barTicks(0,5),"c"+u.barTicks(5,5),"d"+u.barTicks(9,5),"e"+u.barTicks(2,0)].join("\n"))' "$UI" | strip)
|
|
27
|
+
assert_contains "barTicks 4/5 → 4 filled" "$TICKS" "a▰▰▰▰▱"
|
|
28
|
+
assert_contains "barTicks 0/5 → all empty" "$TICKS" "b▱▱▱▱▱"
|
|
29
|
+
assert_contains "barTicks 5/5 → all filled" "$TICKS" "c▰▰▰▰▰"
|
|
30
|
+
assert_contains "barTicks clamps over-fill to 5" "$TICKS" "d▰▰▰▰▰"
|
|
31
|
+
assert_contains "barTicks total<1 → empty string" "$TICKS" "e"
|
|
32
|
+
|
|
33
|
+
# ── onboard card ──
|
|
34
|
+
ONB=$($NODE "$UI" onboard "Maria" 2>&1)
|
|
35
|
+
assert_contains "onboard greets by name" "$ONB" "welcome aboard, Maria"
|
|
36
|
+
assert_contains "onboard shows milestone flow" "$ONB" "Plan"
|
|
37
|
+
assert_contains "onboard shows the first move" "$ONB" "/qualia"
|
|
38
|
+
|
|
39
|
+
# ── phase-complete card ──
|
|
40
|
+
PC=$($NODE "$UI" phase-complete 3 "Checkout flow" tasks=6 mdone=4 mtotal=5 streak=3 next=/qualia-build nextname="Order confirmation" 2>&1)
|
|
41
|
+
assert_contains "phase-complete shows phase + name" "$PC" "PHASE 3 COMPLETE"
|
|
42
|
+
assert_contains "phase-complete shows task count" "$PC" "6 tasks"
|
|
43
|
+
assert_contains "phase-complete shows milestone bar" "$PC" "4/5 phases"
|
|
44
|
+
assert_contains "phase-complete 'one more' at 4/5" "$PC" "one more to close the milestone"
|
|
45
|
+
assert_contains "phase-complete shows streak" "$PC" "3 phases shipped today"
|
|
46
|
+
assert_contains "phase-complete shows next command" "$PC" "/qualia-build"
|
|
47
|
+
# streak of 1 must NOT render (not a streak)
|
|
48
|
+
PC1=$($NODE "$UI" phase-complete 1 "Cart" streak=1 2>&1)
|
|
49
|
+
assert_not_contains "phase-complete hides streak=1" "$PC1" "shipped today"
|
|
50
|
+
# milestone complete wording at full
|
|
51
|
+
PCFULL=$($NODE "$UI" phase-complete 5 "Polish" mdone=5 mtotal=5 2>&1)
|
|
52
|
+
assert_contains "phase-complete 'ready to close' at 5/5" "$PCFULL" "milestone ready to close"
|
|
53
|
+
|
|
54
|
+
# ── clockout card ──
|
|
55
|
+
CO=$($NODE "$UI" clockout "Maria" date="Thu Jun 25" phases=3 tasks=14 commits=9 mdone=4 mtotal=5 streak=4 erp=ok 2>&1)
|
|
56
|
+
assert_contains "clockout names the employee" "$CO" "CLOCK OUT"
|
|
57
|
+
assert_contains "clockout shows shipped phases" "$CO" "3 phases"
|
|
58
|
+
assert_contains "clockout shows commits" "$CO" "9 commits"
|
|
59
|
+
assert_contains "clockout shows milestone %" "$CO" "80%"
|
|
60
|
+
assert_contains "clockout shows streak" "$CO" "4 days"
|
|
61
|
+
assert_contains "clockout confirms ERP upload" "$CO" "uploaded to ERP"
|
|
62
|
+
CO_Q=$($NODE "$UI" clockout "Maria" erp=queued 2>&1)
|
|
63
|
+
assert_contains "clockout queued state" "$CO_Q" "queued"
|
|
64
|
+
|
|
65
|
+
# ── spine: needs a multi-milestone project fixture ──
|
|
66
|
+
TMP=$(mktemp -d)
|
|
67
|
+
(
|
|
68
|
+
cd "$TMP"
|
|
69
|
+
$NODE "$STATE" init --project "acme-portal" \
|
|
70
|
+
--phases '[{"name":"Cart","goal":"x"},{"name":"Checkout","goal":"y"},{"name":"Receipts","goal":"z"}]' >/dev/null 2>&1
|
|
71
|
+
mkdir -p .planning
|
|
72
|
+
cat > .planning/JOURNEY.md <<'JEOF'
|
|
73
|
+
---
|
|
74
|
+
project: "acme-portal"
|
|
75
|
+
---
|
|
76
|
+
## Milestone 1 · Foundations
|
|
77
|
+
1. **Auth**
|
|
78
|
+
## Milestone 2 · Commerce
|
|
79
|
+
1. **Cart**
|
|
80
|
+
## Milestone 3 · Growth
|
|
81
|
+
1. **Referrals**
|
|
82
|
+
## Milestone 4 · Handoff
|
|
83
|
+
1. **Docs**
|
|
84
|
+
JEOF
|
|
85
|
+
)
|
|
86
|
+
SPINE=$(cd "$TMP" && $NODE "$UI" spine 2>&1)
|
|
87
|
+
assert_contains "spine renders the Journey label" "$SPINE" "Journey"
|
|
88
|
+
assert_contains "spine renders all 4 milestones" "$SPINE" "M4"
|
|
89
|
+
assert_contains "spine shows current marker ◆" "$SPINE" "◆"
|
|
90
|
+
assert_contains "spine shows 'you are here'" "$SPINE" "you are here"
|
|
91
|
+
|
|
92
|
+
# caret column == current-marker column (exact alignment)
|
|
93
|
+
ALIGN=$(cd "$TMP" && $NODE "$UI" spine 2>&1 | strip | $NODE -e '
|
|
94
|
+
let s=""; process.stdin.on("data",d=>s+=d).on("end",()=>{
|
|
95
|
+
const lines=s.split("\n").filter(l=>l.length);
|
|
96
|
+
const spine=lines.find(l=>l.includes("Journey"));
|
|
97
|
+
const caret=lines.find(l=>l.includes("you are here"));
|
|
98
|
+
if(!spine||!caret){console.log("NOLINES");return;}
|
|
99
|
+
const markerCol=spine.indexOf("◆");
|
|
100
|
+
const caretCol=caret.indexOf("↑");
|
|
101
|
+
console.log(markerCol===caretCol?"ALIGNED":("MISALIGNED "+markerCol+" vs "+caretCol));
|
|
102
|
+
});
|
|
103
|
+
')
|
|
104
|
+
assert_contains "spine caret aligns under current marker" "$ALIGN" "ALIGNED"
|
|
105
|
+
|
|
106
|
+
# spine self-skips with <2 milestones (single-milestone project)
|
|
107
|
+
TMP2=$(mktemp -d)
|
|
108
|
+
( cd "$TMP2" && $NODE "$STATE" init --project "solo" --phases '[{"name":"A","goal":"x"}]' >/dev/null 2>&1 )
|
|
109
|
+
SPINE2=$(cd "$TMP2" && $NODE "$UI" spine 2>&1)
|
|
110
|
+
assert_not_contains "spine self-skips single-milestone project" "$SPINE2" "you are here"
|
|
111
|
+
|
|
112
|
+
# spine outside any project → no crash, no output
|
|
113
|
+
SPINE3=$(cd / && $NODE "$UI" spine 2>&1)
|
|
114
|
+
assert_not_contains "spine no-ops outside a project" "$SPINE3" "Journey"
|
|
115
|
+
|
|
116
|
+
# ── statusline: the always-visible progress bar ──
|
|
117
|
+
TMP3=$(mktemp -d); mkdir -p "$TMP3/.planning"
|
|
118
|
+
cat > "$TMP3/.planning/tracking.json" <<'TEOF'
|
|
119
|
+
{"phase":3,"total_phases":5,"status":"building","milestone":2,"milestone_name":"Commerce","tasks_done":2,"tasks_total":6,"blockers":[]}
|
|
120
|
+
TEOF
|
|
121
|
+
SL=$(printf '{"workspace":{"current_dir":"%s"},"cost":{"total_cost_usd":0},"duration_ms":0}' "$TMP3" | $NODE "$STATUSLINE" 2>&1)
|
|
122
|
+
assert_contains "statusline shows P3/5" "$SL" "P3/5"
|
|
123
|
+
assert_contains "statusline shows the tick bar" "$SL" "▰▰▰▱▱"
|
|
124
|
+
|
|
125
|
+
# ── skill-wiring smoke: run the ACTUAL ship/report closing bash ──
|
|
126
|
+
# These mirror the exact variable plumbing in skills/qualia-ship/SKILL.md and
|
|
127
|
+
# skills/qualia-report/SKILL.md (state.js parsing + ${VAR:+…} expansions), so a
|
|
128
|
+
# regression in the wiring — not just the renderer — is caught.
|
|
129
|
+
TMP4=$(mktemp -d)
|
|
130
|
+
( cd "$TMP4" && $NODE "$STATE" init --project "wired" \
|
|
131
|
+
--phases '[{"name":"Cart","goal":"x"},{"name":"Checkout","goal":"y"},{"name":"Receipts","goal":"z"}]' >/dev/null 2>&1 )
|
|
132
|
+
|
|
133
|
+
# ---- /qualia-ship closing block (verbatim plumbing) ----
|
|
134
|
+
SHIP_OUT=$(cd "$TMP4" && {
|
|
135
|
+
PHASE_NUM=$($NODE "$STATE" check 2>/dev/null | $NODE -pe 'try{JSON.parse(require("fs").readFileSync(0)).phase||""}catch{""}')
|
|
136
|
+
PHASE_NAME=$($NODE "$STATE" check 2>/dev/null | $NODE -pe 'try{JSON.parse(require("fs").readFileSync(0)).phase_name||""}catch{""}')
|
|
137
|
+
TOTAL_PHASES=$($NODE "$STATE" check 2>/dev/null | $NODE -pe 'try{JSON.parse(require("fs").readFileSync(0)).total_phases||""}catch{""}')
|
|
138
|
+
TASKS_DONE=4
|
|
139
|
+
$NODE "$UI" phase-complete "${PHASE_NUM:-?}" "$PHASE_NAME" \
|
|
140
|
+
${TASKS_DONE:+tasks=$TASKS_DONE} \
|
|
141
|
+
${PHASE_NUM:+mdone=$PHASE_NUM} ${TOTAL_PHASES:+mtotal=$TOTAL_PHASES} \
|
|
142
|
+
next=/qualia-handoff
|
|
143
|
+
} 2>&1)
|
|
144
|
+
assert_contains "ship wiring → resolves phase number into card" "$SHIP_OUT" "PHASE 1 COMPLETE"
|
|
145
|
+
assert_contains "ship wiring → tasks plumbed through" "$SHIP_OUT" "4 tasks"
|
|
146
|
+
assert_contains "ship wiring → milestone bar plumbed" "$SHIP_OUT" "phases"
|
|
147
|
+
assert_contains "ship wiring → next command set" "$SHIP_OUT" "/qualia-handoff"
|
|
148
|
+
assert_not_contains "ship wiring → no literal \${VAR" "$SHIP_OUT" '${'
|
|
149
|
+
|
|
150
|
+
# ---- /qualia-report closing block (verbatim plumbing) ----
|
|
151
|
+
REPORT_OUT=$(cd "$TMP4" && {
|
|
152
|
+
EMP_NAME=$($NODE -pe 'try{JSON.parse(require("fs").readFileSync(process.env.HOME+"/.claude/.qualia-config.json","utf8")).installed_by||""}catch{""}' 2>/dev/null)
|
|
153
|
+
MDONE=$($NODE "$STATE" check 2>/dev/null | $NODE -pe 'try{JSON.parse(require("fs").readFileSync(0)).phase||""}catch{""}')
|
|
154
|
+
MTOTAL=$($NODE "$STATE" check 2>/dev/null | $NODE -pe 'try{JSON.parse(require("fs").readFileSync(0)).total_phases||""}catch{""}')
|
|
155
|
+
COUNT=9
|
|
156
|
+
ERP_RESULT=ok
|
|
157
|
+
$NODE "$UI" clockout "$EMP_NAME" date="2026-06-25" \
|
|
158
|
+
${COUNT:+commits=$COUNT} \
|
|
159
|
+
${MDONE:+mdone=$MDONE} ${MTOTAL:+mtotal=$MTOTAL} \
|
|
160
|
+
${ERP_RESULT:+erp=$ERP_RESULT}
|
|
161
|
+
} 2>&1)
|
|
162
|
+
assert_contains "report wiring → renders CLOCK OUT" "$REPORT_OUT" "CLOCK OUT"
|
|
163
|
+
assert_contains "report wiring → commit count plumbed" "$REPORT_OUT" "9 commits"
|
|
164
|
+
assert_contains "report wiring → milestone % plumbed" "$REPORT_OUT" "%"
|
|
165
|
+
assert_contains "report wiring → erp result plumbed" "$REPORT_OUT" "uploaded to ERP"
|
|
166
|
+
assert_not_contains "report wiring → no literal \${VAR" "$REPORT_OUT" '${'
|
|
167
|
+
|
|
168
|
+
rm -rf "$TMP" "$TMP2" "$TMP3" "$TMP4"
|
|
169
|
+
echo ""
|
|
170
|
+
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
171
|
+
[ "$FAIL" -eq 0 ]
|
package/tests/lib.test.sh
CHANGED
|
@@ -532,7 +532,7 @@ TMP=$(mktmp)
|
|
|
532
532
|
mkdir -p "$TMP/home/.claude/bin" "$TMP/home/.claude/hooks" "$TMP/home/.claude/knowledge/daily-log" "$TMP/home/.claude/qualia-design" "$TMP/home/.claude/agents" "$TMP/home/.claude/qualia-templates" "$TMP/project"
|
|
533
533
|
echo '{"installed_by":"Test","role":"OWNER","version":"6.3.0","erp":{"enabled":false}}' > "$TMP/home/.claude/.qualia-config.json"
|
|
534
534
|
touch "$TMP/home/.claude/CLAUDE.md" "$TMP/home/.claude/settings.json"
|
|
535
|
-
for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js recall.js vault-access.js repo-map.js design-tokens.js batch-plan.js state-ledger.js plan-contract.js contract-runner.js agent-status.js analyze-gate.js verify-panel.js wave-plan.js eval-runner.js branch-hygiene.js last-report.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs dep-verify.mjs erp-retry.js erp-event.js work-packet.js report-payload.js project-snapshot.js project-sync.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js auto-report.js; do
|
|
535
|
+
for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js recall.js vault-access.js repo-map.js design-tokens.js batch-plan.js state-ledger.js plan-contract.js contract-runner.js agent-status.js analyze-gate.js verify-panel.js wave-plan.js eval-runner.js branch-hygiene.js last-report.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs dep-verify.mjs erp-retry.js erp-event.js work-packet.js report-payload.js project-snapshot.js project-sync.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js qualia-doctor.js auto-report.js; do
|
|
536
536
|
touch "$TMP/home/.claude/bin/$f"
|
|
537
537
|
done
|
|
538
538
|
for h in session-start.js auto-update.js branch-guard.js pre-push.js pre-deploy-gate.js migration-guard.js git-guardrails.js stop-session-log.js fawzi-approval-guard.js vercel-account-guard.js env-empty-guard.js supabase-destructive-guard.js secret-guard.js; do
|
|
@@ -647,7 +647,7 @@ TMP=$(mktmp)
|
|
|
647
647
|
mkdir -p "$TMP/.claude/bin" "$TMP/.claude/hooks" "$TMP/.claude/knowledge/daily-log" "$TMP/.claude/qualia-design" "$TMP/.claude/agents" "$TMP/.claude/qualia-templates" "$TMP/project/.planning"
|
|
648
648
|
echo '{"installed_by":"Test","role":"OWNER","erp":{"enabled":false}}' > "$TMP/.claude/.qualia-config.json"
|
|
649
649
|
touch "$TMP/.claude/CLAUDE.md" "$TMP/.claude/settings.json"
|
|
650
|
-
for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js recall.js vault-access.js repo-map.js design-tokens.js batch-plan.js state-ledger.js plan-contract.js contract-runner.js agent-status.js analyze-gate.js verify-panel.js wave-plan.js eval-runner.js branch-hygiene.js last-report.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs dep-verify.mjs erp-retry.js erp-event.js work-packet.js report-payload.js project-snapshot.js project-sync.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js auto-report.js; do
|
|
650
|
+
for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js recall.js vault-access.js repo-map.js design-tokens.js batch-plan.js state-ledger.js plan-contract.js contract-runner.js agent-status.js analyze-gate.js verify-panel.js wave-plan.js eval-runner.js branch-hygiene.js last-report.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs dep-verify.mjs erp-retry.js erp-event.js work-packet.js report-payload.js project-snapshot.js project-sync.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js qualia-doctor.js auto-report.js; do
|
|
651
651
|
touch "$TMP/.claude/bin/$f"
|
|
652
652
|
done
|
|
653
653
|
for h in session-start.js auto-update.js branch-guard.js pre-push.js pre-deploy-gate.js migration-guard.js git-guardrails.js stop-session-log.js fawzi-approval-guard.js vercel-account-guard.js env-empty-guard.js supabase-destructive-guard.js secret-guard.js; do
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# memory-loop.test.sh — the one-loop contract: knowledge-flush vault dual-write
|
|
3
|
+
# (ITEM 13) + qualia-doctor freshness gates (ITEM 14b).
|
|
4
|
+
#
|
|
5
|
+
# Pure-function tests, no agent CLI, no live vault. Each builds a temp QUALIA_HOME
|
|
6
|
+
# and QUALIA_MEMORY so the loop logic is exercised hermetically.
|
|
7
|
+
#
|
|
8
|
+
# Run: bash tests/memory-loop.test.sh
|
|
9
|
+
|
|
10
|
+
PASS=0
|
|
11
|
+
FAIL=0
|
|
12
|
+
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
13
|
+
BIN_DIR="$(cd "$DIR/../bin" && pwd)"
|
|
14
|
+
NODE="${NODE:-node}"
|
|
15
|
+
FLUSH="$BIN_DIR/knowledge-flush.js"
|
|
16
|
+
DOCTOR="$BIN_DIR/qualia-doctor.js"
|
|
17
|
+
|
|
18
|
+
assert_contains() {
|
|
19
|
+
local name="$1" hay="$2" needle="$3"
|
|
20
|
+
if echo "$hay" | grep -qF "$needle"; then echo " ✓ $name"; PASS=$((PASS+1));
|
|
21
|
+
else echo " ✗ $name (missing '$needle' in: $hay)"; FAIL=$((FAIL+1)); fi
|
|
22
|
+
}
|
|
23
|
+
assert_ok() {
|
|
24
|
+
local name="$1" rc="$2"
|
|
25
|
+
if [ "$rc" -eq 0 ]; then echo " ✓ $name"; PASS=$((PASS+1));
|
|
26
|
+
else echo " ✗ $name (rc=$rc)"; FAIL=$((FAIL+1)); fi
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
echo "memory-loop.test.sh — dual-write + freshness gates"
|
|
30
|
+
echo ""
|
|
31
|
+
|
|
32
|
+
# --- syntax ---
|
|
33
|
+
$NODE -c "$FLUSH" 2>/dev/null && { echo " ✓ knowledge-flush.js syntax"; PASS=$((PASS+1)); } || { echo " ✗ knowledge-flush.js syntax"; FAIL=$((FAIL+1)); }
|
|
34
|
+
$NODE -c "$DOCTOR" 2>/dev/null && { echo " ✓ qualia-doctor.js syntax"; PASS=$((PASS+1)); } || { echo " ✗ qualia-doctor.js syntax"; FAIL=$((FAIL+1)); }
|
|
35
|
+
|
|
36
|
+
# ── ITEM 13: dualWriteVault mirrors curated concepts into the vault ──────────
|
|
37
|
+
TMP=$(mktemp -d); mkdir -p "$TMP/vault"
|
|
38
|
+
OUT=$(QUALIA_HOME="$TMP/home" QUALIA_MEMORY="$TMP/vault" $NODE -e '
|
|
39
|
+
const fs=require("fs"),path=require("path");
|
|
40
|
+
const kh=process.env.QUALIA_HOME;
|
|
41
|
+
fs.mkdirSync(path.join(kh,"knowledge","concepts"),{recursive:true});
|
|
42
|
+
fs.writeFileSync(path.join(kh,"knowledge","learned-patterns.md"),"# Patterns\n- always RLS\n");
|
|
43
|
+
fs.writeFileSync(path.join(kh,"knowledge","concepts","voice-call-state.md"),"# Voice\nstate\n");
|
|
44
|
+
const m=require("'"$FLUSH"'");
|
|
45
|
+
const r1=m.dualWriteVault();
|
|
46
|
+
const files1=fs.readdirSync(m.VAULT_CONCEPTS_DIR).sort();
|
|
47
|
+
const r2=m.dualWriteVault();
|
|
48
|
+
const files2=fs.readdirSync(m.VAULT_CONCEPTS_DIR).sort();
|
|
49
|
+
console.log("detail="+r1.event_detail);
|
|
50
|
+
console.log("written="+r1.written);
|
|
51
|
+
console.log("idempotent="+(files1.length===files2.length && files1.join(",")===files2.join(",")));
|
|
52
|
+
console.log("files="+files2.join(","));
|
|
53
|
+
const sample=fs.readFileSync(path.join(m.VAULT_CONCEPTS_DIR,files2[0]),"utf8");
|
|
54
|
+
console.log("hasFrontmatter="+sample.includes("source: qualia-framework/knowledge-flush"));
|
|
55
|
+
' 2>&1)
|
|
56
|
+
assert_contains "dual-write mirrors concepts (vault-mirrored)" "$OUT" "detail=vault-mirrored"
|
|
57
|
+
assert_contains "dual-write wrote 2 files" "$OUT" "written=2"
|
|
58
|
+
assert_contains "dual-write is idempotent" "$OUT" "idempotent=true"
|
|
59
|
+
assert_contains "dual-write emits a concept-prefixed subdir file" "$OUT" "concept-voice-call-state.md"
|
|
60
|
+
assert_contains "dual-write emits curated top-level file" "$OUT" "learned-patterns.md"
|
|
61
|
+
assert_contains "dual-write stamps provenance front-matter" "$OUT" "hasFrontmatter=true"
|
|
62
|
+
rm -rf "$TMP"
|
|
63
|
+
|
|
64
|
+
# --- vault absent → graceful skip, no crash ---
|
|
65
|
+
TMP=$(mktemp -d)
|
|
66
|
+
OUT=$(QUALIA_HOME="$TMP/home" QUALIA_MEMORY="$TMP/does-not-exist" $NODE -e '
|
|
67
|
+
const fs=require("fs"),path=require("path");
|
|
68
|
+
const kh=process.env.QUALIA_HOME;
|
|
69
|
+
fs.mkdirSync(path.join(kh,"knowledge"),{recursive:true});
|
|
70
|
+
fs.writeFileSync(path.join(kh,"knowledge","learned-patterns.md"),"# P\n- x\n");
|
|
71
|
+
const m=require("'"$FLUSH"'");
|
|
72
|
+
const r=m.dualWriteVault();
|
|
73
|
+
console.log("detail="+r.event_detail+" written="+r.written);
|
|
74
|
+
' 2>&1)
|
|
75
|
+
assert_ok "vault-absent does not crash" $?
|
|
76
|
+
assert_contains "vault-absent is skipped gracefully" "$OUT" "detail=vault-absent"
|
|
77
|
+
rm -rf "$TMP"
|
|
78
|
+
|
|
79
|
+
# ── ITEM 14b: freshness gates ───────────────────────────────────────────────
|
|
80
|
+
# Healthy loop: fresh daily-log, recent flush log, fresh export with matching
|
|
81
|
+
# coverage and no deprecated rows → all PASS.
|
|
82
|
+
TMP=$(mktemp -d)
|
|
83
|
+
KH="$TMP/home"; VAULT="$TMP/vault"
|
|
84
|
+
mkdir -p "$KH/knowledge/daily-log"
|
|
85
|
+
TODAY=$(date +%Y-%m-%d)
|
|
86
|
+
echo "# log" > "$KH/knowledge/daily-log/$TODAY.md"
|
|
87
|
+
printf '{"timestamp":"%s","event":"ok"}\n' "$(date -Iseconds)" > "$KH/.qualia-flush.log"
|
|
88
|
+
mkdir -p "$VAULT/wiki/sessions/concepts" "$VAULT/wiki/_export/concepts"
|
|
89
|
+
echo "# c1" > "$VAULT/wiki/sessions/concepts/c1.md"
|
|
90
|
+
echo "# c1 exported" > "$VAULT/wiki/_export/concepts/c1.md"
|
|
91
|
+
OUT=$(QUALIA_HOME="$KH" QUALIA_MEMORY="$VAULT" $NODE "$DOCTOR" --json 2>&1)
|
|
92
|
+
assert_contains "healthy daily-log → PASS" "$OUT" '"gate": "daily-log freshness"'
|
|
93
|
+
HEALTHY_FAILS=$(echo "$OUT" | grep -c '"status": "FAIL"')
|
|
94
|
+
if [ "$HEALTHY_FAILS" -eq 0 ]; then echo " ✓ healthy loop has zero FAIL gates"; PASS=$((PASS+1)); else echo " ✗ healthy loop has $HEALTHY_FAILS FAIL gate(s): $OUT"; FAIL=$((FAIL+1)); fi
|
|
95
|
+
rm -rf "$TMP"
|
|
96
|
+
|
|
97
|
+
# Broken loop: no daily-log, no flush log, no export → FAILs + --exit-code 1.
|
|
98
|
+
TMP=$(mktemp -d)
|
|
99
|
+
mkdir -p "$TMP/home/knowledge" "$TMP/vault"
|
|
100
|
+
OUT=$(QUALIA_HOME="$TMP/home" QUALIA_MEMORY="$TMP/vault" $NODE "$DOCTOR" --json 2>&1)
|
|
101
|
+
assert_contains "broken loop flags stale daily-log" "$OUT" 'no daily-log entries'
|
|
102
|
+
assert_contains "broken loop flags missing flush log" "$OUT" 'flush never ran'
|
|
103
|
+
assert_contains "broken loop flags missing export" "$OUT" 'export never ran'
|
|
104
|
+
QUALIA_HOME="$TMP/home" QUALIA_MEMORY="$TMP/vault" $NODE "$DOCTOR" --exit-code >/dev/null 2>&1
|
|
105
|
+
RC=$?
|
|
106
|
+
if [ "$RC" -eq 1 ]; then echo " ✓ broken loop --exit-code returns 1"; PASS=$((PASS+1)); else echo " ✗ broken loop --exit-code returned $RC"; FAIL=$((FAIL+1)); fi
|
|
107
|
+
rm -rf "$TMP"
|
|
108
|
+
|
|
109
|
+
# Coverage gate: source has 2 concepts, export has 1 → FAIL (stale/partial).
|
|
110
|
+
TMP=$(mktemp -d)
|
|
111
|
+
KH="$TMP/home"; VAULT="$TMP/vault"
|
|
112
|
+
mkdir -p "$KH/knowledge/daily-log"
|
|
113
|
+
echo "# log" > "$KH/knowledge/daily-log/$(date +%Y-%m-%d).md"
|
|
114
|
+
printf '{"timestamp":"%s","event":"ok"}\n' "$(date -Iseconds)" > "$KH/.qualia-flush.log"
|
|
115
|
+
mkdir -p "$VAULT/wiki/sessions/concepts" "$VAULT/wiki/_export/concepts"
|
|
116
|
+
echo "# a" > "$VAULT/wiki/sessions/concepts/a.md"
|
|
117
|
+
echo "# b" > "$VAULT/wiki/sessions/concepts/b.md"
|
|
118
|
+
echo "# a" > "$VAULT/wiki/_export/concepts/a.md"
|
|
119
|
+
OUT=$(QUALIA_HOME="$KH" QUALIA_MEMORY="$VAULT" $NODE "$DOCTOR" --json 2>&1)
|
|
120
|
+
assert_contains "partial export → coverage FAIL" "$OUT" 'export is stale or partial'
|
|
121
|
+
rm -rf "$TMP"
|
|
122
|
+
|
|
123
|
+
# Deprecated gate: an export file tagged deprecated → FAIL.
|
|
124
|
+
TMP=$(mktemp -d)
|
|
125
|
+
KH="$TMP/home"; VAULT="$TMP/vault"
|
|
126
|
+
mkdir -p "$KH/knowledge/daily-log" "$VAULT/wiki/_export"
|
|
127
|
+
echo "# log" > "$KH/knowledge/daily-log/$(date +%Y-%m-%d).md"
|
|
128
|
+
printf '{"timestamp":"%s","event":"ok"}\n' "$(date -Iseconds)" > "$KH/.qualia-flush.log"
|
|
129
|
+
printf -- '---\ntags: [deprecated, old]\n---\n# stale\n' > "$VAULT/wiki/_export/old.md"
|
|
130
|
+
OUT=$(QUALIA_HOME="$KH" QUALIA_MEMORY="$VAULT" $NODE "$DOCTOR" --json 2>&1)
|
|
131
|
+
assert_contains "deprecated row in export → FAIL" "$OUT" 'deprecated file(s)'
|
|
132
|
+
rm -rf "$TMP"
|
|
133
|
+
|
|
134
|
+
echo ""
|
|
135
|
+
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
136
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# r6-golden.test.sh — R6 golden verifier fixture.
|
|
3
|
+
#
|
|
4
|
+
# Proves the deterministic verifier (bin/verify-panel.js) CANNOT default-pass.
|
|
5
|
+
# The fixture tests/fixtures/r6-golden-fail-panel.json is a SEEDED STUB
|
|
6
|
+
# engineered so the only correct verdict is FAIL: it carries a surviving
|
|
7
|
+
# CRITICAL (voted real 3-0) plus an unvoted HIGH (INSUFFICIENT-EVIDENCE style —
|
|
8
|
+
# unverified is not disproven, so it survives the skeptic round).
|
|
9
|
+
#
|
|
10
|
+
# If verify-panel ever regresses to scoring 3/PASS on input it cannot fully
|
|
11
|
+
# clear, this turns red. It is the framework gating its OWN verifier the way it
|
|
12
|
+
# gates user projects (the missing R6 piece from the v7 audit, F3).
|
|
13
|
+
#
|
|
14
|
+
# CONSTRAINT: this test ONLY adds the fixture wiring. It does not touch the
|
|
15
|
+
# anti-default-3 logic, the gates, or Codex parity — it observes them.
|
|
16
|
+
#
|
|
17
|
+
# Run: bash tests/r6-golden.test.sh
|
|
18
|
+
|
|
19
|
+
PASS=0
|
|
20
|
+
FAIL=0
|
|
21
|
+
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
22
|
+
BIN_DIR="$(cd "$DIR/../bin" && pwd)"
|
|
23
|
+
NODE="${NODE:-node}"
|
|
24
|
+
VP="$BIN_DIR/verify-panel.js"
|
|
25
|
+
FIXTURE="$DIR/fixtures/r6-golden-fail-panel.json"
|
|
26
|
+
|
|
27
|
+
assert_exit() {
|
|
28
|
+
local name="$1" expected="$2" actual="$3"
|
|
29
|
+
if [ "$expected" = "$actual" ]; then echo " ✓ $name"; PASS=$((PASS+1));
|
|
30
|
+
else echo " ✗ $name (expected exit $expected, got $actual)"; FAIL=$((FAIL+1)); fi
|
|
31
|
+
}
|
|
32
|
+
assert_contains() {
|
|
33
|
+
local name="$1" hay="$2" needle="$3"
|
|
34
|
+
if echo "$hay" | grep -qF "$needle"; then echo " ✓ $name"; PASS=$((PASS+1));
|
|
35
|
+
else echo " ✗ $name (missing '$needle')"; FAIL=$((FAIL+1)); fi
|
|
36
|
+
}
|
|
37
|
+
assert_absent() {
|
|
38
|
+
local name="$1" hay="$2" needle="$3"
|
|
39
|
+
if echo "$hay" | grep -qF "$needle"; then echo " ✗ $name (found forbidden '$needle')"; FAIL=$((FAIL+1));
|
|
40
|
+
else echo " ✓ $name"; PASS=$((PASS+1)); fi
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
echo "r6-golden.test.sh — verifier cannot default-pass"
|
|
44
|
+
echo ""
|
|
45
|
+
|
|
46
|
+
# Fixture must exist (precondition).
|
|
47
|
+
[ -f "$FIXTURE" ] && { echo " ✓ golden fixture present"; PASS=$((PASS+1)); } \
|
|
48
|
+
|| { echo " ✗ golden fixture missing: $FIXTURE"; FAIL=$((FAIL+1)); }
|
|
49
|
+
|
|
50
|
+
# The golden stub MUST FAIL (exit 1). This is the anti-default-pass proof.
|
|
51
|
+
$NODE "$VP" "$FIXTURE" >/dev/null 2>&1
|
|
52
|
+
assert_exit "golden stub → FAIL (exit 1), not a default PASS" 1 $?
|
|
53
|
+
|
|
54
|
+
OUT=$($NODE "$VP" "$FIXTURE" --json 2>&1)
|
|
55
|
+
assert_contains "verdict is FAIL" "$OUT" '"verdict": "FAIL"'
|
|
56
|
+
assert_absent "verdict is NOT PASS" "$OUT" '"verdict": "PASS"'
|
|
57
|
+
# ok must be false — the contract kernel treats this as a phase fail.
|
|
58
|
+
assert_contains "ok:false (phase does not ship)" "$OUT" '"ok": false'
|
|
59
|
+
# The surviving CRITICAL is the load-bearing reason it cannot pass.
|
|
60
|
+
assert_contains "surviving CRITICAL preserved" "$OUT" '"severity": "CRITICAL"'
|
|
61
|
+
# The unvoted HIGH survives (unverified != disproven).
|
|
62
|
+
assert_contains "unvoted HIGH survives the skeptic round" "$OUT" '"severity": "HIGH"'
|
|
63
|
+
|
|
64
|
+
echo ""
|
|
65
|
+
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
66
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
package/tests/refs.test.sh
CHANGED
|
@@ -184,6 +184,39 @@ for pattern in "${forbidden_surface_patterns[@]}"; do
|
|
|
184
184
|
fi
|
|
185
185
|
done
|
|
186
186
|
|
|
187
|
+
# ── Count-drift guard ─────────────────────────────────────────────────────────
|
|
188
|
+
# The orientation docs (README.md / guide.md) hardcode skill/hook/rule counts.
|
|
189
|
+
# These rot every time a skill/hook/rule is added. Assert each stated count
|
|
190
|
+
# matches the live directory listing so the numbers can't silently drift again.
|
|
191
|
+
# skills/ = directory entries
|
|
192
|
+
# hooks/ = *.js files
|
|
193
|
+
# rules/ = *.md files
|
|
194
|
+
SKILL_COUNT=$(find "$FRAMEWORK_ROOT/skills" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')
|
|
195
|
+
HOOK_COUNT=$(find "$FRAMEWORK_ROOT/hooks" -maxdepth 1 -name '*.js' | wc -l | tr -d ' ')
|
|
196
|
+
RULE_COUNT=$(find "$FRAMEWORK_ROOT/rules" -maxdepth 1 -name '*.md' | wc -l | tr -d ' ')
|
|
197
|
+
|
|
198
|
+
# assert_count <label> <expected-actual> <file> <regex-with-NUM-placeholder>
|
|
199
|
+
# Fails if the file contains a "<N> <label>" claim where N != actual.
|
|
200
|
+
assert_count() {
|
|
201
|
+
local label="$1" actual="$2" file="$3" pattern="$4"
|
|
202
|
+
local bad
|
|
203
|
+
# Find every "<digits> <label>" mention; flag any whose number != actual.
|
|
204
|
+
# grep -oE (no -n) yields just the matched "<N> label" substrings; pull the
|
|
205
|
+
# leading integer from each and compare. (No line-number prefix to confuse it.)
|
|
206
|
+
bad=$(grep -oE "$pattern" "$file" 2>/dev/null | grep -oE '^[0-9]+' | grep -vx "$actual" || true)
|
|
207
|
+
if [ -n "$bad" ]; then
|
|
208
|
+
fail_case "count drift: $label in $(basename "$file")" \
|
|
209
|
+
"stated $(echo "$bad" | paste -sd, -) but actual is $actual — regenerate from the directory listing"
|
|
210
|
+
else
|
|
211
|
+
pass "$(basename "$file") $label count = $actual (matches $label/)"
|
|
212
|
+
fi
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
assert_count "skills" "$SKILL_COUNT" "$FRAMEWORK_ROOT/README.md" '[0-9]+ (installed )?skills'
|
|
216
|
+
assert_count "hooks" "$HOOK_COUNT" "$FRAMEWORK_ROOT/README.md" '[0-9]+ hooks'
|
|
217
|
+
assert_count "rules" "$RULE_COUNT" "$FRAMEWORK_ROOT/README.md" '[0-9]+ (installed )?rules'
|
|
218
|
+
assert_count "skills" "$SKILL_COUNT" "$FRAMEWORK_ROOT/guide.md" '[0-9]+ active skills'
|
|
219
|
+
|
|
187
220
|
echo ""
|
|
188
221
|
echo "Results: $PASS passed, $FAIL failed"
|
|
189
222
|
|