qualia-framework 7.2.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 +24 -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 +119 -1
- package/bin/knowledge-flush.js +123 -2
- package/bin/qualia-doctor.js +249 -0
- package/bin/runtime-manifest.js +1 -0
- package/guide.md +6 -1
- 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-scope/SKILL.md +2 -2
- package/skills/qualia-secure/SKILL.md +1 -1
- package/skills/zoho-workflow/SKILL.md +8 -0
- package/tests/fixtures/r6-golden-fail-panel.json +23 -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 +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
> Note: git tags for historical versions were not retained; commit references are approximate
|
|
9
9
|
> and dates reflect commit history rather than npm publish timestamps.
|
|
10
10
|
|
|
11
|
+
## [7.2.1] - 2026-06-27 (system-audit makeover — portability, least-privilege, the one-loop)
|
|
12
|
+
|
|
13
|
+
Outcome of a full four-system audit. Doc-truth and portability fixes plus the
|
|
14
|
+
memory-loop wiring that makes the brain learn again. No execution-kernel behavior
|
|
15
|
+
change; additive and corrective only.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **`/qualia-scope` portability** — replaced hardcoded `/home/qualia/` paths with
|
|
19
|
+
`${QUALIA_RULES}` / `${QUALIA_REFERENCES}` so the constitution + archetype DoD
|
|
20
|
+
actually load on every install (Claude and Codex). It was silently HALTing before.
|
|
21
|
+
- **Doc truth** — Opus 4.7 to 4.8 strings; skill/hook/rule counts (28/16/12)
|
|
22
|
+
regenerated from disk with a `refs.test.sh` guard so they cannot drift again;
|
|
23
|
+
the 5 undiscoverable skills added to the guide.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- **Least-privilege on `zoho-workflow`** — the only money-moving skill that had no
|
|
27
|
+
`allowed-tools` boundary now has one.
|
|
28
|
+
- **The one memory loop** — `knowledge-flush.js` dual-writes durable concepts into
|
|
29
|
+
the vault; one nightly `systemd --user` timer (installer-owned) runs
|
|
30
|
+
flush -> ingest-erp-reports -> export-team-wiki; `qualia-doctor` gains five
|
|
31
|
+
freshness gates so a frozen loop is loud.
|
|
32
|
+
- **R6 golden verifier fixture** — a seeded panel that must FAIL, proving the
|
|
33
|
+
verifier cannot default to a passing score.
|
|
34
|
+
|
|
11
35
|
## [7.2.0] - 2026-06-25 (journey spine — lifecycle UX for employees)
|
|
12
36
|
|
|
13
37
|
A cosmetic/UX layer that gives an employee a continuous sense of place and
|
package/FLAGS.md
CHANGED
|
@@ -9,6 +9,7 @@ or another underlying CLI.
|
|
|
9
9
|
| Skill | Flag | Effect |
|
|
10
10
|
|---|---|---|
|
|
11
11
|
| `/qualia-new` | `--auto` | Chain end-to-end after journey approval. Pauses only at milestone boundaries. |
|
|
12
|
+
| `/qualia-new` | `--quick` | 4-phase flat wizard for trivial projects (landing pages, prototypes). Skips research and journey mapping. |
|
|
12
13
|
| `/qualia-plan` | `--gaps` | Surgical-fix mode. Reads `phase-{N}-verification.md` FAILs, produces wave-1 fix tasks without touching passing items. |
|
|
13
14
|
| `/qualia-verify` | `--adversarial` | Spawn a second-pass critic that attacks the verifier's own report. Required on auth / payment / migration phases. |
|
|
14
15
|
| `/qualia-postmortem` | `--auto` | Auto-invoked by `/qualia-verify` on FAIL. Identifies which agent/rule/skill should have caught the failure and proposes a delta. |
|
|
@@ -59,6 +60,15 @@ or another underlying CLI.
|
|
|
59
60
|
|---|---|---|
|
|
60
61
|
| `/qualia-test` | `--tdd` | Drive a feature test-first via vertical-slice loop (one test → one impl → repeat). |
|
|
61
62
|
|
|
63
|
+
## Specialized & post-launch
|
|
64
|
+
|
|
65
|
+
| Skill | Flag | Effect |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `/qualia-secure` | `--deep` | Run the optional Opus adversarial red/blue/auditor deep-analysis pass after the static scan. |
|
|
68
|
+
| `/qualia-secure` | `--exit-code` | CI release-gate mode: non-zero exit on findings (wire into pipelines). |
|
|
69
|
+
| `/qualia-recall` | `--scope <knowledge\|vault\|all>` | Restrict recall to the knowledge layer, the qualia-memory vault, or both (default `all`). |
|
|
70
|
+
| `/qualia-recall` | `--json` | Machine-readable output. |
|
|
71
|
+
|
|
62
72
|
## Ship & report
|
|
63
73
|
|
|
64
74
|
| Skill | Flag | Effect |
|
package/README.md
CHANGED
|
@@ -135,12 +135,12 @@ Project
|
|
|
135
135
|
|
|
136
136
|
**Why it matters:** non-technical team members can follow the ladder from any entry point. `/qualia` and `/qualia-milestone` render JOURNEY.md as a visual ladder with current position highlighted. In the ERP, the primary operational dates are project deadline, milestone deadline, and employee shift submission date; framework tasks stay internal to agent execution.
|
|
137
137
|
|
|
138
|
-
## What's Inside
|
|
138
|
+
## What's Inside
|
|
139
139
|
|
|
140
|
-
- **
|
|
140
|
+
- **28 installed skills** (`skills/` is the source of truth — run `qualia-framework doctor` for the live list), focused into Road (new / plan / build / verify / milestone / polish / ship / handoff / report), depth (scope, research, map), navigation (qualia router + road), quality (fix, review, optimize with `--deepen` parallel-interface design, feature, test), AI-feature evaluation (eval), security scanning (secure), post-launch operate mode (update), OWNER recall (recall), health/reporting (doctor, learn, postmortem), and Zoho workflow support. Retired helper commands are pruned on install rather than exposed as default slash commands.
|
|
141
141
|
- **9 agents** (each runs in fresh context): planner, builder, verifier, qa-browser, researcher, research-synthesizer, roadmapper, plan-checker, visual-evaluator
|
|
142
|
-
- **
|
|
143
|
-
- **
|
|
142
|
+
- **16 hooks** (pure Node.js, cross-platform): session-start, auto-update, git-guardrails, branch-guard, pre-push tracking stamp, migration-guard, pre-deploy-gate, stop-session-log, fawzi-approval-guard, vercel-account-guard, env-empty-guard, supabase-destructive-guard, secret-guard, task-write-guard, pre-compact, usage-capture
|
|
143
|
+
- **12 installed rules** (`rules/`): constitution, grounding, security, access, infrastructure, deployment, speed, architecture, trust-boundary, codex-goal, one-opinion, and always-on command-output transparency.
|
|
144
144
|
- **7 lazy-loaded design files** (`qualia-design/`): design-laws, design-brand, design-product, design-rubric, design-reference, frontend, graphics — `Read` on demand by design-aware skills/agents only.
|
|
145
145
|
- **25 template files**: project.md, journey.md, plan.md (story-file format), state.md, DESIGN.md, CONTEXT.md (domain glossary), work-packet.md (ERP-approved session context), decisions/ADR-template.md, tracking.json (with `milestone_name` + `milestones[]`), requirements.md (multi-milestone), roadmap.md (current milestone only), phase-context.md, 4 project-type templates (website, ai-agent, voice-agent, mobile-app), 5 research-project templates (STACK, FEATURES, ARCHITECTURE, PITFALLS, SUMMARY), knowledge templates, help.html
|
|
146
146
|
- **Planning hygiene guard**: `planning-hygiene.js` scans `.planning/` for loose reports/assets and can organize them under `reports/`, `assets/`, `design/`, or `archive/loose/` only with explicit `--write`
|
|
@@ -180,7 +180,7 @@ Splitting planner, builder, and verifier into separate agents with separate cont
|
|
|
180
180
|
|
|
181
181
|
### Production-Grade Hooks
|
|
182
182
|
|
|
183
|
-
All
|
|
183
|
+
All 16 hooks are real ops engineering, not theoretical:
|
|
184
184
|
|
|
185
185
|
- **Pre-deploy gate** — TypeScript, lint, tests, build, and `service_role` leak scan before `vercel --prod`
|
|
186
186
|
- **Fawzi approval guard** — Silently counts employee proxy-approval claims for ERP review
|
|
@@ -214,12 +214,12 @@ npx qualia-framework@latest install
|
|
|
214
214
|
|
|
|
215
215
|
v
|
|
216
216
|
~/.claude/ and/or ~/.codex/
|
|
217
|
-
├── skills/
|
|
217
|
+
├── skills/ 28 installed skills (each may ship SKILL.md + REFERENCE.md + scripts/ + fixtures/)
|
|
218
218
|
├── agents/ 9 agent definitions (Claude .md, Codex .toml)
|
|
219
|
-
├── hooks/
|
|
219
|
+
├── hooks/ 16 Node.js hooks — cross-platform (no bash dependency)
|
|
220
220
|
├── bin/ state.js + qualia-ui.js + statusline.js + knowledge.js + knowledge-flush.js + slop-detect.mjs + planning-hygiene.js + plan-contract.js + agent-runs.js + ERP/report helpers
|
|
221
221
|
├── knowledge/ learned-patterns.md, common-fixes.md, client-prefs.md, daily-log/
|
|
222
|
-
├── rules/ grounding, security, infrastructure, deployment, speed, architecture, trust-boundary, codex-goal, one-opinion, command-output
|
|
222
|
+
├── rules/ constitution, grounding, security, access, infrastructure, deployment, speed, architecture, trust-boundary, codex-goal, one-opinion, command-output
|
|
223
223
|
├── qualia-design/ lazy-loaded design substrate — read on demand
|
|
224
224
|
├── qualia-templates/ project, journey, plan, state, DESIGN, CONTEXT, work-packet, decisions, tracking, requirements, roadmap, research, help
|
|
225
225
|
├── qualia-references/ questioning.md (deep project initialization methodology)
|
package/bin/cli.js
CHANGED
|
@@ -453,6 +453,7 @@ async function cmdUninstall() {
|
|
|
453
453
|
safeRmDir(path.join(home, "qualia-templates"), counters);
|
|
454
454
|
safeRmDir(path.join(home, "qualia-design"), counters);
|
|
455
455
|
safeRmDir(path.join(home, "qualia-references"), counters);
|
|
456
|
+
safeRmDir(path.join(home, "references"), counters);
|
|
456
457
|
|
|
457
458
|
if (!preserveKnowledge) {
|
|
458
459
|
safeRmDir(path.join(home, "knowledge"), counters);
|
|
@@ -466,10 +467,39 @@ async function cmdUninstall() {
|
|
|
466
467
|
|
|
467
468
|
safeRmDir(path.join(home, ".qualia-traces"), counters);
|
|
468
469
|
|
|
470
|
+
// Memory-loop runner (installed alongside bin/, not a .js so not in QUALIA_BIN_FILES).
|
|
471
|
+
safeUnlink(path.join(home, "bin", "qualia-loop.sh"), counters);
|
|
472
|
+
|
|
469
473
|
if (isCodexHome(home)) cleanCodexHooksJson(home, counters);
|
|
470
474
|
else cleanSettingsJson(home, counters);
|
|
471
475
|
}
|
|
472
476
|
|
|
477
|
+
// ── Memory loop systemd --user timer teardown ─────────────
|
|
478
|
+
// installMemoryTimer() (bin/install.js) enables a nightly timer + writes unit
|
|
479
|
+
// files. Disable + remove them so uninstall leaves no scheduled job behind.
|
|
480
|
+
try {
|
|
481
|
+
const os = require("os");
|
|
482
|
+
const { spawnSync } = require("child_process");
|
|
483
|
+
const unitDir = path.join(os.homedir(), ".config", "systemd", "user");
|
|
484
|
+
const units = ["qualia-loop.timer", "qualia-loop.service"];
|
|
485
|
+
const probe = spawnSync("systemctl", ["--user", "--version"], { stdio: "ignore" });
|
|
486
|
+
if (probe.status === 0) {
|
|
487
|
+
spawnSync("systemctl", ["--user", "disable", "--now", "qualia-loop.timer"], { stdio: "ignore" });
|
|
488
|
+
}
|
|
489
|
+
let removedUnit = false;
|
|
490
|
+
for (const u of units) {
|
|
491
|
+
const p = path.join(unitDir, u);
|
|
492
|
+
if (fs.existsSync(p)) { try { fs.unlinkSync(p); removedUnit = true; counters.filesRemoved++; } catch (e) { counters.errors.push(`systemd unit ${u}: ${e.message}`); } }
|
|
493
|
+
}
|
|
494
|
+
const loopLog = path.join(os.homedir(), ".claude", ".qualia-loop.log");
|
|
495
|
+
if (fs.existsSync(loopLog)) { try { fs.unlinkSync(loopLog); counters.filesRemoved++; } catch {} }
|
|
496
|
+
if (probe.status === 0 && removedUnit) {
|
|
497
|
+
spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
|
|
498
|
+
}
|
|
499
|
+
} catch (e) {
|
|
500
|
+
counters.errors.push(`memory-loop timer teardown: ${e.message}`);
|
|
501
|
+
}
|
|
502
|
+
|
|
473
503
|
// Summary.
|
|
474
504
|
console.log("");
|
|
475
505
|
console.log(`${TEAL} ⬢ Uninstall complete${RESET}`);
|
|
@@ -1478,11 +1508,29 @@ function cmdDoctor() {
|
|
|
1478
1508
|
projectAdvisories.push(`project contract health unavailable: ${e.message}`);
|
|
1479
1509
|
}
|
|
1480
1510
|
|
|
1511
|
+
// ── Memory-loop freshness gates (advisory) ─────────────
|
|
1512
|
+
// Surfaces a silently-frozen loop: stale capture, an unscheduled flush, a
|
|
1513
|
+
// stale export, partial coverage, or deprecated rows leaking into the export.
|
|
1514
|
+
let memoryGates = [];
|
|
1515
|
+
try {
|
|
1516
|
+
memoryGates = require("./qualia-doctor.js").runGates();
|
|
1517
|
+
} catch (e) {
|
|
1518
|
+
memoryGates = [{ gate: "memory-loop gates", status: "FAIL", detail: e.message }];
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1481
1521
|
// ── Render ────────────────────────────────────────────
|
|
1482
1522
|
for (const c of checks) {
|
|
1483
1523
|
const mark = c.ok ? `${GREEN}✓${RESET}` : `${RED}✗${RESET}`;
|
|
1484
1524
|
console.log(` ${mark} ${c.label}`);
|
|
1485
1525
|
}
|
|
1526
|
+
if (memoryGates.length > 0) {
|
|
1527
|
+
console.log("");
|
|
1528
|
+
console.log(` ${WHITE}Memory loop:${RESET}`);
|
|
1529
|
+
for (const g of memoryGates) {
|
|
1530
|
+
const mark = g.status === "PASS" ? `${GREEN}✓${RESET}` : g.status === "WARN" ? `${YELLOW}!${RESET}` : `${RED}✗${RESET}`;
|
|
1531
|
+
console.log(` ${mark} ${g.gate} ${DIM}— ${g.detail}${RESET}`);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1486
1534
|
if (projectAdvisories.length > 0) {
|
|
1487
1535
|
console.log("");
|
|
1488
1536
|
console.log(` ${WHITE}Project trust:${RESET}`);
|
package/bin/host-adapters.js
CHANGED
|
@@ -69,7 +69,11 @@ function adapter(name) {
|
|
|
69
69
|
QUALIA_RULES: `${home}/rules`,
|
|
70
70
|
QUALIA_TEMPLATES: `${home}/qualia-templates`,
|
|
71
71
|
QUALIA_KNOWLEDGE: `${home}/knowledge`,
|
|
72
|
-
|
|
72
|
+
// Canonical references tree (methodology docs + archetypes/). The
|
|
73
|
+
// archetype DoD files (read by qualia-scope) live ONLY under this canonical
|
|
74
|
+
// path, so the token points here — NOT at the flat `qualia-references/`
|
|
75
|
+
// mirror, which carries questioning.md but not the nested archetypes/ tree.
|
|
76
|
+
QUALIA_REFERENCES: `${home}/references`,
|
|
73
77
|
QUALIA_DESIGN: `${home}/qualia-design`,
|
|
74
78
|
},
|
|
75
79
|
};
|
package/bin/install.js
CHANGED
|
@@ -629,6 +629,111 @@ function resolveTeamCode(input) {
|
|
|
629
629
|
return null;
|
|
630
630
|
}
|
|
631
631
|
|
|
632
|
+
// ─── Memory loop scheduler (systemd --user timer) ────────────────────────
|
|
633
|
+
// THE ONE LOOP, installer-owned. A single nightly timer runs, in order:
|
|
634
|
+
// 1. knowledge-flush.js → promote daily-log + dual-write vault concepts
|
|
635
|
+
// 2. ingest-erp-reports.py → pull team reports from ERP into the vault
|
|
636
|
+
// 3. export-team-wiki.py → rebuild the sanitized team export
|
|
637
|
+
// Fedora is the target (crontab is empty); systemd --user is the native
|
|
638
|
+
// scheduler. Idempotent: re-running install rewrites the unit files and
|
|
639
|
+
// re-enables the timer. If systemctl --user is unavailable (non-systemd host,
|
|
640
|
+
// or no user bus), we write the units anyway and print manual-run fallback
|
|
641
|
+
// instructions instead of failing the install.
|
|
642
|
+
function installMemoryTimer() {
|
|
643
|
+
printSection("Memory loop scheduler");
|
|
644
|
+
|
|
645
|
+
const os = require("os");
|
|
646
|
+
const { spawnSync } = require("child_process");
|
|
647
|
+
const HOME = os.homedir();
|
|
648
|
+
|
|
649
|
+
const unitDir = path.join(HOME, ".config", "systemd", "user");
|
|
650
|
+
const binFlush = path.join(CLAUDE_DIR, "bin", "knowledge-flush.js");
|
|
651
|
+
const vaultRoot = process.env.QUALIA_MEMORY || path.join(HOME, "qualia-memory");
|
|
652
|
+
const scriptsDir = path.join(vaultRoot, "scripts");
|
|
653
|
+
const ingest = path.join(scriptsDir, "ingest-erp-reports.py");
|
|
654
|
+
const exportWiki = path.join(scriptsDir, "export-team-wiki.py");
|
|
655
|
+
const logFile = path.join(CLAUDE_DIR, ".qualia-loop.log");
|
|
656
|
+
|
|
657
|
+
// The runner script chains the three steps; non-fatal per step so one failure
|
|
658
|
+
// (e.g. ERP down) doesn't block the others. node for the flush, python3 for
|
|
659
|
+
// the vault scripts (skipped gracefully if absent — the vault may be on
|
|
660
|
+
// another machine).
|
|
661
|
+
const runner = path.join(CLAUDE_DIR, "bin", "qualia-loop.sh");
|
|
662
|
+
const runnerBody = [
|
|
663
|
+
"#!/usr/bin/env bash",
|
|
664
|
+
"# qualia-loop.sh — nightly memory-loop runner (installed by qualia-framework).",
|
|
665
|
+
"# One loop: flush → ingest ERP team reports → export team wiki. Each step is",
|
|
666
|
+
"# non-fatal so a single failure does not block the others.",
|
|
667
|
+
"set -u",
|
|
668
|
+
`LOG="${logFile}"`,
|
|
669
|
+
'echo "[$(date -Iseconds)] qualia-loop start" >> "$LOG"',
|
|
670
|
+
`node "${binFlush}" >> "$LOG" 2>&1 || echo "[$(date -Iseconds)] flush failed" >> "$LOG"`,
|
|
671
|
+
`if [ -f "${ingest}" ]; then python3 "${ingest}" >> "$LOG" 2>&1 || echo "[$(date -Iseconds)] ingest failed" >> "$LOG"; else echo "[$(date -Iseconds)] ingest skipped (no vault scripts)" >> "$LOG"; fi`,
|
|
672
|
+
`if [ -f "${exportWiki}" ]; then python3 "${exportWiki}" >> "$LOG" 2>&1 || echo "[$(date -Iseconds)] export failed" >> "$LOG"; else echo "[$(date -Iseconds)] export skipped (no vault scripts)" >> "$LOG"; fi`,
|
|
673
|
+
'echo "[$(date -Iseconds)] qualia-loop done" >> "$LOG"',
|
|
674
|
+
"",
|
|
675
|
+
].join("\n");
|
|
676
|
+
|
|
677
|
+
const serviceBody = [
|
|
678
|
+
"[Unit]",
|
|
679
|
+
"Description=Qualia memory loop (flush → ingest ERP → export team wiki)",
|
|
680
|
+
"",
|
|
681
|
+
"[Service]",
|
|
682
|
+
"Type=oneshot",
|
|
683
|
+
`Environment=QUALIA_MEMORY=${vaultRoot}`,
|
|
684
|
+
`ExecStart=/usr/bin/env bash ${runner}`,
|
|
685
|
+
"",
|
|
686
|
+
].join("\n");
|
|
687
|
+
|
|
688
|
+
const timerBody = [
|
|
689
|
+
"[Unit]",
|
|
690
|
+
"Description=Nightly Qualia memory loop",
|
|
691
|
+
"",
|
|
692
|
+
"[Timer]",
|
|
693
|
+
"OnCalendar=*-*-* 03:00:00",
|
|
694
|
+
"Persistent=true",
|
|
695
|
+
"",
|
|
696
|
+
"[Install]",
|
|
697
|
+
"WantedBy=timers.target",
|
|
698
|
+
"",
|
|
699
|
+
].join("\n");
|
|
700
|
+
|
|
701
|
+
try {
|
|
702
|
+
if (!fs.existsSync(unitDir)) fs.mkdirSync(unitDir, { recursive: true });
|
|
703
|
+
fs.writeFileSync(runner, runnerBody);
|
|
704
|
+
try { fs.chmodSync(runner, 0o755); } catch {}
|
|
705
|
+
fs.writeFileSync(path.join(unitDir, "qualia-loop.service"), serviceBody);
|
|
706
|
+
fs.writeFileSync(path.join(unitDir, "qualia-loop.timer"), timerBody);
|
|
707
|
+
ok("unit files written (~/.config/systemd/user/qualia-loop.{service,timer})");
|
|
708
|
+
} catch (e) {
|
|
709
|
+
warn(`memory-loop unit files — ${e.message}`);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Only enable if systemctl --user is usable. On non-systemd hosts or where
|
|
714
|
+
// there's no user bus (e.g. CI), print manual fallback and move on.
|
|
715
|
+
const probe = spawnSync("systemctl", ["--user", "--version"], { stdio: "ignore" });
|
|
716
|
+
if (probe.status !== 0) {
|
|
717
|
+
log(`${DIM}systemctl --user unavailable — units written but not enabled.${RESET}`);
|
|
718
|
+
log(`${DIM}Manual run:${RESET} ${TEAL}node ${binFlush}${RESET}`);
|
|
719
|
+
log(`${DIM}Or schedule via cron:${RESET} ${TEAL}0 3 * * * bash ${runner}${RESET}`);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const reload = spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
|
|
724
|
+
const enable = spawnSync("systemctl", ["--user", "enable", "--now", "qualia-loop.timer"], {
|
|
725
|
+
encoding: "utf8",
|
|
726
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
727
|
+
});
|
|
728
|
+
if (reload.status === 0 && enable.status === 0) {
|
|
729
|
+
ok("nightly timer enabled (qualia-loop.timer · 03:00 daily)");
|
|
730
|
+
} else {
|
|
731
|
+
log(`${DIM}timer written but enable failed — run manually:${RESET}`);
|
|
732
|
+
log(` ${TEAL}systemctl --user daemon-reload && systemctl --user enable --now qualia-loop.timer${RESET}`);
|
|
733
|
+
if (enable.stderr) log(`${DIM}${enable.stderr.trim().split("\n").slice(0, 2).join(" ")}${RESET}`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
632
737
|
// ─── Main ────────────────────────────────────────────────
|
|
633
738
|
async function main() {
|
|
634
739
|
// Piped install: drain stdin once up front. Avoids EOF/'close' racing
|
|
@@ -1479,10 +1584,19 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
1479
1584
|
fs.writeFileSync(settingsTmp, JSON.stringify(settings, null, 2));
|
|
1480
1585
|
fs.renameSync(settingsTmp, settingsPath);
|
|
1481
1586
|
|
|
1482
|
-
ok("Hooks: session-start, auto-update, branch-guard, pre-push, migration-guard, deploy-gate, git-guardrails, stop-session-log, fawzi-approval-guard, vercel-account-guard, env-empty-guard, supabase-destructive-guard");
|
|
1587
|
+
ok("Hooks: session-start, auto-update, branch-guard, pre-push, migration-guard, deploy-gate, git-guardrails, stop-session-log, fawzi-approval-guard, vercel-account-guard, env-empty-guard, supabase-destructive-guard, secret-guard, task-write-guard, pre-compact, usage-capture");
|
|
1483
1588
|
ok("Status line + spinner configured");
|
|
1484
1589
|
ok("Environment variables + permissions");
|
|
1485
1590
|
|
|
1591
|
+
// ─── Memory loop scheduler (systemd --user timer) ─────────
|
|
1592
|
+
// Installs ONE nightly timer: knowledge-flush → ingest-erp-reports →
|
|
1593
|
+
// export-team-wiki. Idempotent; safe-skips when systemctl --user is absent.
|
|
1594
|
+
try {
|
|
1595
|
+
installMemoryTimer();
|
|
1596
|
+
} catch (e) {
|
|
1597
|
+
warn(`memory-loop scheduler — ${e.message}`);
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1486
1600
|
// ─── Codex (optional second target) ──────────────────────
|
|
1487
1601
|
if (installCodexTarget) {
|
|
1488
1602
|
await installCodex(member, target, employeeMode);
|
|
@@ -1802,6 +1916,10 @@ async function installCodex(member, target, employeeMode = false) {
|
|
|
1802
1916
|
const referencesSrc = path.join(FRAMEWORK_DIR, "references");
|
|
1803
1917
|
if (fs.existsSync(referencesSrc)) {
|
|
1804
1918
|
copyTreeTransform(referencesSrc, path.join(CODEX_DIR, "qualia-references"), codexText);
|
|
1919
|
+
// Canonical copy: ${QUALIA_REFERENCES} resolves to <home>/references on both
|
|
1920
|
+
// runtimes, and qualia-scope reads references/archetypes/*.md from it. Mirror
|
|
1921
|
+
// the whole tree (incl. nested archetypes/) so the token resolves on Codex.
|
|
1922
|
+
copyTreeTransform(referencesSrc, path.join(CODEX_DIR, "references"), codexText);
|
|
1805
1923
|
}
|
|
1806
1924
|
const knowledgeSrc = path.join(FRAMEWORK_DIR, "templates", "knowledge");
|
|
1807
1925
|
const knowledgeDest = path.join(CODEX_DIR, "knowledge");
|
package/bin/knowledge-flush.js
CHANGED
|
@@ -6,10 +6,19 @@
|
|
|
6
6
|
// memory loop end-to-end:
|
|
7
7
|
//
|
|
8
8
|
// Stop hook (auto, every turn) → <install-home>/knowledge/daily-log/{date}.md
|
|
9
|
-
// THIS SCRIPT (
|
|
10
|
-
// flush prompt → promotes raw → curated tier
|
|
9
|
+
// THIS SCRIPT (nightly timer) → spawns the installed agent CLI
|
|
10
|
+
// flush prompt → promotes raw → curated tier (~/.claude/knowledge)
|
|
11
|
+
// → ALSO dual-writes durable concepts into the vault
|
|
12
|
+
// at ${QUALIA_MEMORY:-~/qualia-memory}/wiki/sessions/concepts/
|
|
11
13
|
// bin/knowledge.js (every spawn) → reads index.md → reaches the right file
|
|
12
14
|
//
|
|
15
|
+
// PROMOTE step (the agreed one-loop contract, replacing Cole's compile.py):
|
|
16
|
+
// after the agent CLI flush succeeds, this script reads the freshly-promoted
|
|
17
|
+
// curated concepts and dual-writes a deterministic markdown file per concept
|
|
18
|
+
// into the qualia-memory vault. The vault is resolved from $QUALIA_MEMORY (else
|
|
19
|
+
// ~/qualia-memory); if the vault dir does not exist the dual-write is skipped
|
|
20
|
+
// gracefully with a logged notice — it never crashes the flush.
|
|
21
|
+
//
|
|
13
22
|
// Usage:
|
|
14
23
|
// node ~/.claude/bin/knowledge-flush.js # 7-day flush
|
|
15
24
|
// node ~/.claude/bin/knowledge-flush.js --days 14 # custom window
|
|
@@ -50,6 +59,24 @@ const KNOWLEDGE_DIR = path.join(QUALIA_HOME, "knowledge");
|
|
|
50
59
|
const DAILY_DIR = path.join(KNOWLEDGE_DIR, "daily-log");
|
|
51
60
|
const LOG_FILE = path.join(QUALIA_HOME, ".qualia-flush.log");
|
|
52
61
|
|
|
62
|
+
// ── Vault dual-write target (PROMOTE step of the one-loop contract) ──────
|
|
63
|
+
// Resolve the qualia-memory vault from $QUALIA_MEMORY, else ~/qualia-memory.
|
|
64
|
+
// Durable concepts promoted into the curated tier are mirrored into
|
|
65
|
+
// <vault>/wiki/sessions/concepts/ so the brain (qualia-memory) and the
|
|
66
|
+
// framework-local curated tier stay in sync. If the vault dir is absent the
|
|
67
|
+
// mirror is skipped gracefully (logged "vault-absent"), never crashing.
|
|
68
|
+
function vaultRoot() {
|
|
69
|
+
return process.env.QUALIA_MEMORY || path.join(HOME, "qualia-memory");
|
|
70
|
+
}
|
|
71
|
+
const VAULT_ROOT = vaultRoot();
|
|
72
|
+
const VAULT_CONCEPTS_DIR = path.join(VAULT_ROOT, "wiki", "sessions", "concepts");
|
|
73
|
+
|
|
74
|
+
// The curated concept sources to mirror into the vault. Top-level curated
|
|
75
|
+
// files (promoted by the agent flush) plus anything the flush wrote under
|
|
76
|
+
// knowledge/concepts/. Deterministic so each run overwrites idempotently.
|
|
77
|
+
const CURATED_CONCEPT_FILES = ["learned-patterns.md", "common-fixes.md", "client-prefs.md"];
|
|
78
|
+
const CURATED_CONCEPTS_SUBDIR = path.join(KNOWLEDGE_DIR, "concepts");
|
|
79
|
+
|
|
53
80
|
const _start = Date.now();
|
|
54
81
|
|
|
55
82
|
function logEvent(event) {
|
|
@@ -65,6 +92,82 @@ function logEvent(event) {
|
|
|
65
92
|
} catch {}
|
|
66
93
|
}
|
|
67
94
|
|
|
95
|
+
// ── PROMOTE → vault dual-write ──────────────────────────────────────────
|
|
96
|
+
// Mirror the curated concept tier into the qualia-memory vault. Each curated
|
|
97
|
+
// source becomes a deterministic file at <vault>/wiki/sessions/concepts/ —
|
|
98
|
+
// stable name (slug of the source path) so re-runs overwrite idempotently
|
|
99
|
+
// rather than accumulating duplicates. Returns a structured summary for the
|
|
100
|
+
// flush log. NEVER throws — a vault-write failure must not fail the flush.
|
|
101
|
+
function dualWriteVault() {
|
|
102
|
+
// Resolve the set of curated concept files to mirror.
|
|
103
|
+
const sources = [];
|
|
104
|
+
for (const f of CURATED_CONCEPT_FILES) {
|
|
105
|
+
const p = path.join(KNOWLEDGE_DIR, f);
|
|
106
|
+
if (fs.existsSync(p)) sources.push({ abs: p, slug: f.replace(/\.md$/, "") });
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
if (fs.existsSync(CURATED_CONCEPTS_SUBDIR)) {
|
|
110
|
+
for (const f of fs.readdirSync(CURATED_CONCEPTS_SUBDIR)) {
|
|
111
|
+
if (!f.endsWith(".md")) continue;
|
|
112
|
+
sources.push({
|
|
113
|
+
abs: path.join(CURATED_CONCEPTS_SUBDIR, f),
|
|
114
|
+
slug: `concept-${f.replace(/\.md$/, "")}`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch {}
|
|
119
|
+
|
|
120
|
+
if (sources.length === 0) {
|
|
121
|
+
return { event_detail: "no-curated-concepts", written: 0 };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Vault must exist on disk; if not, skip gracefully (the brain may live on a
|
|
125
|
+
// different machine than the one running the timer).
|
|
126
|
+
if (!fs.existsSync(VAULT_ROOT)) {
|
|
127
|
+
return { event_detail: "vault-absent", vault: VAULT_ROOT, written: 0 };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let written = 0;
|
|
131
|
+
const failures = [];
|
|
132
|
+
try {
|
|
133
|
+
if (!fs.existsSync(VAULT_CONCEPTS_DIR)) {
|
|
134
|
+
fs.mkdirSync(VAULT_CONCEPTS_DIR, { recursive: true });
|
|
135
|
+
}
|
|
136
|
+
} catch (e) {
|
|
137
|
+
return { event_detail: "vault-mkdir-failed", vault: VAULT_CONCEPTS_DIR, error: e.message, written: 0 };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const stamp = new Date().toISOString();
|
|
141
|
+
for (const src of sources) {
|
|
142
|
+
try {
|
|
143
|
+
const body = fs.readFileSync(src.abs, "utf8").trim();
|
|
144
|
+
if (!body) continue;
|
|
145
|
+
// Deterministic filename: slug of the curated source. Idempotent overwrite.
|
|
146
|
+
const safeSlug = src.slug.replace(/[^a-z0-9._-]/gi, "-").toLowerCase();
|
|
147
|
+
const dest = path.join(VAULT_CONCEPTS_DIR, `${safeSlug}.md`);
|
|
148
|
+
const frontMatter = [
|
|
149
|
+
"---",
|
|
150
|
+
"source: qualia-framework/knowledge-flush",
|
|
151
|
+
`promoted_from: knowledge/${path.relative(KNOWLEDGE_DIR, src.abs)}`,
|
|
152
|
+
`last_promoted: ${stamp}`,
|
|
153
|
+
"share: team",
|
|
154
|
+
"---",
|
|
155
|
+
"",
|
|
156
|
+
].join("\n");
|
|
157
|
+
fs.writeFileSync(dest, frontMatter + body + "\n");
|
|
158
|
+
written++;
|
|
159
|
+
} catch (e) {
|
|
160
|
+
failures.push(`${src.slug}: ${e.message}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
event_detail: "vault-mirrored",
|
|
165
|
+
vault_dir: VAULT_CONCEPTS_DIR,
|
|
166
|
+
written,
|
|
167
|
+
failures: failures.length ? failures : undefined,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
68
171
|
function which(cmd) {
|
|
69
172
|
// Cross-platform `which`. Returns the first PATH match or null.
|
|
70
173
|
// We don't shell out to `which` itself because it doesn't exist on Windows
|
|
@@ -109,6 +212,18 @@ function dailyLogHasRecentEntries(windowDays) {
|
|
|
109
212
|
return false;
|
|
110
213
|
}
|
|
111
214
|
|
|
215
|
+
// When required as a module (tests), export the pure helpers and stop here —
|
|
216
|
+
// do not spawn the agent CLI or touch process exit.
|
|
217
|
+
if (require.main !== module) {
|
|
218
|
+
module.exports = {
|
|
219
|
+
dualWriteVault,
|
|
220
|
+
vaultRoot,
|
|
221
|
+
VAULT_CONCEPTS_DIR,
|
|
222
|
+
CURATED_CONCEPT_FILES,
|
|
223
|
+
};
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
112
227
|
// ── Preflight ────────────────────────────────────────────
|
|
113
228
|
// Per-host facts (which CLI, how to invoke it) come from the adapter — the one
|
|
114
229
|
// place runtime differences live. See bin/host-adapters.js.
|
|
@@ -171,6 +286,11 @@ if (status !== 0) {
|
|
|
171
286
|
process.exit(1);
|
|
172
287
|
}
|
|
173
288
|
|
|
289
|
+
// Success: the curated tier has just been (re)written by the agent flush.
|
|
290
|
+
// PROMOTE step — mirror the durable concepts into the qualia-memory vault.
|
|
291
|
+
const vault = dualWriteVault();
|
|
292
|
+
logEvent({ event: "vault-dual-write", ...vault });
|
|
293
|
+
|
|
174
294
|
// Success: parse stdout for the skill's summary line if present, else log
|
|
175
295
|
// the full output tail.
|
|
176
296
|
const summaryMatch = stdout.match(/⬢ Flushed daily-log .+/);
|
|
@@ -178,6 +298,7 @@ logEvent({
|
|
|
178
298
|
event: "ok",
|
|
179
299
|
prompt,
|
|
180
300
|
summary: summaryMatch ? summaryMatch[0] : stdout.split("\n").slice(-3).join(" | "),
|
|
301
|
+
vault: vault.event_detail,
|
|
181
302
|
});
|
|
182
303
|
|
|
183
304
|
// Echo the user-facing summary to stdout so cron logs / interactive runs
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ~/.claude/bin/qualia-doctor.js — memory-loop freshness gates for /qualia-doctor.
|
|
3
|
+
//
|
|
4
|
+
// The memory loop (capture → promote → ingest → export) can silently freeze:
|
|
5
|
+
// the audit found raw/sessions/ stale for 23 days with no monitoring. These
|
|
6
|
+
// gates make staleness loud. Each gate prints PASS / WARN / FAIL with the
|
|
7
|
+
// offending detail, so the operator sees exactly which leg of the loop stopped.
|
|
8
|
+
//
|
|
9
|
+
// Usage:
|
|
10
|
+
// node ~/.claude/bin/qualia-doctor.js # human-readable gates
|
|
11
|
+
// node ~/.claude/bin/qualia-doctor.js --json # machine-readable
|
|
12
|
+
// node ~/.claude/bin/qualia-doctor.js --exit-code # exit 1 if any gate FAILs
|
|
13
|
+
//
|
|
14
|
+
// Gates:
|
|
15
|
+
// 1. daily-log newest ≤ 2 days (capture is alive)
|
|
16
|
+
// 2. flush log last run ≤ 8 days (promote/flush is scheduled & running)
|
|
17
|
+
// 3. wiki/_export rebuilt ≤ 2 days (export is scheduled & running)
|
|
18
|
+
// 4. export concept count == allowed source set (export covers all concepts)
|
|
19
|
+
// 5. tags:deprecated == 0 in _export (no deprecated rows leaked out)
|
|
20
|
+
//
|
|
21
|
+
// Zero-dependency. Resolves the vault from $QUALIA_MEMORY else ~/qualia-memory,
|
|
22
|
+
// and the install home from $QUALIA_HOME else ~/.claude. Missing dirs surface
|
|
23
|
+
// as the gate's offending detail rather than a crash.
|
|
24
|
+
|
|
25
|
+
const fs = require("fs");
|
|
26
|
+
const path = require("path");
|
|
27
|
+
const os = require("os");
|
|
28
|
+
|
|
29
|
+
const HOME = os.homedir();
|
|
30
|
+
|
|
31
|
+
function qualiaHome() {
|
|
32
|
+
if (process.env.QUALIA_HOME) return process.env.QUALIA_HOME;
|
|
33
|
+
const parent = path.basename(path.dirname(__dirname));
|
|
34
|
+
if (parent === ".codex" || parent === ".claude") return path.dirname(__dirname);
|
|
35
|
+
return path.join(HOME, ".claude");
|
|
36
|
+
}
|
|
37
|
+
function vaultRoot() {
|
|
38
|
+
return process.env.QUALIA_MEMORY || path.join(HOME, "qualia-memory");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
42
|
+
|
|
43
|
+
function ageDays(ms) {
|
|
44
|
+
return (Date.now() - ms) / DAY_MS;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Newest mtime among files in a dir (optionally filtered). null if dir absent/empty.
|
|
48
|
+
function newestMtime(dir, filterFn) {
|
|
49
|
+
if (!fs.existsSync(dir)) return null;
|
|
50
|
+
let newest = null;
|
|
51
|
+
let stack = [dir];
|
|
52
|
+
while (stack.length) {
|
|
53
|
+
const d = stack.pop();
|
|
54
|
+
let entries;
|
|
55
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { continue; }
|
|
56
|
+
for (const e of entries) {
|
|
57
|
+
const p = path.join(d, e.name);
|
|
58
|
+
if (e.isDirectory()) { stack.push(p); continue; }
|
|
59
|
+
if (filterFn && !filterFn(e.name, p)) continue;
|
|
60
|
+
try {
|
|
61
|
+
const m = fs.statSync(p).mtimeMs;
|
|
62
|
+
if (newest === null || m > newest) newest = m;
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return newest;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Gate definitions ────────────────────────────────────────────────────
|
|
70
|
+
// Each returns { gate, status: PASS|WARN|FAIL, detail }.
|
|
71
|
+
|
|
72
|
+
function gateDailyLog(QUALIA_HOME) {
|
|
73
|
+
const dir = path.join(QUALIA_HOME, "knowledge", "daily-log");
|
|
74
|
+
const newest = newestMtime(dir, (name) => name.endsWith(".md"));
|
|
75
|
+
if (newest === null) {
|
|
76
|
+
return { gate: "daily-log freshness", status: "FAIL", detail: `no daily-log entries at ${dir}` };
|
|
77
|
+
}
|
|
78
|
+
const age = ageDays(newest);
|
|
79
|
+
if (age <= 2) return { gate: "daily-log freshness", status: "PASS", detail: `newest ${age.toFixed(1)}d old (≤2d)` };
|
|
80
|
+
if (age <= 4) return { gate: "daily-log freshness", status: "WARN", detail: `newest ${age.toFixed(1)}d old (>2d)` };
|
|
81
|
+
return { gate: "daily-log freshness", status: "FAIL", detail: `newest ${age.toFixed(1)}d old — capture may be dead` };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function gateFlushLog(QUALIA_HOME) {
|
|
85
|
+
const logFile = path.join(QUALIA_HOME, ".qualia-flush.log");
|
|
86
|
+
if (!fs.existsSync(logFile)) {
|
|
87
|
+
return { gate: "flush last-run", status: "FAIL", detail: `no flush log at ${logFile} — flush never ran` };
|
|
88
|
+
}
|
|
89
|
+
// Last run = last "ok"/"skipped"/"failed" event timestamp in the JSONL log.
|
|
90
|
+
let lastTs = null;
|
|
91
|
+
try {
|
|
92
|
+
const lines = fs.readFileSync(logFile, "utf8").trim().split("\n").filter(Boolean);
|
|
93
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
94
|
+
try {
|
|
95
|
+
const ev = JSON.parse(lines[i]);
|
|
96
|
+
if (ev.timestamp) { lastTs = Date.parse(ev.timestamp); break; }
|
|
97
|
+
} catch {}
|
|
98
|
+
}
|
|
99
|
+
} catch {}
|
|
100
|
+
if (lastTs === null || Number.isNaN(lastTs)) {
|
|
101
|
+
// Fall back to file mtime.
|
|
102
|
+
try { lastTs = fs.statSync(logFile).mtimeMs; } catch { lastTs = null; }
|
|
103
|
+
}
|
|
104
|
+
if (lastTs === null) {
|
|
105
|
+
return { gate: "flush last-run", status: "FAIL", detail: "flush log unreadable" };
|
|
106
|
+
}
|
|
107
|
+
const age = ageDays(lastTs);
|
|
108
|
+
if (age <= 8) return { gate: "flush last-run", status: "PASS", detail: `last run ${age.toFixed(1)}d ago (≤8d)` };
|
|
109
|
+
if (age <= 12) return { gate: "flush last-run", status: "WARN", detail: `last run ${age.toFixed(1)}d ago (>8d)` };
|
|
110
|
+
return { gate: "flush last-run", status: "FAIL", detail: `last run ${age.toFixed(1)}d ago — flush schedule may be broken` };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function gateExportFresh(VAULT) {
|
|
114
|
+
const exportDir = path.join(VAULT, "wiki", "_export");
|
|
115
|
+
const newest = newestMtime(exportDir);
|
|
116
|
+
if (newest === null) {
|
|
117
|
+
return { gate: "team-export freshness", status: "FAIL", detail: `no export at ${exportDir} — export never ran` };
|
|
118
|
+
}
|
|
119
|
+
const age = ageDays(newest);
|
|
120
|
+
if (age <= 2) return { gate: "team-export freshness", status: "PASS", detail: `rebuilt ${age.toFixed(1)}d ago (≤2d)` };
|
|
121
|
+
if (age <= 4) return { gate: "team-export freshness", status: "WARN", detail: `rebuilt ${age.toFixed(1)}d ago (>2d)` };
|
|
122
|
+
return { gate: "team-export freshness", status: "FAIL", detail: `rebuilt ${age.toFixed(1)}d ago — export schedule may be broken` };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Allowed source set = concept markdown in the vault's source concepts tier
|
|
126
|
+
// (wiki/sessions/concepts/ + wiki/concepts/). Export must cover the same count
|
|
127
|
+
// of concept files (under _export/.../concepts or _export wiki concepts).
|
|
128
|
+
function countConceptMd(dir) {
|
|
129
|
+
if (!fs.existsSync(dir)) return 0;
|
|
130
|
+
let n = 0;
|
|
131
|
+
let stack = [dir];
|
|
132
|
+
while (stack.length) {
|
|
133
|
+
const d = stack.pop();
|
|
134
|
+
let entries;
|
|
135
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { continue; }
|
|
136
|
+
for (const e of entries) {
|
|
137
|
+
const p = path.join(d, e.name);
|
|
138
|
+
if (e.isDirectory()) {
|
|
139
|
+
if (/concepts?$/i.test(e.name)) stack.push(p);
|
|
140
|
+
else stack.push(p);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (e.name.endsWith(".md") && /concepts?[\\/]/i.test(path.relative(dir, p) + "/")) n++;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return n;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function gateExportCoverage(VAULT) {
|
|
150
|
+
// Source concepts: the curated/promoted concept tier in the vault.
|
|
151
|
+
const sourceDirs = [
|
|
152
|
+
path.join(VAULT, "wiki", "sessions", "concepts"),
|
|
153
|
+
path.join(VAULT, "wiki", "concepts"),
|
|
154
|
+
];
|
|
155
|
+
let sourceCount = 0;
|
|
156
|
+
for (const d of sourceDirs) {
|
|
157
|
+
if (!fs.existsSync(d)) continue;
|
|
158
|
+
try { sourceCount += fs.readdirSync(d).filter((f) => f.endsWith(".md")).length; } catch {}
|
|
159
|
+
}
|
|
160
|
+
const exportRoot = path.join(VAULT, "wiki", "_export");
|
|
161
|
+
if (!fs.existsSync(exportRoot)) {
|
|
162
|
+
return { gate: "export concept coverage", status: "FAIL", detail: `no export dir — source has ${sourceCount} concept(s)` };
|
|
163
|
+
}
|
|
164
|
+
const exportCount = countConceptMd(exportRoot);
|
|
165
|
+
if (sourceCount === 0) {
|
|
166
|
+
return { gate: "export concept coverage", status: "WARN", detail: "no source concepts to cover yet" };
|
|
167
|
+
}
|
|
168
|
+
if (exportCount === sourceCount) {
|
|
169
|
+
return { gate: "export concept coverage", status: "PASS", detail: `${exportCount}/${sourceCount} concepts exported` };
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
gate: "export concept coverage",
|
|
173
|
+
status: "FAIL",
|
|
174
|
+
detail: `export has ${exportCount} concept(s), source has ${sourceCount} — export is stale or partial`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function gateNoDeprecated(VAULT) {
|
|
179
|
+
const exportRoot = path.join(VAULT, "wiki", "_export");
|
|
180
|
+
if (!fs.existsSync(exportRoot)) {
|
|
181
|
+
return { gate: "no deprecated in export", status: "FAIL", detail: "no export dir to scan" };
|
|
182
|
+
}
|
|
183
|
+
const offenders = [];
|
|
184
|
+
let stack = [exportRoot];
|
|
185
|
+
while (stack.length) {
|
|
186
|
+
const d = stack.pop();
|
|
187
|
+
let entries;
|
|
188
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { continue; }
|
|
189
|
+
for (const e of entries) {
|
|
190
|
+
const p = path.join(d, e.name);
|
|
191
|
+
if (e.isDirectory()) { stack.push(p); continue; }
|
|
192
|
+
if (!e.name.endsWith(".md")) continue;
|
|
193
|
+
try {
|
|
194
|
+
const txt = fs.readFileSync(p, "utf8");
|
|
195
|
+
// Match `tags: [..., deprecated, ...]`, `tags:deprecated`, or a `deprecated` tag line.
|
|
196
|
+
if (/tags?\s*:\s*\[?[^\]\n]*\bdeprecated\b/i.test(txt) || /^\s*-\s*deprecated\s*$/im.test(txt)) {
|
|
197
|
+
offenders.push(path.relative(exportRoot, p));
|
|
198
|
+
}
|
|
199
|
+
} catch {}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (offenders.length === 0) {
|
|
203
|
+
return { gate: "no deprecated in export", status: "PASS", detail: "0 deprecated-tagged files" };
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
gate: "no deprecated in export",
|
|
207
|
+
status: "FAIL",
|
|
208
|
+
detail: `${offenders.length} deprecated file(s): ${offenders.slice(0, 3).join(", ")}${offenders.length > 3 ? "…" : ""}`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function runGates() {
|
|
213
|
+
const QUALIA_HOME = qualiaHome();
|
|
214
|
+
const VAULT = vaultRoot();
|
|
215
|
+
return [
|
|
216
|
+
gateDailyLog(QUALIA_HOME),
|
|
217
|
+
gateFlushLog(QUALIA_HOME),
|
|
218
|
+
gateExportFresh(VAULT),
|
|
219
|
+
gateExportCoverage(VAULT),
|
|
220
|
+
gateNoDeprecated(VAULT),
|
|
221
|
+
];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function main() {
|
|
225
|
+
const args = process.argv.slice(2);
|
|
226
|
+
const json = args.includes("--json");
|
|
227
|
+
const exitCode = args.includes("--exit-code");
|
|
228
|
+
const results = runGates();
|
|
229
|
+
|
|
230
|
+
if (json) {
|
|
231
|
+
console.log(JSON.stringify({ gates: results }, null, 2));
|
|
232
|
+
} else {
|
|
233
|
+
console.log("Memory-loop freshness gates");
|
|
234
|
+
for (const r of results) {
|
|
235
|
+
const mark = r.status === "PASS" ? "PASS" : r.status === "WARN" ? "WARN" : "FAIL";
|
|
236
|
+
console.log(` [${mark}] ${r.gate} — ${r.detail}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const anyFail = results.some((r) => r.status === "FAIL");
|
|
241
|
+
if (exitCode && anyFail) process.exit(1);
|
|
242
|
+
process.exit(0);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (require.main === module) {
|
|
246
|
+
main();
|
|
247
|
+
} else {
|
|
248
|
+
module.exports = { runGates, gateDailyLog, gateFlushLog, gateExportFresh, gateExportCoverage, gateNoDeprecated, qualiaHome, vaultRoot };
|
|
249
|
+
}
|
package/bin/runtime-manifest.js
CHANGED
|
@@ -43,6 +43,7 @@ const RUNTIME_BIN_SCRIPTS = [
|
|
|
43
43
|
{ file: "learning-candidates.js", label: "learning-candidates.js (scan recent commits + daily-log for patterns worth promoting)" },
|
|
44
44
|
{ file: "status-snapshot.js", label: "status-snapshot.js (portable operator snapshot — install + project + work + ERP + memory)" },
|
|
45
45
|
{ file: "security-scan.js", label: "security-scan.js (static security scanner for agent config — secrets, permissions, hook hygiene)" },
|
|
46
|
+
{ file: "qualia-doctor.js", label: "qualia-doctor.js (memory-loop freshness gates for /qualia-doctor)" },
|
|
46
47
|
];
|
|
47
48
|
|
|
48
49
|
function binFiles() {
|
package/guide.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> Follow the road. Type the commands. The framework handles the rest.
|
|
4
4
|
> `--auto` chains the whole road end-to-end with only two human checkpoints per project.
|
|
5
5
|
|
|
6
|
-
Surface: **
|
|
6
|
+
Surface: **28 active skills** (`skills/` is the source of truth — run `qualia-framework doctor` for the live list). Use `/qualia-fix` for broken behavior, `/qualia-feature` for new single-feature work, `/qualia-scope` in PROJECT MODE for kickoff capture, `/qualia-polish --loop` for the autonomous visual loop, and `/qualia-polish --vibe` for fast layout-preserving aesthetic pivots.
|
|
7
7
|
|
|
8
8
|
For the identity statement see [`SOUL.md`](./SOUL.md). For every skill flag see [`FLAGS.md`](./FLAGS.md). When something breaks see [`TROUBLESHOOTING.md`](./TROUBLESHOOTING.md). For release history see [`CHANGELOG.md`](./CHANGELOG.md).
|
|
9
9
|
|
|
@@ -73,6 +73,11 @@ Append `--auto` to `/qualia-new` and the framework chains every step:
|
|
|
73
73
|
| Road view | `/qualia-road` | View and navigate journey/milestone/phase status |
|
|
74
74
|
| Lost — need next command | `/qualia` | Mechanical state-driven router |
|
|
75
75
|
| Confused — need to understand the situation | `/qualia-idk` | Three-scan diagnostic + paste-ready command sequence |
|
|
76
|
+
| Post-launch change | `/qualia-update` | Ship one update to a LAUNCHED project (lean plan → build → verify → ship, no milestone machinery) |
|
|
77
|
+
| Security scan | `/qualia-secure` | Scan agent config (CLAUDE.md / settings / hooks / MCP) for injection, leaked secrets, unscoped perms |
|
|
78
|
+
| AI-feature gate | `/qualia-eval` | Evaluate an AI feature (chat / RAG / voice / agent) against a layered eval suite and gate on the result |
|
|
79
|
+
| Recall prior knowledge | `/qualia-recall` | OWNER-only — recall curated lessons from the knowledge layer + qualia-memory vault |
|
|
80
|
+
| Health check | `/qualia-doctor` | Framework health — install, project state, contracts, hooks, memory, ERP queue + safe repair suggestions |
|
|
76
81
|
|
|
77
82
|
## Full Journey Hierarchy
|
|
78
83
|
|
package/package.json
CHANGED
package/rules/infrastructure.md
CHANGED
|
@@ -46,8 +46,10 @@ Standard services across all Qualia projects. Use these unless the project expli
|
|
|
46
46
|
- `gh` — GitHub CLI (PRs, issues, repos)
|
|
47
47
|
|
|
48
48
|
## GitHub Organizations
|
|
49
|
-
|
|
50
|
-
-
|
|
49
|
+
> Canonical slugs (verified via `gh repo list`). This list is the single source of truth — other docs reference it, they do not restate it.
|
|
50
|
+
- **`Qualiasolutions`** — the home of the four core Qualia systems (`qualia-framework`, `qualia-memory`, `qualia-erp`, `qualiafinal`). Default target for internal/system repos.
|
|
51
|
+
- **`QualiaSolutionsCY`** — org for client/delivery projects (e.g. USD-Academy, innrvo). Default target for new client work.
|
|
52
|
+
- **`SakaniQualia`** — org for Sakani-related projects (real estate platform).
|
|
51
53
|
- All repos are private by default
|
|
52
54
|
- Main integration: feature branches integrate to `main` at **`/qualia-ship`** (ship is the single merge point — it fast-forwards the branch into `main`, deploys from `main`, and deletes the branch). Pushes to `main` are **allowed and recorded** by `branch-guard` (per-employee tally → ERP) — accountability, not a hard block. `/qualia-report` sweeps for branches with unshipped commits + stale PRs at clock-out so nothing lingers. Keep GitHub branch protection on `main` OFF (or with the team allowed to push) for this model; if you re-enable required reviews, switch ship to an auto-merged PR instead.
|
|
53
55
|
|
|
@@ -92,6 +92,29 @@ node ${QUALIA_BIN}/knowledge.js list
|
|
|
92
92
|
|
|
93
93
|
Healthy memory has at least `index.md`, `agents.md`, and a writable `daily-log/` directory. Missing curated memory is not fatal, but missing installed memory files means reinstall.
|
|
94
94
|
|
|
95
|
+
### 5a. Memory-loop freshness gates
|
|
96
|
+
|
|
97
|
+
The memory loop (capture → promote/flush → ingest ERP → export team wiki) can silently freeze — the audit found capture stale for 23 days with zero monitoring. Run the freshness gates:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
node ${QUALIA_BIN}/qualia-doctor.js
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
It prints PASS / WARN / FAIL with the offending detail for five gates:
|
|
104
|
+
|
|
105
|
+
1. **daily-log freshness** — newest `knowledge/daily-log/*.md` ≤ 2 days old (capture is alive).
|
|
106
|
+
2. **flush last-run** — `.qualia-flush.log` last event ≤ 8 days ago (the nightly flush is scheduled and running).
|
|
107
|
+
3. **team-export freshness** — `${QUALIA_MEMORY:-~/qualia-memory}/wiki/_export` rebuilt ≤ 2 days ago (export is scheduled and running).
|
|
108
|
+
4. **export concept coverage** — `_export` concept count == the vault source concept set (`wiki/sessions/concepts/` + `wiki/concepts/`); a mismatch means the export is stale or partial.
|
|
109
|
+
5. **no deprecated in export** — zero `tags: deprecated` files in `_export` (deprecated rows must not leak into the published team snapshot).
|
|
110
|
+
|
|
111
|
+
Any **FAIL** means a leg of the loop stopped. Repair routing:
|
|
112
|
+
- daily-log FAIL → capture (Stop hook) not firing → reinstall to re-wire the `Stop` hook.
|
|
113
|
+
- flush / export FAIL → the nightly `qualia-loop.timer` is not running → `systemctl --user status qualia-loop.timer`, or reinstall (`npx qualia-framework@latest install`) to re-install the timer.
|
|
114
|
+
- coverage / deprecated FAIL → run the vault export manually (`python3 ~/qualia-memory/scripts/export-team-wiki.py`) and investigate the offending file.
|
|
115
|
+
|
|
116
|
+
These gates are advisory in `qualia-framework doctor` (they render under `Memory loop:`) and exit-coded when run with `--exit-code`.
|
|
117
|
+
|
|
95
118
|
## 6. ERP Queue Health
|
|
96
119
|
|
|
97
120
|
Run:
|
|
@@ -180,6 +203,7 @@ State ledger: ...
|
|
|
180
203
|
Contracts: ...
|
|
181
204
|
Planning hygiene: ...
|
|
182
205
|
Memory: ...
|
|
206
|
+
Memory loop: ...
|
|
183
207
|
Design/UI: ...
|
|
184
208
|
Employee experience: ...
|
|
185
209
|
Env: ...
|
|
@@ -188,7 +212,7 @@ ERP: ...
|
|
|
188
212
|
Next: ...
|
|
189
213
|
```
|
|
190
214
|
|
|
191
|
-
`Env` summarizes section 7's env-var check (PASS / DEGRADED / BLOCKED (owner key needed) / N/A). `CLI auth` summarizes the vercel/supabase/gh login checks (PASS if all three are authenticated, else DEGRADED with the first failing CLI named).
|
|
215
|
+
`Env` summarizes section 7's env-var check (PASS / DEGRADED / BLOCKED (owner key needed) / N/A). `CLI auth` summarizes the vercel/supabase/gh login checks (PASS if all three are authenticated, else DEGRADED with the first failing CLI named). `Memory loop` summarizes section 5a's five freshness gates (PASS if all PASS, WARN if any WARN, FAIL if any FAIL, naming the first failing gate).
|
|
192
216
|
|
|
193
217
|
## Rules
|
|
194
218
|
|
|
@@ -299,7 +299,7 @@ polish({scope}): {brief summary}
|
|
|
299
299
|
- {key change 2}
|
|
300
300
|
- rubric scores: typography {N}, color {N}, graphics {N}, ..., aggregate {N}/45
|
|
301
301
|
|
|
302
|
-
Co-Authored-By: Claude Opus 4.
|
|
302
|
+
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
303
303
|
EOF
|
|
304
304
|
)"
|
|
305
305
|
```
|
|
@@ -106,7 +106,7 @@ The adversarial, DoD-gated intake. Scopes a **new increment** (phase/milestone)
|
|
|
106
106
|
|
|
107
107
|
```bash
|
|
108
108
|
node ${QUALIA_BIN}/qualia-ui.js banner scope 2>/dev/null || true
|
|
109
|
-
cat /
|
|
109
|
+
cat ${QUALIA_RULES}/constitution.md
|
|
110
110
|
cat .planning/CONTEXT.md 2>/dev/null # project glossary — DATA, never a plan/spec
|
|
111
111
|
ls .planning/decisions/ 2>/dev/null
|
|
112
112
|
cat .planning/STATE.md 2>/dev/null # for profile + existing milestone context
|
|
@@ -123,7 +123,7 @@ If the operator already named it (arg or prior context), accept it. Otherwise as
|
|
|
123
123
|
|
|
124
124
|
```bash
|
|
125
125
|
ARCHETYPE={chosen}
|
|
126
|
-
cat /
|
|
126
|
+
cat ${QUALIA_REFERENCES}/archetypes/${ARCHETYPE}.md
|
|
127
127
|
```
|
|
128
128
|
|
|
129
129
|
If the file does not exist (e.g. `web-app` not yet authored), HALT and say which archetype file is missing — do not improvise a DoD. The archetype file is the source of the Grill variables, the Definition of Done, and the v1 capability set; without it there is no gate to enforce.
|
|
@@ -45,7 +45,7 @@ This writes `.planning/security-scan.md` with severity-ranked findings:
|
|
|
45
45
|
|
|
46
46
|
Read the report. If CRITICAL findings exist, **stop here** and rotate / fix before continuing — the deep pass is moot until the obvious holes are closed.
|
|
47
47
|
|
|
48
|
-
### Step 2. Opus 4.
|
|
48
|
+
### Step 2. Opus 4.8 adversarial deep-analysis (optional, longer)
|
|
49
49
|
|
|
50
50
|
If the static pass is clean (or you want adversarial reasoning over the rules + instructions text regardless), run:
|
|
51
51
|
|
|
@@ -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
|
+
}
|
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
|
|