qualia-framework 7.2.0 → 7.2.2

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 CHANGED
@@ -8,6 +8,44 @@ 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.2] - 2026-06-27 (install UX — masked codes, clean references, update-on-/qualia)
12
+
13
+ ### Fixed
14
+ - **Install code is masked** — the `QS-NAME-##` code now shows as `*` in the
15
+ interactive prompt and is hidden in piped/log output, so it never appears on screen.
16
+ - **`archetypes` EISDIR warning gone** — the `references/` flat copy now skips
17
+ directories (the `archetypes/` tree is handled by the recursive canonical copy);
18
+ a clean install reports zero warnings.
19
+
20
+ ### Added
21
+ - **Update notice on `/qualia`** — the router surfaces the cached "update available"
22
+ notice (the same one `auto-update.js` writes for the session-start banner), so an
23
+ operator who only ever runs `/qualia` still learns a new framework version is out.
24
+
25
+ ## [7.2.1] - 2026-06-27 (system-audit makeover — portability, least-privilege, the one-loop)
26
+
27
+ Outcome of a full four-system audit. Doc-truth and portability fixes plus the
28
+ memory-loop wiring that makes the brain learn again. No execution-kernel behavior
29
+ change; additive and corrective only.
30
+
31
+ ### Fixed
32
+ - **`/qualia-scope` portability** — replaced hardcoded `/home/qualia/` paths with
33
+ `${QUALIA_RULES}` / `${QUALIA_REFERENCES}` so the constitution + archetype DoD
34
+ actually load on every install (Claude and Codex). It was silently HALTing before.
35
+ - **Doc truth** — Opus 4.7 to 4.8 strings; skill/hook/rule counts (28/16/12)
36
+ regenerated from disk with a `refs.test.sh` guard so they cannot drift again;
37
+ the 5 undiscoverable skills added to the guide.
38
+
39
+ ### Added
40
+ - **Least-privilege on `zoho-workflow`** — the only money-moving skill that had no
41
+ `allowed-tools` boundary now has one.
42
+ - **The one memory loop** — `knowledge-flush.js` dual-writes durable concepts into
43
+ the vault; one nightly `systemd --user` timer (installer-owned) runs
44
+ flush -> ingest-erp-reports -> export-team-wiki; `qualia-doctor` gains five
45
+ freshness gates so a frozen loop is loud.
46
+ - **R6 golden verifier fixture** — a seeded panel that must FAIL, proving the
47
+ verifier cannot default to a passing score.
48
+
11
49
  ## [7.2.0] - 2026-06-25 (journey spine — lifecycle UX for employees)
12
50
 
13
51
  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 (v6.3.0)
138
+ ## What's Inside
139
139
 
