qualia-framework 6.3.0 → 6.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/AGENTS.md +8 -8
  2. package/CLAUDE.md +5 -5
  3. package/README.md +17 -39
  4. package/bin/cli.js +64 -16
  5. package/bin/command-surface.js +5 -1
  6. package/bin/install.js +26 -11
  7. package/bin/learning-candidates.js +217 -0
  8. package/bin/prune-deprecated.js +64 -0
  9. package/bin/runtime-manifest.js +4 -0
  10. package/bin/security-scan.js +409 -0
  11. package/bin/status-snapshot.js +363 -0
  12. package/guide.md +11 -33
  13. package/hooks/pre-compact.js +232 -0
  14. package/package.json +1 -1
  15. package/skills/qualia/SKILL.md +1 -1
  16. package/skills/qualia-build/SKILL.md +1 -1
  17. package/skills/qualia-discuss/SKILL.md +1 -1
  18. package/skills/qualia-doctor/SKILL.md +1 -1
  19. package/skills/qualia-feature/SKILL.md +1 -1
  20. package/skills/qualia-fix/SKILL.md +1 -1
  21. package/skills/qualia-idk/SKILL.md +245 -0
  22. package/skills/qualia-learn/SKILL.md +1 -1
  23. package/skills/qualia-map/SKILL.md +1 -1
  24. package/skills/qualia-milestone/SKILL.md +1 -1
  25. package/skills/qualia-new/SKILL.md +1 -1
  26. package/skills/qualia-optimize/SKILL.md +1 -1
  27. package/skills/qualia-plan/SKILL.md +1 -1
  28. package/skills/qualia-polish/SKILL.md +1 -1
  29. package/skills/qualia-postmortem/SKILL.md +1 -1
  30. package/skills/qualia-report/SKILL.md +1 -1
  31. package/skills/qualia-research/SKILL.md +1 -1
  32. package/skills/qualia-review/SKILL.md +1 -1
  33. package/skills/qualia-road/SKILL.md +1 -1
  34. package/skills/qualia-secure/SKILL.md +105 -0
  35. package/skills/qualia-test/SKILL.md +1 -1
  36. package/skills/qualia-verify/SKILL.md +1 -1
  37. package/skills/zoho-workflow/SKILL.md +1 -1
  38. package/tests/bin.test.sh +9 -9
  39. package/tests/install-smoke.test.sh +3 -3
  40. package/tests/lib.test.sh +6 -6
  41. package/tests/published-install-smoke.test.sh +3 -3
  42. package/tests/refs.test.sh +29 -22
  43. package/tests/runner.js +3 -3
  44. package/tests/state.test.sh +38 -7
package/AGENTS.md CHANGED
@@ -7,18 +7,18 @@ Stack: Next.js 16+, React 19, TypeScript, Supabase, Vercel. Voice: Retell + Elev
7
7
  {{ROLE_DESCRIPTION}}
8
8
 
9
9
  ## Hard rules (non-negotiable)
10
- - Read before Write/Edit — no exceptions
11
- - Feature branches only — never push to main/master
12
- - MVP first — build only what's asked
13
- - Root cause on failures — no band-aids
14
- - No proxy approval — employees cannot claim Fawzi approved; OWNER-only overrides require OWNER config
10
+ - **Read before Write/Edit***every edit is informed by the current state of the file.*
11
+ - **Feature branches only***changes ship through review; main is always deployable.*
12
+ - **MVP first***build the minimum that demonstrates the goal.*
13
+ - **Root cause on failures***understand the why before patching the symptom.*
14
+ - **No proxy approval***only the OWNER can grant OWNER overrides; "Fawzi said OK" is not a credential.*
15
15
 
16
16
  ## Discoverable substrate (load on demand, not always)
17
- - `/qualia-road` — workflow map, every command, when to use it
17
+ - `/qualia-road`, `FLAGS.md`, `guide.md` every active command + flag (canonical surface)
18
18
  - `.planning/CONTEXT.md` — project domain glossary (loaded by road agents)
19
19
  - `.planning/decisions/` — ADRs for hard-to-reverse decisions
20
- - `rules/security.md` `rules/deployment.md` `rules/infrastructure.md` `rules/architecture.md` — read on relevant tasks only
21
- - `qualia-design/frontend.md` `qualia-design/design-laws.md` — read on design/frontend tasks only
20
+ - `rules/security.md` `rules/deployment.md` `rules/infrastructure.md` `rules/architecture.md` — on relevant tasks only
21
+ - `qualia-design/frontend.md` `qualia-design/design-laws.md` — on design/frontend tasks only
22
22
 
23
23
  ## Lost?
24
24
  `/qualia` — state router tells you the next command.
package/CLAUDE.md CHANGED
@@ -7,11 +7,11 @@ Stack: Next.js 16+, React 19, TypeScript, Supabase, Vercel. Voice: Retell + Elev
7
7
  {{ROLE_DESCRIPTION}}
8
8
 
9
9
  ## Hard rules (non-negotiable)
