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.
- package/AGENTS.md +8 -8
- package/CLAUDE.md +5 -5
- package/README.md +17 -39
- package/bin/cli.js +64 -16
- package/bin/command-surface.js +5 -1
- package/bin/install.js +26 -11
- package/bin/learning-candidates.js +217 -0
- package/bin/prune-deprecated.js +64 -0
- package/bin/runtime-manifest.js +4 -0
- package/bin/security-scan.js +409 -0
- package/bin/status-snapshot.js +363 -0
- package/guide.md +11 -33
- package/hooks/pre-compact.js +232 -0
- package/package.json +1 -1
- package/skills/qualia/SKILL.md +1 -1
- package/skills/qualia-build/SKILL.md +1 -1
- package/skills/qualia-discuss/SKILL.md +1 -1
- package/skills/qualia-doctor/SKILL.md +1 -1
- package/skills/qualia-feature/SKILL.md +1 -1
- package/skills/qualia-fix/SKILL.md +1 -1
- package/skills/qualia-idk/SKILL.md +245 -0
- package/skills/qualia-learn/SKILL.md +1 -1
- package/skills/qualia-map/SKILL.md +1 -1
- package/skills/qualia-milestone/SKILL.md +1 -1
- package/skills/qualia-new/SKILL.md +1 -1
- package/skills/qualia-optimize/SKILL.md +1 -1
- package/skills/qualia-plan/SKILL.md +1 -1
- package/skills/qualia-polish/SKILL.md +1 -1
- package/skills/qualia-postmortem/SKILL.md +1 -1
- package/skills/qualia-report/SKILL.md +1 -1
- package/skills/qualia-research/SKILL.md +1 -1
- package/skills/qualia-review/SKILL.md +1 -1
- package/skills/qualia-road/SKILL.md +1 -1
- package/skills/qualia-secure/SKILL.md +105 -0
- package/skills/qualia-test/SKILL.md +1 -1
- package/skills/qualia-verify/SKILL.md +1 -1
- package/skills/zoho-workflow/SKILL.md +1 -1
- package/tests/bin.test.sh +9 -9
- package/tests/install-smoke.test.sh +3 -3
- package/tests/lib.test.sh +6 -6
- package/tests/published-install-smoke.test.sh +3 -3
- package/tests/refs.test.sh +29 -22
- package/tests/runner.js +3 -3
- 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 —
|
|
11
|
-
- Feature branches only —
|
|
12
|
-
- MVP first — build
|
|
13
|
-
- Root cause on failures —
|
|
14
|
-
- No proxy approval —
|
|
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` —
|
|
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` —
|
|
21
|
-
- `qualia-design/frontend.md` `qualia-design/design-laws.md` —
|
|
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 —
|
|
11
|
-
- Feature branches only —
|
|
12
|
-
- MVP first — build
|
|
13
|
-
- Root cause on failures —
|
|
14
|
-
- No proxy approval —
|
|
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
|
|
2
|
-
|
|
3
|
-
A harness
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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 → research → roadmap)
|
|
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
|
-
|
|
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
|
|
771
|
-
//
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
}
|
|
780
|
-
.
|
|
781
|
-
|
|
782
|
-
if (
|
|
783
|
-
|
|
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}
|
|
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
|
}
|
package/bin/command-surface.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
|
|
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
|
|
1104
|
-
//
|
|
1105
|
-
//
|
|
1106
|
-
//
|
|
1107
|
-
|
|
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
|
+
}
|
package/bin/runtime-manifest.js
CHANGED
|
@@ -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() {
|