140
- - **25 installed skills**, 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), design (`qualia-polish --loop` and `--vibe`), health/reporting (doctor, learn, postmortem), and Zoho workflow support. Retired helper commands are pruned on install rather than exposed as default slash commands.
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
- - **12 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
143
- - **10 installed rules** (`rules/`): grounding, security, infrastructure, deployment, speed, architecture, trust-boundary, codex-goal, one-opinion, and always-on command-output transparency.
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 12 hooks are real ops engineering, not theoretical:
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/ 25 installed skills (each may ship SKILL.md + REFERENCE.md + scripts/ + fixtures/)
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/ 11 Node.js hooks — cross-platform (no bash dependency)
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}`);
@@ -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
- QUALIA_REFERENCES: `${home}/qualia-references`,
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
@@ -507,6 +507,29 @@ function closeRl() {
507
507
  if (SHARED_RL) { try { SHARED_RL.close(); } catch {} SHARED_RL = null; }
508
508
  }
509
509
 
510
+ // Ask a question while masking the typed answer with '*' (for install codes).
511
+ // The prompt itself is written before muting, so only keystrokes are hidden.
512
+ function questionMasked(rl, prompt) {
513
+ return new Promise((resolve) => {
514
+ rl.question(prompt, (answer) => {
515
+ rl.stdoutMuted = false;
516
+ rl.output.write("\n");
517
+ resolve(answer);
518
+ });
519
+ rl.stdoutMuted = true;
520
+ if (!rl.__maskPatched) {
521
+ rl.__maskPatched = true;
522
+ const origWrite = rl._writeToOutput.bind(rl);
523
+ rl._writeToOutput = function (str) {
524
+ if (!rl.stdoutMuted) return origWrite(str);
525
+ // Pass control sequences (newlines) through; mask printable input.
526
+ if (str === "\n" || str === "\r\n" || str === "\r") return rl.output.write(str);
527
+ return rl.output.write("*");
528
+ };
529
+ }
530
+ });
531
+ }
532
+
510
533
  // Read every available stdin line into an array. Resolves immediately on
511
534
  // 'end'. Used only when stdin is piped (legacy `echo ... | install`).
512
535
  function bufferStdin() {
@@ -536,8 +559,9 @@ function askCode() {
536
559
  if (!IS_INTERACTIVE) {
537
560
  printHeader();
538
561
  const line = nextPipedLine();
539
- // Echo the prompt + answer for log readability.
540
- process.stdout.write(` ${WHITE}Enter install code (or "EMPLOYEE" for no-code install):${RESET} ${line}\n`);
562
+ // Echo the prompt for log readability, but never reveal the code itself.
563
+ const shown = isEmployeeKeyword(line) ? String(line).trim().toUpperCase() : (line ? "*".repeat(String(line).trim().length || 1) : "");
564
+ process.stdout.write(` ${WHITE}Enter install code (or "EMPLOYEE" for no-code install):${RESET} ${shown}\n`);
541
565
  resolve(String(line || "").trim());
542
566
  return;
543
567
  }
@@ -546,7 +570,7 @@ function askCode() {
546
570
  console.log(` ${DIM}OWNER / team member? Enter your install code (QS-NAME-##).${RESET}`);
547
571
  console.log(` ${DIM}New employee without a code? Type ${RESET}${TEAL}EMPLOYEE${RESET}${DIM} to install in employee mode.${RESET}`);
548
572
  console.log("");
549
- rl.question(` ${WHITE}Install code or "EMPLOYEE":${RESET} `, (answer) => {
573
+ questionMasked(rl, ` ${WHITE}Install code or "EMPLOYEE":${RESET} `).then((answer) => {
550
574
  resolve(String(answer || "").trim());
551
575
  });
552
576
  });
@@ -629,6 +653,111 @@ function resolveTeamCode(input) {
629
653
  return null;
630
654
  }
631
655
 
656
+ // ─── Memory loop scheduler (systemd --user timer) ────────────────────────
657
+ // THE ONE LOOP, installer-owned. A single nightly timer runs, in order:
658
+ // 1. knowledge-flush.js → promote daily-log + dual-write vault concepts
659
+ // 2. ingest-erp-reports.py → pull team reports from ERP into the vault
660
+ // 3. export-team-wiki.py → rebuild the sanitized team export
661
+ // Fedora is the target (crontab is empty); systemd --user is the native
662
+ // scheduler. Idempotent: re-running install rewrites the unit files and
663
+ // re-enables the timer. If systemctl --user is unavailable (non-systemd host,
664
+ // or no user bus), we write the units anyway and print manual-run fallback
665
+ // instructions instead of failing the install.
666
+ function installMemoryTimer() {
667
+ printSection("Memory loop scheduler");
668
+
669
+ const os = require("os");
670
+ const { spawnSync } = require("child_process");
671
+ const HOME = os.homedir();
672
+
673
+ const unitDir = path.join(HOME, ".config", "systemd", "user");
674
+ const binFlush = path.join(CLAUDE_DIR, "bin", "knowledge-flush.js");
675
+ const vaultRoot = process.env.QUALIA_MEMORY || path.join(HOME, "qualia-memory");
676
+ const scriptsDir = path.join(vaultRoot, "scripts");
677
+ const ingest = path.join(scriptsDir, "ingest-erp-reports.py");
678
+ const exportWiki = path.join(scriptsDir, "export-team-wiki.py");
679
+ const logFile = path.join(CLAUDE_DIR, ".qualia-loop.log");
680
+
681
+ // The runner script chains the three steps; non-fatal per step so one failure
682
+ // (e.g. ERP down) doesn't block the others. node for the flush, python3 for
683
+ // the vault scripts (skipped gracefully if absent — the vault may be on
684
+ // another machine).
685
+ const runner = path.join(CLAUDE_DIR, "bin", "qualia-loop.sh");
686
+ const runnerBody = [
687
+ "#!/usr/bin/env bash",
688
+ "# qualia-loop.sh — nightly memory-loop runner (installed by qualia-framework).",
689
+ "# One loop: flush → ingest ERP team reports → export team wiki. Each step is",
690
+ "# non-fatal so a single failure does not block the others.",
691
+ "set -u",
692
+ `LOG="${logFile}"`,
693
+ 'echo "[$(date -Iseconds)] qualia-loop start" >> "$LOG"',
694
+ `node "${binFlush}" >> "$LOG" 2>&1 || echo "[$(date -Iseconds)] flush failed" >> "$LOG"`,
695
+ `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`,
696
+ `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`,
697
+ 'echo "[$(date -Iseconds)] qualia-loop done" >> "$LOG"',
698
+ "",
699
+ ].join("\n");
700
+
701
+ const serviceBody = [
702
+ "[Unit]",
703
+ "Description=Qualia memory loop (flush → ingest ERP → export team wiki)",
704
+ "",
705
+ "[Service]",
706
+ "Type=oneshot",
707
+ `Environment=QUALIA_MEMORY=${vaultRoot}`,
708
+ `ExecStart=/usr/bin/env bash ${runner}`,
709
+ "",
710
+ ].join("\n");
711
+
712
+ const timerBody = [
713
+ "[Unit]",
714
+ "Description=Nightly Qualia memory loop",
715
+ "",
716
+ "[Timer]",
717
+ "OnCalendar=*-*-* 03:00:00",
718
+ "Persistent=true",
719
+ "",
720
+ "[Install]",
721
+ "WantedBy=timers.target",
722
+ "",
723
+ ].join("\n");
724
+
725
+ try {
726
+ if (!fs.existsSync(unitDir)) fs.mkdirSync(unitDir, { recursive: true });
727
+ fs.writeFileSync(runner, runnerBody);
728
+ try { fs.chmodSync(runner, 0o755); } catch {}
729
+ fs.writeFileSync(path.join(unitDir, "qualia-loop.service"), serviceBody);
730
+ fs.writeFileSync(path.join(unitDir, "qualia-loop.timer"), timerBody);
731
+ ok("unit files written (~/.config/systemd/user/qualia-loop.{service,timer})");
732
+ } catch (e) {
733
+ warn(`memory-loop unit files — ${e.message}`);
734
+ return;
735
+ }
736
+
737
+ // Only enable if systemctl --user is usable. On non-systemd hosts or where
738
+ // there's no user bus (e.g. CI), print manual fallback and move on.
739
+ const probe = spawnSync("systemctl", ["--user", "--version"], { stdio: "ignore" });
740
+ if (probe.status !== 0) {
741
+ log(`${DIM}systemctl --user unavailable — units written but not enabled.${RESET}`);
742
+ log(`${DIM}Manual run:${RESET} ${TEAL}node ${binFlush}${RESET}`);
743
+ log(`${DIM}Or schedule via cron:${RESET} ${TEAL}0 3 * * * bash ${runner}${RESET}`);
744
+ return;
745
+ }
746
+
747
+ const reload = spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
748
+ const enable = spawnSync("systemctl", ["--user", "enable", "--now", "qualia-loop.timer"], {
749
+ encoding: "utf8",
750
+ stdio: ["ignore", "pipe", "pipe"],
751
+ });
752
+ if (reload.status === 0 && enable.status === 0) {
753
+ ok("nightly timer enabled (qualia-loop.timer · 03:00 daily)");
754
+ } else {
755
+ log(`${DIM}timer written but enable failed — run manually:${RESET}`);
756
+ log(` ${TEAL}systemctl --user daemon-reload && systemctl --user enable --now qualia-loop.timer${RESET}`);
757
+ if (enable.stderr) log(`${DIM}${enable.stderr.trim().split("\n").slice(0, 2).join(" ")}${RESET}`);
758
+ }
759
+ }
760
+
632
761
  // ─── Main ────────────────────────────────────────────────
633
762
  async function main() {
634
763
  // Piped install: drain stdin once up front. Avoids EOF/'close' racing
@@ -923,7 +1052,11 @@ async function main() {
923
1052
  const refDest = path.join(CLAUDE_DIR, "qualia-references");
924
1053
  if (fs.existsSync(refDir)) {
925
1054
  if (!fs.existsSync(refDest)) fs.mkdirSync(refDest, { recursive: true });
926
- for (const file of fs.readdirSync(refDir)) {
1055
+ for (const entry of fs.readdirSync(refDir, { withFileTypes: true })) {
1056
+ // Skip nested dirs (e.g. archetypes/) — they are handled by the canonical
1057
+ // tree copy below. Reading a directory as a text file throws EISDIR.
1058
+ if (entry.isDirectory()) continue;
1059
+ const file = entry.name;
927
1060
  try {
928
1061
  copyTextTransform(path.join(refDir, file), path.join(refDest, file), claudeText);
929
1062
  ok(file);
@@ -1479,10 +1612,19 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
1479
1612
  fs.writeFileSync(settingsTmp, JSON.stringify(settings, null, 2));
1480
1613
  fs.renameSync(settingsTmp, settingsPath);
1481
1614
 
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");
1615
+ 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
1616
  ok("Status line + spinner configured");
1484
1617
  ok("Environment variables + permissions");
1485
1618
 
1619
+ // ─── Memory loop scheduler (systemd --user timer) ─────────
1620
+ // Installs ONE nightly timer: knowledge-flush → ingest-erp-reports →
1621
+ // export-team-wiki. Idempotent; safe-skips when systemctl --user is absent.
1622
+ try {
1623
+ installMemoryTimer();
1624
+ } catch (e) {
1625
+ warn(`memory-loop scheduler — ${e.message}`);
1626
+ }
1627
+
1486
1628
  // ─── Codex (optional second target) ──────────────────────
1487
1629
  if (installCodexTarget) {
1488
1630
  await installCodex(member, target, employeeMode);
@@ -1802,6 +1944,10 @@ async function installCodex(member, target, employeeMode = false) {
1802
1944
  const referencesSrc = path.join(FRAMEWORK_DIR, "references");
1803
1945
  if (fs.existsSync(referencesSrc)) {
1804
1946
  copyTreeTransform(referencesSrc, path.join(CODEX_DIR, "qualia-references"), codexText);
1947
+ // Canonical copy: ${QUALIA_REFERENCES} resolves to <home>/references on both
1948
+ // runtimes, and qualia-scope reads references/archetypes/*.md from it. Mirror
1949
+ // the whole tree (incl. nested archetypes/) so the token resolves on Codex.
1950
+ copyTreeTransform(referencesSrc, path.join(CODEX_DIR, "references"), codexText);
1805
1951
  }
1806
1952
  const knowledgeSrc = path.join(FRAMEWORK_DIR, "templates", "knowledge");
1807
1953
  const knowledgeDest = path.join(CODEX_DIR, "knowledge");
@@ -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 (weekly cron) → spawns the installed agent CLI
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