10
- - Read before Write/Edit — no exceptions
11
- - Feature branches only — never push to main/master
12
- - MVP first — build only what's asked
13
- - Root cause on failures — no band-aids
14
- - No proxy approval — employees cannot claim Fawzi approved; OWNER-only overrides require OWNER config
10
+ - **Read before Write/Edit***every edit is informed by the current state of the file.*
11
+ - **Feature branches only***changes ship through review; main is always deployable.*
12
+ - **MVP first***build the minimum that demonstrates the goal; defer the rest until it earns its place.*
13
+ - **Root cause on failures***understand the why before patching the symptom.*
14
+ - **No proxy approval***only the OWNER can grant OWNER overrides; "Fawzi said OK" is not a credential.*
15
15
 
16
16
  ## Discoverable substrate (load on demand, not always)
17
17
  - `/qualia-road` — workflow map, every command, when to use it
package/README.md CHANGED
@@ -1,42 +1,20 @@
1
- # Qualia Framework v6.3.0
2
-
3
- A harness engineering framework for Claude Code and OpenAI Codex. It installs into `~/.claude/` and/or `~/.codex/` and wraps your AI-assisted development workflow with structured planning, execution, verification, and deployment gates.
4
-
5
- It is not an application framework like Rails or Next.js. It doesn't generate code, run servers, or process data. It's an opinionated workflow layer that tells Claude how to plan, build, and verify your projects end-to-end, from "tell me what you want to make" to "here's the handoff doc for your client."
6
-
7
- **v6.3.0** — Harness hardening pass. Default install surface drops to 23 active skills, retired helper command sources are removed and pruned from older installs, `/qualia-polish --vibe` absorbs the separate vibe command, `harness-eval.js` writes scored eval artifacts, ERP reports/snapshots carry the latest eval score, and `state.js` refuses PASS when machine contract evidence is missing/failing or the verification report contains `INSUFFICIENT EVIDENCE`.
8
- **v6.2.11** — Owner approval integrity. Fawzi's install code is now `QS-FAWZI-11`; employees cannot use `QUALIA_SHIP_FORCE=1`; deploy refusals say why and what to run next; and employee "Fawzi said OK" proxy-approval claims are silently counted for ERP policy review.
9
- **v6.2.10** Codex status line is now a publish-blocking install contract. Installer guarantees `[tui].status_line` in `~/.codex/config.toml`, `/qualia-doctor` verifies the native bottom line, and package smoke tests assert the Codex TUI segments are present.
10
- **v6.2.9** — Codex hook noise + status line. Conditional PreToolUse hooks no longer status-message on every Bash call (Codex was printing 8 "Running hook…" lines on every command). Self-filtering added to `pre-deploy-gate.js` and `pre-push.js` so they never trip on unrelated commands (Claude's substring matcher was firing them on for-loop arguments). Installer now writes `[tui] status_line = [...]` to Codex's `config.toml` for the rich native bottom status line.
11
-
12
- **v6.2.8** — Codex `/goal` integration + install hardening. Phase-start skills now set the Codex thread goal (with token budget) via `bin/codex-goal.js` and `rules/codex-goal.md`. Installer fixes: agent TOMLs now emit `name = "..."` (Codex 0.133 was rejecting all 9), ERP API key is mirrored from `~/.claude/` `~/.codex/`, and deprecated skills (`qualia-task`, `qualia-quick`, `qualia-polish-loop`, `qualia-design`, `qualia-prd`) are pruned on upgrade.
13
-
14
- **v6.2.7** Codex runtime compatibility. The installer now writes Codex-native hooks, TOML agents, bin scripts, rules, skills, templates, knowledge, guide, and role config under `~/.codex/`, not just `AGENTS.md`.
15
-
16
- **The v5 line (preserved):**
17
- - **v5.0**, alignment discipline. CONTEXT.md domain glossary, decisions/ ADRs, zoom/queue helper experiments, slim CLAUDE.md per Matt Pocock's instruction-budget rule, insights-driven hooks.
18
- - **v5.1**, autonomous visual-polish loop. Screenshots a URL at three viewports, scores design dimensions with vision, fixes top issues, loops until pass or kill-switch. Multi-target installer (Claude Code + Codex AGENTS.md + Both).
19
- - **v5.2**, polish-loop reliability. `--reduced-motion` capture flag, `--routes URL1,URL2` multi-route mode, first supervised end-to-end run.
20
- - **v5.3**, Matt Pocock gaps closed. hook-generation utility experiment, `/qualia-optimize --deepen` Step 5b parallel-interface design (3 fan-out agents producing radically different interfaces).
21
- - **v5.4-5.5**, token-discipline and plan-discipline. Cache-aware spawn ordering, scope-reduction prohibition, decision-coverage audit, requirement-coverage check.
22
- - **v5.6**, Demo vs Full Project gate at kickoff. Mandatory discovery interview via `/qualia-discuss` in PROJECT MODE (8 questions for demos, 14 for full projects). Demo-extension branch in `/qualia-milestone` for client-signs-after-demo conversion.
23
- - **v5.7**, `/qualia-feature` consolidates `/qualia-quick` + `/qualia-task` into one auto-scoped command.
24
- - **v5.8**, surface cleanup. `/qualia-polish --loop` replaces `/qualia-polish-loop`. `/qualia-quick`, `/qualia-task`, and `/qualia-prd` removed (deprecated in v5.7).
25
- - **v5.9**, deep-research fixes. Surface-drift test (`tests/refs.test.sh`) catches dead command references on every release. ERP report retry queue (`bin/erp-retry.js`) replaces the v5.8 lying retry message with a real persistent queue. Four structured agents (verifier, plan-checker, roadmapper, qa-browser) move to Sonnet for ~40% per-phase cost cut. Verifier downgrades to FAIL on any `INSUFFICIENT EVIDENCE` line, closing the false-pass vector.
26
- - **v5.9.1**, kickoff UX fix. `/qualia-new` now opens with the Demo/Full/Quick gate as Step 1 (`AskUserQuestion`), then exactly one free-text pitch question, then mandatory hand-off to `/qualia-discuss` — no ad-hoc clarification questioning between them. The shape gate drives the whole downstream interview, so it must come first.
27
- - **v5.9.2**, hook ordering + ERP payload fixes. `pre-push.js` self-gates against `branch-guard.js` so a blocked-push no longer leaves an orphan bot commit in local history. `qualia-report` ERP payload omits empty ISO datetime fields (`session_started_at`, `last_pushed_at`) instead of sending `''`, which the ERP validator rejected as 422.
28
- - **v6.0.0**, audit + cleanup pass. See CHANGELOG for the full list. Highlights: uninstall/migrate manifests fixed, silent hook `catch{}` blocks now traced, phantom `rules/frontend.md` references replaced, `/qualia-learn` and `/qualia-map` declare their actually-used tools, `/qualia-plan` revision-cycle contradiction reconciled (max 2), `agents/planner.md` and `agents/qa-browser.md` MCP tools declared in frontmatter, `rules/trust-boundary.md` extracted, hardcoded `/tmp` paths replaced with `mktemp`, fail-collect test runner, pre-v4 CHANGELOG archived.
29
- - **v6.1.0**, `/qualia-polish --vibe` adds a fast layout-preserving design pivot path and strengthens design-surface guards.
30
- - **v6.2.0**, removes hook-created bot commits. The ERP/report contract is `/qualia-report` POSTs, not passive git scraping of `tracking.json`.
31
- - **v6.2.1**, active-surface drift guard. README, guide, onboarding, ERP contract, road, milestone, polish, verify, and roadmapper wording now align with v6.2 behavior; refs tests fail on the stale claims.
32
- - **v6.2.2**, Framework/Memory/ERP clarity. ERP can hand a work packet into Framework sessions, reports can carry ERP-native IDs, and public npm install proof is a first-class release smoke.
33
- - **v6.2.3**, ERP ID guard. ERP-native IDs are UUID-only in report payloads; slugs remain in `project_id`/`team_id`.
34
- - **v6.2.4**, report payload contract. The ERP payload builder is now a shipped, tested script instead of shell-embedded inline code.
35
- - **v6.2.5**, project snapshot export. Framework can write `.planning/snapshots/project-snapshot-*.json` for explicit ERP/admin import.
36
- - **v6.2.6**, project snapshot upload. Framework can POST that project snapshot directly to ERP's project snapshot intake.
37
- - **v6.2.7**, Codex runtime compatibility. Codex installs now get native `hooks.json`, `agents/*.toml`, runtime scripts, rules, skills, templates, knowledge, guide, and config under `~/.codex/`.
38
-
39
- The Full Journey architecture carries forward: `/qualia-new` maps the entire project arc from kickoff to client handoff upfront, and the Road chains end-to-end in `--auto` mode with only two human gates per project.
1
+ # Qualia Framework
2
+
3
+ A vertical, two-harness, owner-first workflow framework for **Claude Code** and **OpenAI Codex**, opinionated for a Cyprus-based **Next.js · Supabase · Vercel · OpenRouter · Retell** stack.
4
+
5
+ Qualia tells the agent how to plan, build, verify, and ship end-to-end, from kickoff to client handoff. It is not an application framework. It is the harness that makes the agent productive on *your* stack.
6
+
7
+ Read [`SOUL.md`](./SOUL.md) for the identity statement (17 lines).
8
+
9
+ ## First commands
10
+
11
+ ```text
12
+ /qualia-new Set up a new project (kickoff interview researchroadmap)
13
+ /qualia Smart router — "what's my next command?"
14
+ /qualia-feature Add a single feature (auto-scoped, inline or fresh spawn)
15
+ ```
16
+
17
+ The full road and command reference live in [`guide.md`](./guide.md). Every skill flag is in [`FLAGS.md`](./FLAGS.md). When something breaks, see [`TROUBLESHOOTING.md`](./TROUBLESHOOTING.md). Release history is in [`CHANGELOG.md`](./CHANGELOG.md).
40
18
 
41
19
  ## Don't run Claude's `/init` in a Qualia project
42
20
 
package/bin/cli.js CHANGED
@@ -212,7 +212,9 @@ const QUALIA_HOOK_FILES = [
212
212
  ];
213
213
  const QUALIA_LEGACY_HOOK_FILES = [
214
214
  "block-env-edit.js", // removed in v3.2.0
215
- "pre-compact.js", // removed in v6.2.0 state.js journal makes bot-commits redundant
215
+ // pre-compact.js was removed in v6.2.0 and REINSTATED in v6.3.2 with a
216
+ // different mechanism (sidecar snapshot, no git commits). It's an active
217
+ // hook now — not in the legacy list.
216
218
  ];
217
219
 
218
220
  // Qualia agents — only these are removed.
@@ -767,22 +769,24 @@ function cmdMigrate() {
767
769
  }
768
770
  }
