qualia-framework 4.3.0 → 4.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/CLAUDE.md +13 -1
- package/README.md +16 -13
- package/agents/builder.md +12 -20
- package/agents/plan-checker.md +18 -0
- package/agents/planner.md +9 -0
- package/agents/verifier.md +62 -0
- package/bin/agent-runs.js +233 -0
- package/bin/cli.js +225 -21
- package/bin/install.js +25 -5
- package/bin/plan-contract.js +220 -0
- package/bin/slop-detect.mjs +357 -0
- package/bin/state.js +199 -10
- package/docs/agent-runs.md +273 -0
- package/docs/erp-contract.md +5 -0
- package/docs/plan-contract.md +321 -0
- package/hooks/auto-update.js +3 -7
- package/hooks/pre-compact.js +22 -11
- package/hooks/pre-deploy-gate.js +16 -2
- package/hooks/pre-push.js +22 -2
- package/hooks/stop-session-log.js +1 -1
- package/package.json +8 -2
- package/rules/design-brand.md +110 -0
- package/rules/design-laws.md +144 -0
- package/rules/design-product.md +110 -0
- package/rules/design-rubric.md +153 -0
- package/skills/qualia-build/SKILL.md +5 -5
- package/skills/qualia-flush/SKILL.md +1 -1
- package/skills/qualia-new/SKILL.md +40 -3
- package/skills/qualia-polish/SKILL.md +180 -136
- package/skills/qualia-quick/SKILL.md +1 -1
- package/skills/qualia-report/SKILL.md +25 -5
- package/skills/qualia-ship/SKILL.md +12 -10
- package/skills/zoho-workflow/SKILL.md +64 -0
- package/templates/DESIGN.md +229 -435
- package/templates/PRODUCT.md +95 -0
- package/templates/help.html +13 -7
- package/tests/bin.test.sh +6 -3
- package/tests/hooks.test.sh +9 -20
- package/tests/lib.test.sh +217 -0
- package/tests/runner.js +96 -75
- package/tests/state.test.sh +4 -3
- package/skills/qualia-design/SKILL.md +0 -169
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# PRODUCT — {Project Name}
|
|
2
|
+
|
|
3
|
+
> **Required.** The "who and why" every Qualia agent reads before designing or building anything.
|
|
4
|
+
> `/qualia-new` generates this from 5 questions. Update as the product clarifies.
|
|
5
|
+
|
|
6
|
+
## Register
|
|
7
|
+
|
|
8
|
+
**One of:** `brand` · `product`
|
|
9
|
+
|
|
10
|
+
- **brand** — design IS the product (marketing, landing, campaign, portfolio). Distinctiveness is the bar.
|
|
11
|
+
- **product** — design SERVES the product (app, admin, dashboard, tool). Earned familiarity is the bar.
|
|
12
|
+
|
|
13
|
+
A project may have BOTH (e.g., marketing site + app). When that's true, list both, and note which surfaces fall under each.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
register: product
|
|
17
|
+
secondary: brand (only for /marketing/* routes)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Users (specific, not personas)
|
|
21
|
+
|
|
22
|
+
Real humans with real jobs who will use this. Not "small business owners" — Sarah, who runs a 3-person bookkeeping practice in Limassol and pays for QuickBooks because she trusts it. Three users, named, with the workflow in mind.
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
1. {Name} — {role + scenario}. {What they're trying to do when they open this}.
|
|
26
|
+
2. ...
|
|
27
|
+
3. ...
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Brand voice
|
|
31
|
+
|
|
32
|
+
How the product sounds in writing. UI strings, error messages, marketing copy. Three adjectives, then one sentence of "voice in motion."
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
Voice: {three adjectives — e.g., "direct, confident, dry"}
|
|
36
|
+
|
|
37
|
+
Voice in motion: {one paragraph showing the voice in actual sentences. What an
|
|
38
|
+
error message sounds like. What a confirmation sounds like. What the empty state
|
|
39
|
+
sounds like. Concrete, not adjectives.}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Anti-references (mandatory, 3-5)
|
|
43
|
+
|
|
44
|
+
Sites the project should NOT look like. Anti-references pin down what the design is reacting against — more useful than positive references.
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
- {URL or descriptor} — {what it gets wrong for our use case}
|
|
48
|
+
- {URL or descriptor} — ...
|
|
49
|
+
- {URL or descriptor} — ...
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
For Brand register, these are usually saturated aesthetic lanes (see `rules/design-brand.md`).
|
|
53
|
+
For Product register, these are usually patterns we don't want to inherit (e.g., "Salesforce Lightning — too dense, too many panels").
|
|
54
|
+
|
|
55
|
+
## Positive references (optional, ≤3)
|
|
56
|
+
|
|
57
|
+
Sites that demonstrate qualities we want to inherit. Less important than anti-references.
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
- {URL or app name} — {specific quality, e.g., "Linear's keyboard fluency", "Stripe's empty state copy", "Vercel's color restraint"}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Strategic principles
|
|
64
|
+
|
|
65
|
+
3-5 statements that resolve future arguments. Not feature ideas — design/UX principles that compound.
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
- {Principle}. {One-line rationale.}
|
|
69
|
+
- {Principle}. {One-line rationale.}
|
|
70
|
+
- {Principle}. {One-line rationale.}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
- **Density before delight.** Users live in this app 6+ hours/day. Density wins over flourish.
|
|
75
|
+
- **Power users are the primary persona.** Beginners get onboarding. The app optimizes for power.
|
|
76
|
+
- **Speed of glance, not depth of click.** Information visible without interaction beats information one click away.
|
|
77
|
+
|
|
78
|
+
## Constraints
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
Framework: {Next.js 16 / React 19 / Vite / Astro / etc}
|
|
82
|
+
Component library: {shadcn-ui · custom · radix · arco · ...}
|
|
83
|
+
Performance budget: {LCP < 2.5s, INP < 200ms, CLS < 0.1, etc}
|
|
84
|
+
Accessibility: WCAG 2.2 AA minimum
|
|
85
|
+
Browsers: {evergreen / IE11 / mobile-first / ...}
|
|
86
|
+
i18n: {languages supported, RTL needed?}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Differentiation (one sentence)
|
|
90
|
+
|
|
91
|
+
What someone will remember about this product 24 hours after using it for the first time. If you can't write this in one sentence, the product hasn't decided what it is yet.
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
{One sentence. What's the memory hook?}
|
|
95
|
+
```
|
package/templates/help.html
CHANGED
|
@@ -297,7 +297,7 @@
|
|
|
297
297
|
<div class="header-content">
|
|
298
298
|
<h1><span>Qualia</span> Framework</h1>
|
|
299
299
|
<p>Plan, build, verify, ship. The AI-powered workflow for Qualia Solutions.</p>
|
|
300
|
-
<div class="version">{{VERSION}} ·
|
|
300
|
+
<div class="version">{{VERSION}} · 28 skills</div>
|
|
301
301
|
</div>
|
|
302
302
|
</div>
|
|
303
303
|
|
|
@@ -328,7 +328,7 @@
|
|
|
328
328
|
|
|
329
329
|
<!-- Skills -->
|
|
330
330
|
<section>
|
|
331
|
-
<h2>Skills (
|
|
331
|
+
<h2>Skills (28)</h2>
|
|
332
332
|
|
|
333
333
|
<!-- Road (core flow) -->
|
|
334
334
|
<div class="cmd-group">
|
|
@@ -336,7 +336,7 @@
|
|
|
336
336
|
<p class="cmd-group-note">The seven steps every project walks through.</p>
|
|
337
337
|
<div class="commands">
|
|
338
338
|
<div class="cmd"><span class="cmd-name">/qualia-new</span><span class="cmd-desc">Set up a new project from scratch — deep questioning, parallel research, REQUIREMENTS.md, ROADMAP.md, approval gate. Use when starting any new client project.</span></div>
|
|
339
|
-
<div class="cmd"><span class="cmd-name">/qualia-plan</span><span class="cmd-desc">Plan the current phase — spawns planner, validates with plan-checker in a revision loop (max
|
|
339
|
+
<div class="cmd"><span class="cmd-name">/qualia-plan</span><span class="cmd-desc">Plan the current phase — spawns planner, validates with plan-checker in a revision loop (max 2), optionally runs discuss/research first. Use when ready to plan a phase.</span></div>
|
|
340
340
|
<div class="cmd"><span class="cmd-name">/qualia-build</span><span class="cmd-desc">Execute the current phase — spawns builder subagents per task with wave-based parallelization. Fresh context per task.</span></div>
|
|
341
341
|
<div class="cmd"><span class="cmd-name">/qualia-verify</span><span class="cmd-desc">Goal-backward verification — checks if the phase ACTUALLY works, not just if tasks completed. Spawns verifier agent.</span></div>
|
|
342
342
|
<div class="cmd"><span class="cmd-name">/qualia-polish</span><span class="cmd-desc">Design and UX pass — anti-AI-slop, genuine craft, responsive, accessible. Run after all phases are verified.</span></div>
|
|
@@ -365,7 +365,6 @@
|
|
|
365
365
|
<div class="cmd"><span class="cmd-name">/qualia-debug</span><span class="cmd-desc">Structured debugging — symptom gathering, diagnosis confirmation, root cause analysis. Trigger on 'debug', 'find bug', 'fix error', 'something is broken'.</span></div>
|
|
366
366
|
<div class="cmd"><span class="cmd-name">/qualia-review</span><span class="cmd-desc">Production audit with scored diagnostics. Runs real commands, scores findings by severity. Trigger on 'review', 'audit', 'code review', 'security check'.</span></div>
|
|
367
367
|
<div class="cmd"><span class="cmd-name">/qualia-optimize</span><span class="cmd-desc">Deep optimization pass — reads .planning/ AND codebase to find performance, design, UI, backend, and frontend issues. Spawns parallel specialist agents. Supports --perf, --ui, --backend, --alignment, --fix flags.</span></div>
|
|
368
|
-
<div class="cmd"><span class="cmd-name">/qualia-polish</span><span class="cmd-desc">Design and UX pass — anti-AI-slop, genuine craft, responsive, accessible. Run after all phases are verified.</span></div>
|
|
369
368
|
<div class="cmd"><span class="cmd-name">/qualia-test</span><span class="cmd-desc">Generate or run tests for client projects. Trigger on 'write tests', 'add tests', 'test this', 'test coverage'.</span></div>
|
|
370
369
|
</div>
|
|
371
370
|
</div>
|
|
@@ -387,6 +386,7 @@
|
|
|
387
386
|
<p class="cmd-group-note">Persist learnings and log work.</p>
|
|
388
387
|
<div class="commands">
|
|
389
388
|
<div class="cmd"><span class="cmd-name">/qualia-learn</span><span class="cmd-desc">Save a learning, pattern, fix, or client preference to the knowledge base. Persists across projects and sessions.</span></div>
|
|
389
|
+
<div class="cmd"><span class="cmd-name">/qualia-flush</span><span class="cmd-desc">Promote raw daily logs into curated knowledge concepts. Use weekly or when session logs contain durable lessons.</span></div>
|
|
390
390
|
<div class="cmd"><span class="cmd-name">/qualia-report</span><span class="cmd-desc">Generate session report and commit to repo. Mandatory before clock-out.</span></div>
|
|
391
391
|
</div>
|
|
392
392
|
</div>
|
|
@@ -418,6 +418,7 @@
|
|
|
418
418
|
<p class="cmd-group-note">Extend the framework itself.</p>
|
|
419
419
|
<div class="commands">
|
|
420
420
|
<div class="cmd"><span class="cmd-name">/qualia-skill-new</span><span class="cmd-desc">Author a new Qualia skill or agent. Generates the SKILL.md, registers it in the right location, and optionally ships to the framework repo.</span></div>
|
|
421
|
+
<div class="cmd"><span class="cmd-name">/qualia-postmortem</span><span class="cmd-desc">Analyze a verification failure and turn the lesson into a framework improvement.</span></div>
|
|
421
422
|
</div>
|
|
422
423
|
</div>
|
|
423
424
|
</section>
|
|
@@ -430,8 +431,13 @@
|
|
|
430
431
|
<div class="cmd"><span class="cmd-name">qualia-framework install</span><span class="cmd-desc">Install or reinstall the framework.</span></div>
|
|
431
432
|
<div class="cmd"><span class="cmd-name">qualia-framework update</span><span class="cmd-desc">Update to the latest version.</span></div>
|
|
432
433
|
<div class="cmd"><span class="cmd-name">qualia-framework version</span><span class="cmd-desc">Show installed version + check for updates.</span></div>
|
|
434
|
+
<div class="cmd"><span class="cmd-name">qualia-framework uninstall</span><span class="cmd-desc">Clean removal from ~/.claude/ while preserving user-owned settings.</span></div>
|
|
433
435
|
<div class="cmd"><span class="cmd-name">qualia-framework migrate</span><span class="cmd-desc">Upgrade legacy settings.json to the current hook layout.</span></div>
|
|
434
436
|
<div class="cmd"><span class="cmd-name">qualia-framework analytics</span><span class="cmd-desc">Hook telemetry, verification pass rates, gap cycles.</span></div>
|
|
437
|
+
<div class="cmd"><span class="cmd-name">qualia-framework set-erp-key</span><span class="cmd-desc">Save and enable the ERP API key.</span></div>
|
|
438
|
+
<div class="cmd"><span class="cmd-name">qualia-framework erp-ping</span><span class="cmd-desc">Verify ERP connectivity and API key health.</span></div>
|
|
439
|
+
<div class="cmd"><span class="cmd-name">qualia-framework doctor</span><span class="cmd-desc">Health-check installed files, hooks, settings, and knowledge layer.</span></div>
|
|
440
|
+
<div class="cmd"><span class="cmd-name">qualia-framework flush</span><span class="cmd-desc">Run the non-interactive knowledge flush.</span></div>
|
|
435
441
|
<div class="cmd"><span class="cmd-name">qualia-framework team</span><span class="cmd-desc">List, add, or remove team members.</span></div>
|
|
436
442
|
<div class="cmd"><span class="cmd-name">qualia-framework traces</span><span class="cmd-desc">View recent hook activity.</span></div>
|
|
437
443
|
</div>
|
|
@@ -470,11 +476,11 @@
|
|
|
470
476
|
<section>
|
|
471
477
|
<h2>Rules</h2>
|
|
472
478
|
<ul class="rules">
|
|
473
|
-
<li><span class="rule-icon">1</span> Feature branches
|
|
479
|
+
<li><span class="rule-icon">1</span> Feature branches by default — OWNER overrides must be explicit</li>
|
|
474
480
|
<li><span class="rule-icon">2</span> Read before write — understand files before editing</li>
|
|
475
481
|
<li><span class="rule-icon">3</span> MVP first — build what's asked, nothing extra</li>
|
|
476
482
|
<li><span class="rule-icon">4</span> /qualia-report before clock-out — mandatory, enforced by ERP</li>
|
|
477
|
-
<li><span class="rule-icon">5</span>
|
|
483
|
+
<li><span class="rule-icon">5</span> Secrets through approved flows — use set-erp-key or ask Fawzi</li>
|
|
478
484
|
<li><span class="rule-icon">6</span> Stuck 30+ minutes? Ask Fawzi</li>
|
|
479
485
|
</ul>
|
|
480
486
|
</section>
|
|
@@ -536,7 +542,7 @@
|
|
|
536
542
|
<div class="footer">
|
|
537
543
|
<strong>Welcome to the future with Qualia.</strong><br>
|
|
538
544
|
Qualia Solutions — Nicosia, Cyprus
|
|
539
|
-
<span class="footer-version">qualia-framework {{VERSION}} ·
|
|
545
|
+
<span class="footer-version">qualia-framework {{VERSION}} · 28 skills</span>
|
|
540
546
|
</div>
|
|
541
547
|
|
|
542
548
|
</body>
|
package/tests/bin.test.sh
CHANGED
|
@@ -638,8 +638,11 @@ else
|
|
|
638
638
|
fail_case "knowledge idempotency" "exit=$EXIT"
|
|
639
639
|
fi
|
|
640
640
|
|
|
641
|
-
# 43. ERP API key
|
|
642
|
-
|
|
641
|
+
# 43. ERP API key is opt-in and preserved on re-install
|
|
642
|
+
CONFIG_ENABLED=$($NODE -e "const c=require('$TMP/.claude/.qualia-config.json'); console.log(c.erp && c.erp.enabled === false ? 'disabled' : 'enabled')")
|
|
643
|
+
if [ ! -f "$TMP/.claude/.erp-api-key" ] && [ "$CONFIG_ENABLED" = "disabled" ]; then
|
|
644
|
+
echo " ✓ ERP disabled when no API key is provided"
|
|
645
|
+
PASS=$((PASS + 1))
|
|
643
646
|
echo "custom-erp-key" > "$TMP/.claude/.erp-api-key"
|
|
644
647
|
echo "QS-FAWZI-01" | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/out3.log" 2>&1
|
|
645
648
|
if grep -q "custom-erp-key" "$TMP/.claude/.erp-api-key"; then
|
|
@@ -648,7 +651,7 @@ if [ -f "$TMP/.claude/.erp-api-key" ]; then
|
|
|
648
651
|
fail_case ".erp-api-key preservation"
|
|
649
652
|
fi
|
|
650
653
|
else
|
|
651
|
-
fail_case "
|
|
654
|
+
fail_case "ERP opt-in default" "key_exists=$(test -f "$TMP/.claude/.erp-api-key" && echo yes || echo no) config=$CONFIG_ENABLED"
|
|
652
655
|
fi
|
|
653
656
|
|
|
654
657
|
# 44. Templates copied to qualia-templates/
|
package/tests/hooks.test.sh
CHANGED
|
@@ -34,28 +34,17 @@ for f in "$HOOKS_DIR"/*.js; do
|
|
|
34
34
|
fi
|
|
35
35
|
done
|
|
36
36
|
|
|
37
|
-
# --- block-env-edit.js ---
|
|
37
|
+
# --- block-env-edit.js retired in v3.2.0 ---
|
|
38
38
|
echo ""
|
|
39
39
|
echo "block-env-edit:"
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
assert_exit "blocks .env" 2 $?
|
|
49
|
-
|
|
50
|
-
# Windows-style path with backslashes (normalized by the hook)
|
|
51
|
-
echo '{"tool_input":{"file_path":"C:\\project\\.env.local"}}' | $NODE "$HOOKS_DIR/block-env-edit.js" > /dev/null 2>&1
|
|
52
|
-
assert_exit "blocks windows .env.local" 2 $?
|
|
53
|
-
|
|
54
|
-
echo '{"tool_input":{"file_path":"src/app.tsx"}}' | $NODE "$HOOKS_DIR/block-env-edit.js" > /dev/null 2>&1
|
|
55
|
-
assert_exit "allows src/app.tsx" 0 $?
|
|
56
|
-
|
|
57
|
-
echo '{"tool_input":{"file_path":"components/Footer.tsx"}}' | $NODE "$HOOKS_DIR/block-env-edit.js" > /dev/null 2>&1
|
|
58
|
-
assert_exit "allows components/Footer.tsx" 0 $?
|
|
41
|
+
if [ ! -f "$HOOKS_DIR/block-env-edit.js" ]; then
|
|
42
|
+
echo " ✓ retired hook is absent"
|
|
43
|
+
PASS=$((PASS + 1))
|
|
44
|
+
else
|
|
45
|
+
echo " ✗ retired hook still exists"
|
|
46
|
+
FAIL=$((FAIL + 1))
|
|
47
|
+
fi
|
|
59
48
|
|
|
60
49
|
# --- migration-guard.js ---
|
|
61
50
|
echo ""
|
|
@@ -329,7 +318,7 @@ mkdir -p "$TMP/app/admin"
|
|
|
329
318
|
echo 'const key = "service_role"; export default function Page() { return <div>{key}</div>; }' > "$TMP/app/admin/page.tsx"
|
|
330
319
|
OUT=$( (cd "$TMP" && $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
|
|
331
320
|
RC=$?
|
|
332
|
-
assert_exit "regular page.tsx with service_role → blocked (exit
|
|
321
|
+
assert_exit "regular page.tsx with service_role → blocked (exit 2)" 2 $RC
|
|
333
322
|
rm -rf "$TMP"
|
|
334
323
|
|
|
335
324
|
# --- session-start.js — must exit 0 always ---
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Qualia Framework — bin/plan-contract.js + bin/agent-runs.js
|
|
3
|
+
# Run: bash tests/lib.test.sh
|
|
4
|
+
|
|
5
|
+
PASS=0
|
|
6
|
+
FAIL=0
|
|
7
|
+
FRAMEWORK_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
8
|
+
NODE="${NODE:-node}"
|
|
9
|
+
|
|
10
|
+
TMP_DIRS=()
|
|
11
|
+
cleanup() {
|
|
12
|
+
for d in "${TMP_DIRS[@]}"; do
|
|
13
|
+
[ -d "$d" ] && rm -rf "$d"
|
|
14
|
+
done
|
|
15
|
+
}
|
|
16
|
+
trap cleanup EXIT
|
|
17
|
+
|
|
18
|
+
mktmp() {
|
|
19
|
+
local t
|
|
20
|
+
t=$(mktemp -d)
|
|
21
|
+
TMP_DIRS+=("$t")
|
|
22
|
+
echo "$t"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
ok() { echo " ✓ $1"; PASS=$((PASS + 1)); }
|
|
26
|
+
fail() { echo " ✗ $1"; FAIL=$((FAIL + 1)); }
|
|
27
|
+
|
|
28
|
+
echo "lib.test.sh — plan-contract + agent-runs"
|
|
29
|
+
|
|
30
|
+
# ─── plan-contract ──────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
PC="$FRAMEWORK_DIR/bin/plan-contract.js"
|
|
33
|
+
$NODE --check "$PC" >/dev/null 2>&1 && ok "plan-contract.js parses" || fail "plan-contract.js parse"
|
|
34
|
+
|
|
35
|
+
OUT=$($NODE -e '
|
|
36
|
+
const pc = require("'"$PC"'");
|
|
37
|
+
const good = {
|
|
38
|
+
version: 1, phase: 1, goal: "x", why: "y",
|
|
39
|
+
generated_at: "2026-04-28T12:00:00Z", generated_by: "planner",
|
|
40
|
+
source_plan_hash: "sha256:abc", success_criteria: ["sc"],
|
|
41
|
+
tasks: [{
|
|
42
|
+
id: "T1", title: "t1", wave: 1, depends_on: [],
|
|
43
|
+
files_modify: [], files_create: [], files_delete: [],
|
|
44
|
+
acceptance_criteria: ["ac"], action: "do", context_files: [],
|
|
45
|
+
verification: [{ type: "file-exists", path: "a.ts" }]
|
|
46
|
+
}]
|
|
47
|
+
};
|
|
48
|
+
const errs = pc.validate(good);
|
|
49
|
+
console.log(errs.length === 0 ? "VALID" : "INVALID:" + errs.join(";"));
|
|
50
|
+
')
|
|
51
|
+
[ "$OUT" = "VALID" ] && ok "validates well-formed contract" || fail "well-formed contract: $OUT"
|
|
52
|
+
|
|
53
|
+
OUT=$($NODE -e '
|
|
54
|
+
const pc = require("'"$PC"'");
|
|
55
|
+
const errs = pc.validate({ version: 2, phase: 0, tasks: [] });
|
|
56
|
+
console.log(errs.length > 0 ? "REJECTED" : "ACCEPTED");
|
|
57
|
+
')
|
|
58
|
+
[ "$OUT" = "REJECTED" ] && ok "rejects malformed contract" || fail "malformed accepted"
|
|
59
|
+
|
|
60
|
+
OUT=$($NODE -e '
|
|
61
|
+
const pc = require("'"$PC"'");
|
|
62
|
+
const c = {
|
|
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: "a", wave: 1, depends_on: ["T2"], files_modify: [], files_create: [], files_delete: [],
|
|
68
|
+
acceptance_criteria: ["x"], action: "", context_files: [],
|
|
69
|
+
verification: [{ type: "file-exists", path: "a" }] },
|
|
70
|
+
{ id: "T2", title: "b", wave: 1, depends_on: ["T1"], files_modify: [], files_create: [], files_delete: [],
|
|
71
|
+
acceptance_criteria: ["x"], action: "", context_files: [],
|
|
72
|
+
verification: [{ type: "file-exists", path: "b" }] },
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
const errs = pc.validate(c);
|
|
76
|
+
const cycle = errs.some(e => /cycle/.test(e));
|
|
77
|
+
console.log(cycle ? "CYCLE-DETECTED" : "MISSED:" + errs.join(";"));
|
|
78
|
+
')
|
|
79
|
+
[ "$OUT" = "CYCLE-DETECTED" ] && ok "detects depends_on cycle" || fail "cycle missed: $OUT"
|
|
80
|
+
|
|
81
|
+
OUT=$($NODE -e '
|
|
82
|
+
const pc = require("'"$PC"'");
|
|
83
|
+
const c = {
|
|
84
|
+
version: 1, phase: 1, goal: "x", why: "y",
|
|
85
|
+
generated_at: "t", generated_by: "planner", source_plan_hash: "h",
|
|
86
|
+
success_criteria: ["sc"],
|
|
87
|
+
tasks: [{ id: "T1", title: "a", wave: 1, depends_on: [], files_modify: [], files_create: [], files_delete: [],
|
|
88
|
+
acceptance_criteria: ["x"], action: "", context_files: [],
|
|
89
|
+
verification: [{ type: "command-exit", command: "node && rm", args: [], expected_exit: 0 }] }],
|
|
90
|
+
};
|
|
91
|
+
const errs = pc.validate(c);
|
|
92
|
+
const blocked = errs.some(e => /shell metacharacters/.test(e));
|
|
93
|
+
console.log(blocked ? "BLOCKED" : "ALLOWED");
|
|
94
|
+
')
|
|
95
|
+
[ "$OUT" = "BLOCKED" ] && ok "blocks shell metacharacters in command-exit" || fail "shell metas allowed: $OUT"
|
|
96
|
+
|
|
97
|
+
OUT=$($NODE -e '
|
|
98
|
+
const pc = require("'"$PC"'");
|
|
99
|
+
const c = {
|
|
100
|
+
version: 1, phase: 1, goal: "x", why: "y",
|
|
101
|
+
generated_at: "t", generated_by: "planner", source_plan_hash: "h",
|
|
102
|
+
success_criteria: ["sc"],
|
|
103
|
+
tasks: [{ id: "T1", title: "a", wave: 1, depends_on: [], files_modify: [], files_create: [], files_delete: [],
|
|
104
|
+
acceptance_criteria: ["x"], action: "", context_files: [],
|
|
105
|
+
verification: [{ type: "behavioral", description: "feel", evidence_required: [] }] }],
|
|
106
|
+
};
|
|
107
|
+
const errs = pc.validate(c);
|
|
108
|
+
const blocked = errs.some(e => /evidence_required/.test(e));
|
|
109
|
+
console.log(blocked ? "BLOCKED" : "ALLOWED");
|
|
110
|
+
')
|
|
111
|
+
[ "$OUT" = "BLOCKED" ] && ok "rejects behavioral check with empty evidence" || fail "empty evidence allowed: $OUT"
|
|
112
|
+
|
|
113
|
+
# Drift detection
|
|
114
|
+
TMP=$(mktmp)
|
|
115
|
+
$NODE -e '
|
|
116
|
+
const pc = require("'"$PC"'");
|
|
117
|
+
const fs = require("fs"), path = require("path");
|
|
118
|
+
const dir = "'"$TMP"'/.planning";
|
|
119
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
120
|
+
fs.writeFileSync(path.join(dir, "phase-1-plan.md"), "plan v1");
|
|
121
|
+
const h = pc.hashPlan("plan v1");
|
|
122
|
+
fs.writeFileSync(path.join(dir, "phase-1-contract.json"), JSON.stringify({ source_plan_hash: h }));
|
|
123
|
+
const d1 = pc.checkDrift(path.join(dir, "phase-1-contract.json"), path.join(dir, "phase-1-plan.md"));
|
|
124
|
+
console.log(d1.drift ? "FAIL1" : "");
|
|
125
|
+
fs.appendFileSync(path.join(dir, "phase-1-plan.md"), "\nedit\n");
|
|
126
|
+
const d2 = pc.checkDrift(path.join(dir, "phase-1-contract.json"), path.join(dir, "phase-1-plan.md"));
|
|
127
|
+
console.log(d2.drift ? "DRIFT-DETECTED" : "FAIL2");
|
|
128
|
+
' | tail -1 > /tmp/qfdrift.out
|
|
129
|
+
[ "$(cat /tmp/qfdrift.out)" = "DRIFT-DETECTED" ] && ok "drift detection" || fail "drift detection: $(cat /tmp/qfdrift.out)"
|
|
130
|
+
|
|
131
|
+
# ─── agent-runs ──────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
AR="$FRAMEWORK_DIR/bin/agent-runs.js"
|
|
134
|
+
$NODE --check "$AR" >/dev/null 2>&1 && ok "agent-runs.js parses" || fail "agent-runs.js parse"
|
|
135
|
+
|
|
136
|
+
TMP=$(mktmp)
|
|
137
|
+
mkdir -p "$TMP/.planning"
|
|
138
|
+
RES=$($NODE -e '
|
|
139
|
+
const ar = require("'"$AR"'");
|
|
140
|
+
const tok = ar.start({ agent_type: "builder", model: "claude-sonnet-4-6", phase: 1, task_id: "T1" });
|
|
141
|
+
const r = ar.finish(tok, { status: "success", commit_sha: "abc", cwd: "'"$TMP"'" });
|
|
142
|
+
console.log(r.written ? "WRITTEN" : "NOT");
|
|
143
|
+
')
|
|
144
|
+
[ "$RES" = "WRITTEN" ] && ok "agent-runs writes a record" || fail "agent-runs write: $RES"
|
|
145
|
+
|
|
146
|
+
RES=$($NODE -e '
|
|
147
|
+
const ar = require("'"$AR"'");
|
|
148
|
+
const recs = ar.read("'"$TMP"'");
|
|
149
|
+
console.log(recs.length === 1 && recs[0].agent_type === "builder" ? "READ-OK" : "READ-FAIL");
|
|
150
|
+
')
|
|
151
|
+
[ "$RES" = "READ-OK" ] && ok "agent-runs reads back the record" || fail "read failed: $RES"
|
|
152
|
+
|
|
153
|
+
# Telemetry off → no write
|
|
154
|
+
TMP2=$(mktmp)
|
|
155
|
+
mkdir -p "$TMP2/.planning"
|
|
156
|
+
RES=$(QUALIA_TELEMETRY=off $NODE -e '
|
|
157
|
+
const ar = require("'"$AR"'");
|
|
158
|
+
const tok = ar.start({ agent_type: "verifier", model: "claude-opus-4-7" });
|
|
159
|
+
const r = ar.finish(tok, { status: "success", cwd: "'"$TMP2"'" });
|
|
160
|
+
console.log(r.written ? "WRITTEN" : "SKIPPED");
|
|
161
|
+
')
|
|
162
|
+
[ "$RES" = "SKIPPED" ] && ok "QUALIA_TELEMETRY=off disables writes" || fail "telemetry off: $RES"
|
|
163
|
+
|
|
164
|
+
# Read still works after opt-out (history not hidden) — synthesize one record then disable
|
|
165
|
+
TMP3=$(mktmp)
|
|
166
|
+
mkdir -p "$TMP3/.planning"
|
|
167
|
+
$NODE -e '
|
|
168
|
+
const ar = require("'"$AR"'");
|
|
169
|
+
const tok = ar.start({ agent_type: "builder", model: "x" });
|
|
170
|
+
ar.finish(tok, { status: "success", cwd: "'"$TMP3"'" });
|
|
171
|
+
' >/dev/null
|
|
172
|
+
RES=$(QUALIA_TELEMETRY=off $NODE -e '
|
|
173
|
+
const ar = require("'"$AR"'");
|
|
174
|
+
console.log(ar.read("'"$TMP3"'").length);
|
|
175
|
+
')
|
|
176
|
+
[ "$RES" = "1" ] && ok "QUALIA_TELEMETRY=off does NOT hide existing records" || fail "history hidden: $RES"
|
|
177
|
+
|
|
178
|
+
# Failure log file
|
|
179
|
+
TMP4=$(mktmp)
|
|
180
|
+
mkdir -p "$TMP4/.planning"
|
|
181
|
+
RES=$($NODE -e '
|
|
182
|
+
const ar = require("'"$AR"'");
|
|
183
|
+
const tok = ar.start({ agent_type: "verifier", model: "x" });
|
|
184
|
+
const r = ar.finish(tok, { status: "failure", failure_reason: "tsc-failed",
|
|
185
|
+
failure_detail: "x".repeat(800), full_stderr: "trace lines", cwd: "'"$TMP4"'" });
|
|
186
|
+
console.log(r.log_file ? "LOG:" + r.log_file : "NO-LOG");
|
|
187
|
+
' )
|
|
188
|
+
echo "$RES" | grep -q '^LOG:' && ok "failure writes side log file" || fail "no log on failure: $RES"
|
|
189
|
+
|
|
190
|
+
# Truncation: must keep tail
|
|
191
|
+
RES=$($NODE -e '
|
|
192
|
+
const ar = require("'"$AR"'");
|
|
193
|
+
const recs = ar.read("'"$TMP4"'");
|
|
194
|
+
const fd = recs[0].failure_detail;
|
|
195
|
+
const allXs = /^x+$/.test(fd) && fd.length === 500;
|
|
196
|
+
console.log(allXs ? "TAIL-KEPT-500" : "WRONG-LEN-OR-CONTENT:" + fd.length);
|
|
197
|
+
')
|
|
198
|
+
[ "$RES" = "TAIL-KEPT-500" ] && ok "failure_detail keeps tail at 500 chars" || fail "truncation: $RES"
|
|
199
|
+
|
|
200
|
+
# Prune
|
|
201
|
+
TMP5=$(mktmp)
|
|
202
|
+
mkdir -p "$TMP5/.planning"
|
|
203
|
+
$NODE -e '
|
|
204
|
+
const ar = require("'"$AR"'");
|
|
205
|
+
const t1 = ar.start({ agent_type: "builder", model: "x" });
|
|
206
|
+
ar.finish(t1, { status: "success", cwd: "'"$TMP5"'" });
|
|
207
|
+
' >/dev/null
|
|
208
|
+
RES=$($NODE -e '
|
|
209
|
+
const ar = require("'"$AR"'");
|
|
210
|
+
const r = ar.prune("'"$TMP5"'", "2099-01-01T00:00:00Z");
|
|
211
|
+
console.log("removed:" + r.removed);
|
|
212
|
+
')
|
|
213
|
+
[ "$RES" = "removed:1" ] && ok "prune --before removes old records" || fail "prune: $RES"
|
|
214
|
+
|
|
215
|
+
echo ""
|
|
216
|
+
echo "lib.test.sh: $PASS passed, $FAIL failed"
|
|
217
|
+
[ "$FAIL" -eq 0 ]
|