769
771
 
770
- // PreCompact: pre-compact.js was removed in v6.2.0 (state.js already provides
771
- // crash-safe atomic writes with a write-ahead journal the bot commit added
772
- // no durability). Strip any legacy entry; drop the event key if it's empty.
773
- if (Array.isArray(settings.hooks.PreCompact)) {
774
- const beforeLen = settings.hooks.PreCompact.length;
775
- settings.hooks.PreCompact = settings.hooks.PreCompact
776
- .map(block => {
777
- if (!block || !Array.isArray(block.hooks)) return block;
778
- return { ...block, hooks: block.hooks.filter(h => extractScriptName(h && h.command) !== "pre-compact.js") };
779
- })
780
- .filter(block => Array.isArray(block.hooks) && block.hooks.length > 0);
781
- const removed = beforeLen !== settings.hooks.PreCompact.length || settings.hooks.PreCompact.length === 0;
782
- if (settings.hooks.PreCompact.length === 0) delete settings.hooks.PreCompact;
783
- if (removed) {
772
+ // PreCompact: wire pre-compact.js (v6.3.2 sidecar snapshot, no git).
773
+ // If a PreCompact array exists, ensure our hook is in it; otherwise create it.
774
+ if (!Array.isArray(settings.hooks.PreCompact)) {
775
+ settings.hooks.PreCompact = [{ matcher: ".*", hooks: [{ type: "command", command: nodeCmd("pre-compact.js"), timeout: 10 }] }];
776
+ changes++;
777
+ console.log(` ${GREEN}+${RESET} Added PreCompact hook (pre-compact.js)`);
778
+ } else {
779
+ let preCompactEntry = settings.hooks.PreCompact.find(e => e.matcher === ".*");
780
+ if (!preCompactEntry) {
781
+ preCompactEntry = { matcher: ".*", hooks: [] };
782
+ settings.hooks.PreCompact.push(preCompactEntry);
783
+ }
784
+ if (!Array.isArray(preCompactEntry.hooks)) preCompactEntry.hooks = [];
785
+ const exists = preCompactEntry.hooks.some(h => extractScriptName(h && h.command) === "pre-compact.js");
786
+ if (!exists) {
787
+ preCompactEntry.hooks.push({ type: "command", command: nodeCmd("pre-compact.js"), timeout: 10 });
784
788
  changes++;
785
- console.log(` ${GREEN}-${RESET} Removed legacy pre-compact.js from PreCompact`);
789
+ console.log(` ${GREEN}+${RESET} Wired pre-compact.js into PreCompact`);
786
790
  }
787
791
  }
788
792
 
@@ -1329,6 +1333,10 @@ function cmdDoctor() {
1329
1333
  "bin/report-payload.js",
1330
1334
  "bin/project-snapshot.js",
1331
1335
  "bin/planning-hygiene.js",
1336
+ "bin/prune-deprecated.js",
1337
+ "bin/learning-candidates.js",
1338
+ "bin/status-snapshot.js",
1339
+ "bin/security-scan.js",
1332
1340
  "knowledge/agents.md",
1333
1341
  "knowledge/index.md",
1334
1342
  "knowledge/daily-log",
@@ -1392,6 +1400,31 @@ function cmdDoctor() {
1392
1400
  check("Codex config.toml status_line parseable", false, e.message);
1393
1401
  }
1394
1402
  }
1403
+
1404
+ // Ghost-skill detection: retired skills must NOT remain in skills/ or the harness
1405
+ // will keep advertising them as live invocable commands ("trap skills").
1406
+ try {
1407
+ const { findGhostSkills, pruneGhostSkills } = require("./prune-deprecated.js");
1408
+ const ghosts = findGhostSkills(home);
1409
+ if (ghosts.length === 0) {
1410
+ check(`${label} no retired ghost skills`, true);
1411
+ } else {
1412
+ // Auto-prune. Doctor is the right place — users run it often, and the action
1413
+ // is safe (RETIRED_SKILLS is a static, hand-curated list).
1414
+ const { removed, errors } = pruneGhostSkills(home);
1415
+ if (errors.length > 0) {
1416
+ // Don't silently swallow filesystem errors — surface them so the user
1417
+ // can fix the underlying permission/mount issue. Trap skills will keep
1418
+ // appearing until the prune actually succeeds.
1419
+ const hint = errors.map((e) => `${e.name}: ${e.error}`).join("; ");
1420
+ check(`${label} ghost-skill prune (${removed.length} ok, ${errors.length} failed)`, false, hint);
1421
+ } else {
1422
+ check(`${label} pruned ${removed.length} ghost skill(s): ${removed.join(", ")}`, true);
1423
+ }
1424
+ }
1425
+ } catch (e) {
1426
+ check(`${label} ghost-skill prune`, false, e.message);
1427
+ }
1395
1428
  }
1396
1429
 
1397
1430
  // ── Version vs. installed ──────────────────────────────
@@ -1562,6 +1595,9 @@ function cmdHelp() {
1562
1595
  console.log(` qualia-framework ${TEAL}trust${RESET} Score install, state, contracts, memory, ERP (${DIM}--json${RESET})`);
1563
1596
  console.log(` qualia-framework ${TEAL}eval${RESET} Write/run project harness eval scoring (${DIM}--run --write --json${RESET})`);
1564
1597
  console.log(` qualia-framework ${TEAL}flush${RESET} Promote daily-log → curated knowledge (memory layer)`);
1598
+ console.log(` qualia-framework ${TEAL}learn-scan${RESET} Scan recent commits + daily-log for repeated patterns worth promoting (${DIM}--since=N --print${RESET})`);
1599
+ console.log(` qualia-framework ${TEAL}status${RESET} Portable operator snapshot — install + project + work + ERP + memory (${DIM}--write --json --exit-code${RESET})`);
1600
+ console.log(` qualia-framework ${TEAL}secure${RESET} Security scan of agent config — secrets/permissions/hook hygiene (${DIM}--json --write --paths${RESET})`);
1565
1601
  console.log("");
1566
1602
  console.log(` ${WHITE}After install:${RESET}`);
1567
1603
  console.log(` ${TG}/qualia${RESET} What should I do next?`);
@@ -1659,6 +1695,18 @@ switch (cmd) {
1659
1695
  case "knowledge-flush":
1660
1696
  cmdFlush();
1661
1697
  break;
1698
+ case "learn-scan":
1699
+ case "learning-candidates":
1700
+ require("./learning-candidates.js").main();
1701
+ break;
1702
+ case "status":
1703
+ case "operator-status":
1704
+ require("./status-snapshot.js").main();
1705
+ break;
1706
+ case "secure":
1707
+ case "security-scan":
1708
+ require("./security-scan.js").main();
1709
+ break;
1662
1710
  default:
1663
1711
  cmdHelp();
1664
1712
  }
@@ -28,6 +28,8 @@ const ACTIVE_SKILLS = [
28
28
  "qualia-road",
29
29
  "qualia-learn",
30
30
  "qualia-postmortem",
31
+ "qualia-idk",
32
+ "qualia-secure",
31
33
  "zoho-workflow",
32
34
  ];
33
35
 
@@ -43,7 +45,9 @@ const RETIRED_SKILLS = [
43
45
  "qualia-debug", // folded into qualia-fix for actionable repairs
44
46
  "qualia-vibe", // folded into qualia-polish modes/documentation
45
47
  "qualia-help", // guide/help files remain installed; no slash command
46
- "qualia-idk", // folded into qualia router diagnostic branch
48
+ // qualia-idk RESTORED in v6.3.1 owner pivot: keep the deep diagnostic
49
+ // separate from the cheap router. /qualia stays mechanical, /qualia-idk
50
+ // does the heavy three-scan synthesis with paste-ready command sequence.
47
51
  "qualia-pause", // folded into qualia router handoff branch
48
52
  "qualia-resume", // folded into qualia router handoff branch
49
53
  "qualia-zoom", // folded into qualia-map/qualia-review as an analysis mode
package/bin/install.js CHANGED
@@ -661,12 +661,18 @@ async function main() {
661
661
  } catch {}
662
662
  // Purge deprecated hooks from existing installs on upgrade.
663
663
  // - block-env-edit.js (v3.2.0): team now has full read/write on .env*.
664
- // - pre-compact.js (v6.2.0): bot-committed STATE.md + tracking.json on
665
- // context compaction for ERP visibility. ERP never read tracking.json
666
- // from git, and state.js already provides crash-safe atomic writes with
667
- // a write-ahead journal (state.js:36-64) the bot commit added no
668
- // durability, just history noise.
669
- const DEPRECATED_HOOKS = ["block-env-edit.js", "pre-compact.js"];
664
+ //
665
+ // Note: pre-compact.js was removed in v6.2.0 (it bot-committed STATE.md +
666
+ // tracking.json, which added no durability over state.js's atomic writes
667
+ // + journal). v6.3.2 reintroduces pre-compact.js but with a fundamentally
668
+ // different mechanism — it writes a markdown SIDECAR to
669
+ // .planning/.compaction-snapshot.md (no git, no state.js writes) so the
670
+ // next session can see what was in flight when compaction wiped context.
671
+ // Safe to ship as a fresh install; the v6.2.0 stripping logic in cli.js
672
+ // doctor remains, but only strips the OLD legacy command (which our v2
673
+ // hook does not match — different content, same filename is fine because
674
+ // install always overwrites).
675
+ const DEPRECATED_HOOKS = ["block-env-edit.js"];
670
676
  for (const f of DEPRECATED_HOOKS) {
671
677
  const p = path.join(hooksDest, f);
672
678
  try { if (fs.existsSync(p)) fs.unlinkSync(p); } catch {}
@@ -1100,11 +1106,20 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
1100
1106
  ],
1101
1107
  },
1102
1108
  ],
1103
- // v6.2.0: PreCompact intentionally empty. The qualiaHooks loop in the
1104
- // settings.json merge below still iterates over this key, so legacy
1105
- // Qualia-owned pre-compact.js entries get stripped from existing user
1106
- // settings on upgrade. Nothing new is wired in.
1107
- PreCompact: [],
1109
+ // v6.3.2: PreCompact reintroduced with a NEW mechanism. The old hook
1110
+ // (removed in v6.2.0) bot-committed STATE.md + tracking.json. This v2
1111
+ // hook writes a markdown SIDECAR to .planning/.compaction-snapshot.md
1112
+ // (no git, no state.js writes). The qualiaHooks loop below still
1113
+ // iterates over this key, so any leftover legacy pre-compact.js wiring
1114
+ // is replaced by the new one cleanly.
1115
+ PreCompact: [
1116
+ {
1117
+ matcher: ".*",
1118
+ hooks: [
1119
+ { type: "command", command: nodeCmd("pre-compact.js"), timeout: 10 },
1120
+ ],
1121
+ },
1122
+ ],
1108
1123
  Stop: [
1109
1124
  {
1110
1125
  matcher: ".*",
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env node
2
+ // bin/learning-candidates.js — scan recent git history + daily-log for
3
+ // repeated patterns and emit a list of skill-creation candidates.
4
+ //
5
+ // Run via:
6
+ // qualia-framework learn-scan # writes ~/.claude/knowledge/learning-candidates.md
7
+ // qualia-framework learn-scan --print # also prints to stdout
8
+ // qualia-framework learn-scan --since=7 # look at last 7 days (default: 14)
9
+ //
10
+ // What it detects:
11
+ // 1. Repeated fix-scope patterns — "fix(auth):" appearing 3+ times in 14 days
12
+ // → suggest a skill or hook that prevents/diagnoses that class of bug.
13
+ // 2. Repeated touched-file patterns — same file appearing in 4+ session
14
+ // checkpoints → suggest factoring or codifying the workflow around it.
15
+ // 3. Repeated phrases in daily-log entries — heuristic, low-signal v1.
16
+ //
17
+ // What it does NOT do:
18
+ // - Auto-create skills. That's /qualia-skill-new's job (with human review).
19
+ // - Touch the wiki tier (knowledge/concepts/). That's /qualia-flush's job.
20
+ // - Run continuously in background. ECC's Haiku observer is over-engineered
21
+ // for Qualia's weekly cadence. Run this manually or via /qualia-flush.
22
+
23
+ const fs = require("fs");
24
+ const path = require("path");
25
+ const os = require("os");
26
+ const { spawnSync } = require("child_process");
27
+
28
+ function qualiaHome() {
29
+ if (process.env.QUALIA_HOME) return process.env.QUALIA_HOME;
30
+ const parent = path.basename(path.dirname(__dirname));
31
+ if (parent === ".codex" || parent === ".claude") return path.dirname(__dirname);
32
+ return path.join(os.homedir(), ".claude");
33
+ }
34
+
35
+ function git(args, opts = {}) {
36
+ try {
37
+ const r = spawnSync("git", args, { encoding: "utf8", timeout: 3000, shell: process.platform === "win32", ...opts });
38
+ if (r.status !== 0) return "";
39
+ return (r.stdout || "").trim();
40
+ } catch {
41
+ return "";
42
+ }
43
+ }
44
+
45
+ function parseArgs(argv) {
46
+ const args = { print: false, sinceDays: 14 };
47
+ for (let i = 0; i < argv.length; i++) {
48
+ const a = argv[i];
49
+ if (a === "--print") args.print = true;
50
+ else if (a.startsWith("--since=")) {
51
+ const n = parseInt(a.split("=")[1], 10);
52
+ if (n > 0) args.sinceDays = n;
53
+ } else if (a === "--since" && argv[i + 1]) {
54
+ const n = parseInt(argv[++i], 10);
55
+ if (n > 0) args.sinceDays = n;
56
+ }
57
+ }
58
+ return args;
59
+ }
60
+
61
+ // Conventional Commits subject parser. Returns {type, scope} or null.
62
+ function parseCommitSubject(subject) {
63
+ const m = subject.match(/^([a-z]+)(?:\(([^)]+)\))?(!)?:\s*(.+)$/i);
64
+ if (!m) return null;
65
+ return { type: m[1].toLowerCase(), scope: m[2] || "", description: m[4] };
66
+ }
67
+
68
+ function scanRepoCommits(repoRoot, sinceDays) {
69
+ const out = git(["log", `--since=${sinceDays}.days`, "--pretty=%H%x09%s"], { cwd: repoRoot });
70
+ if (!out) return [];
71
+ const commits = [];
72
+ for (const line of out.split("\n")) {
73
+ const [sha, subject] = line.split("\t");
74
+ if (!sha || !subject) continue;
75
+ const parsed = parseCommitSubject(subject);
76
+ if (parsed) commits.push({ sha, subject, ...parsed });
77
+ }
78
+ return commits;
79
+ }
80
+
81
+ function aggregateFixPatterns(commits) {
82
+ // Group fix-type commits by scope (or by first-word of description if no scope).
83
+ const groups = new Map();
84
+ for (const c of commits) {
85
+ if (c.type !== "fix") continue;
86
+ const key = c.scope || c.description.split(" ")[0].toLowerCase().replace(/[^a-z0-9-]/g, "");
87
+ if (!key) continue;
88
+ const arr = groups.get(key) || [];
89
+ arr.push(c);
90
+ groups.set(key, arr);
91
+ }
92
+ return [...groups.entries()]
93
+ .filter(([, arr]) => arr.length >= 3)
94
+ .sort((a, b) => b[1].length - a[1].length)
95
+ .map(([scope, arr]) => ({ scope, count: arr.length, samples: arr.slice(0, 3).map((c) => `${c.sha.slice(0, 7)} ${c.subject}`) }));
96
+ }
97
+
98
+ function readDailyLogs(qhome, sinceDays) {
99
+ const dir = path.join(qhome, "knowledge", "daily-log");
100
+ if (!fs.existsSync(dir)) return [];
101
+ const cutoff = Date.now() - sinceDays * 86_400_000;
102
+ const out = [];
103
+ for (const f of fs.readdirSync(dir)) {
104
+ if (!f.endsWith(".md")) continue;
105
+ const m = f.match(/^(\d{4}-\d{2}-\d{2})\.md$/);
106
+ if (!m) continue;
107
+ const date = new Date(m[1] + "T00:00:00Z").getTime();
108
+ if (date < cutoff) continue;
109
+ try {
110
+ out.push({ date: m[1], content: fs.readFileSync(path.join(dir, f), "utf8") });
111
+ } catch {}
112
+ }
113
+ return out;
114
+ }
115
+
116
+ function aggregateTouchedFiles(dailyLogs) {
117
+ // Look for "touched=a,b,c" across entries. Count occurrences per file.
118
+ const counts = new Map();
119
+ for (const log of dailyLogs) {
120
+ const matches = log.content.matchAll(/touched=([^\s·]+)/g);
121
+ for (const m of matches) {
122
+ for (const f of m[1].split(",")) {
123
+ const k = f.trim();
124
+ if (!k) continue;
125
+ counts.set(k, (counts.get(k) || 0) + 1);
126
+ }
127
+ }
128
+ }
129
+ return [...counts.entries()]
130
+ .filter(([, n]) => n >= 4)
131
+ .sort((a, b) => b[1] - a[1])
132
+ .slice(0, 10)
133
+ .map(([file, count]) => ({ file, count }));
134
+ }
135
+
136
+ function render({ sinceDays, fixPatterns, touchedFiles, repoRoot }) {
137
+ const lines = [];
138
+ lines.push(`# Learning candidates — generated ${new Date().toISOString()}`);
139
+ lines.push("");
140
+ lines.push(`Scope: last ${sinceDays} days.`);
141
+ if (repoRoot) lines.push(`Repo: ${repoRoot}`);
142
+ lines.push("");
143
+ lines.push("Each candidate below is a pattern that recurred enough times to be worth promoting into a skill, agent, or hook. Review and act:");
144
+ lines.push("- `/qualia-skill-new` — create a Qualia skill for the workflow");
145
+ lines.push("- `/qualia-hook-gen` — convert a CLAUDE.md instruction into a deterministic hook");
146
+ lines.push("- `/qualia-learn` — save a one-off learning to the knowledge wiki");
147
+ lines.push("- Ignore — patterns that aren't worth automating");
148
+ lines.push("");
149
+
150
+ lines.push("## Repeated fix-scopes (Conventional Commits)");
151
+ if (fixPatterns.length === 0) {
152
+ lines.push("(none — no scope has 3+ fixes in the window)");
153
+ } else {
154
+ for (const fp of fixPatterns) {
155
+ lines.push(`### \`fix(${fp.scope})\` — ${fp.count} commits`);
156
+ lines.push("Recent samples:");
157
+ for (const s of fp.samples) lines.push(`- ${s}`);
158
+ lines.push("");
159
+ lines.push(`> **Suggested action:** if this keeps recurring, add a guard hook or a /qualia-skill-new dedicated to debugging \`${fp.scope}\`.`);
160
+ lines.push("");
161
+ }
162
+ }
163
+
164
+ lines.push("## Repeatedly-touched files");
165
+ if (touchedFiles.length === 0) {
166
+ lines.push("(none — no file appeared in 4+ session checkpoints)");
167
+ } else {
168
+ lines.push("File | Touched count");
169
+ lines.push("---|---:");
170
+ for (const t of touchedFiles) lines.push(`\`${t.file}\` | ${t.count}`);
171
+ lines.push("");
172
+ lines.push("> **Suggested action:** files touched this often may be a hotspot (deserves more tests) or a friction point (deserves a skill that automates the recurring edit).");
173
+ }
174
+ lines.push("");
175
+ lines.push("---");
176
+ lines.push("_Generated by `bin/learning-candidates.js`. Re-run with `qualia-framework learn-scan` or `qualia-framework learn-scan --since=30`._");
177
+
178
+ return lines.join("\n") + "\n";
179
+ }
180
+
181
+ function main() {
182
+ const args = parseArgs(process.argv.slice(2));
183
+ const qhome = qualiaHome();
184
+ const cwd = process.cwd();
185
+ const repoRoot = git(["rev-parse", "--show-toplevel"], { cwd }) || "";
186
+
187
+ const commits = repoRoot ? scanRepoCommits(repoRoot, args.sinceDays) : [];
188
+ const fixPatterns = aggregateFixPatterns(commits);
189
+ const dailyLogs = readDailyLogs(qhome, args.sinceDays);
190
+ const touchedFiles = aggregateTouchedFiles(dailyLogs);
191
+
192
+ const doc = render({ sinceDays: args.sinceDays, fixPatterns, touchedFiles, repoRoot });
193
+
194
+ const outDir = path.join(qhome, "knowledge");
195
+ if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
196
+ const outPath = path.join(outDir, "learning-candidates.md");
197
+ fs.writeFileSync(outPath, doc);
198
+
199
+ // Stamp last-scan time.
200
+ fs.writeFileSync(path.join(qhome, ".qualia-last-learning-scan"), String(Date.now()));
201
+
202
+ if (args.print) {
203
+ process.stdout.write(doc);
204
+ } else {
205
+ console.log(`Wrote ${fixPatterns.length} fix-pattern candidate(s) + ${touchedFiles.length} hot file(s) to ${outPath}`);
206
+ }
207
+ }
208
+
209
+ module.exports = { main, aggregateFixPatterns, aggregateTouchedFiles, parseCommitSubject };
210
+
211
+ if (require.main === module) {
212
+ try { main(); }
213
+ catch (e) {
214
+ console.error(`learn-scan failed: ${e.message}`);
215
+ process.exit(1);
216
+ }
217
+ }
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ // Shared helper: prune retired skill folders from an install home (~/.claude or ~/.codex).
3
+ //
4
+ // Why a separate file: install.js already runs prune on install, but users often run
5
+ // `qualia-framework doctor` more frequently than they re-install. When a skill is
6
+ // retired (moved from ACTIVE_SKILLS → RETIRED_SKILLS in command-surface.js), the
7
+ // folder must also be removed from the install home, or the harness will keep
8
+ // advertising it as live and users will invoke "trap skills" that no longer route.
9
+ //
10
+ // Both install.js and cli.js (doctor) call this.
11
+
12
+ const fs = require("fs");
13
+ const path = require("path");
14
+ const { RETIRED_SKILLS } = require("./command-surface.js");
15
+
16
+ function findGhostSkills(baseDir) {
17
+ const skillsDir = path.join(baseDir, "skills");
18
+ if (!fs.existsSync(skillsDir)) return [];
19
+ return RETIRED_SKILLS.filter((name) => fs.existsSync(path.join(skillsDir, name)));
20
+ }
21
+
22
+ // Returns { removed: [names], errors: [{name, target, error}] }.
23
+ // Callers can decide how to surface errors — the doctor flow logs them, the
24
+ // install flow shows them as warnings. We do NOT silently swallow them (per
25
+ // CodeRabbit review on PR #46): a permissions / mount-point failure during
26
+ // prune leaves stale ghost skills installed and the user has no signal.
27
+ function pruneGhostSkills(baseDir) {
28
+ const skillsDir = path.join(baseDir, "skills");
29
+ if (!fs.existsSync(skillsDir)) return { removed: [], errors: [] };
30
+ const removed = [];
31
+ const errors = [];
32
+ for (const name of RETIRED_SKILLS) {
33
+ const target = path.join(skillsDir, name);
34
+ try {
35
+ if (fs.existsSync(target)) {
36
+ fs.rmSync(target, { recursive: true, force: true });
37
+ removed.push(name);
38
+ }
39
+ } catch (err) {
40
+ errors.push({ name, target, error: err && err.message ? err.message : String(err) });
41
+ }
42
+ }
43
+ return { removed, errors };
44
+ }
45
+
46
+ module.exports = { findGhostSkills, pruneGhostSkills };
47
+
48
+ if (require.main === module) {
49
+ // CLI: `node prune-deprecated.js <baseDir>` — useful for manual cleanup.
50
+ const baseDir = process.argv[2] || path.join(require("os").homedir(), ".claude");
51
+ const { removed, errors } = pruneGhostSkills(baseDir);
52
+ if (errors.length > 0) {
53
+ console.error(`Pruned ${removed.length} ghost skill(s); ${errors.length} failed:`);
54
+ for (const e of errors) console.error(` ✗ ${e.name} (${e.target}): ${e.error}`);
55
+ if (removed.length > 0) for (const name of removed) console.log(` - ${name}`);
56
+ process.exit(1);
57
+ }
58
+ if (removed.length === 0) {
59
+ console.log(`No ghost skills in ${baseDir}/skills/`);
60
+ } else {
61
+ console.log(`Pruned ${removed.length} ghost skill(s) from ${baseDir}/skills/:`);
62
+ for (const name of removed) console.log(` - ${name}`);
63
+ }
64
+ }
@@ -23,6 +23,10 @@ const RUNTIME_BIN_SCRIPTS = [
23
23
  { file: "harness-eval.js", label: "harness-eval.js (project eval scoring + evidence artifact)" },
24
24
  { file: "codex-goal.js", label: "codex-goal.js (Codex /goal objective + token-budget suggester)" },
25
25
  { file: "planning-hygiene.js", label: "planning-hygiene.js (.planning organization scanner)" },
26
+ { file: "prune-deprecated.js", label: "prune-deprecated.js (ghost-skill cleanup for retired commands)" },
27
+ { file: "learning-candidates.js", label: "learning-candidates.js (scan recent commits + daily-log for patterns worth promoting)" },
28
+ { file: "status-snapshot.js", label: "status-snapshot.js (portable operator snapshot — install + project + work + ERP + memory)" },
29
+ { file: "security-scan.js", label: "security-scan.js (static security scanner for agent config — secrets, permissions, hook hygiene)" },
26
30
  ];
27
31
 
28
32
  function binFiles() {