qualia-framework 4.1.1 → 4.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/README.md +15 -11
- package/agents/builder.md +28 -0
- package/agents/research-synthesizer.md +7 -0
- package/bin/agent-runs.js +233 -0
- package/bin/cli.js +355 -16
- package/bin/install.js +87 -6
- package/bin/knowledge-flush.js +164 -0
- package/bin/knowledge.js +317 -0
- package/bin/plan-contract.js +220 -0
- package/bin/state.js +15 -9
- package/docs/agent-runs.md +273 -0
- package/docs/journey-demo.html +1008 -0
- package/docs/plan-contract.md +321 -0
- package/docs/reviews/v4.1.0-audit.html +1488 -0
- package/docs/reviews/v4.1.0-audit.md +263 -0
- package/hooks/auto-update.js +3 -7
- package/hooks/git-guardrails.js +167 -0
- package/hooks/pre-compact.js +22 -11
- package/hooks/pre-deploy-gate.js +16 -2
- package/hooks/pre-push.js +22 -2
- package/hooks/stop-session-log.js +180 -0
- package/package.json +8 -2
- package/skills/qualia-build/SKILL.md +5 -5
- package/skills/qualia-debug/SKILL.md +1 -1
- package/skills/qualia-design/SKILL.md +15 -0
- package/skills/qualia-flush/SKILL.md +200 -0
- package/skills/qualia-learn/SKILL.md +47 -37
- package/skills/qualia-new/SKILL.md +1 -1
- package/skills/qualia-plan/SKILL.md +3 -2
- package/skills/qualia-postmortem/SKILL.md +238 -0
- package/skills/qualia-quick/SKILL.md +1 -1
- package/skills/qualia-report/SKILL.md +1 -1
- package/skills/qualia-review/SKILL.md +3 -2
- package/skills/qualia-ship/SKILL.md +12 -10
- package/skills/qualia-verify/SKILL.md +60 -0
- package/templates/help.html +13 -7
- package/templates/knowledge/agents.md +71 -0
- package/templates/knowledge/index.md +47 -0
- package/tests/bin.test.sh +322 -12
- package/tests/hooks.test.sh +131 -20
- package/tests/lib.test.sh +217 -0
- package/tests/runner.js +103 -77
- package/tests/state.test.sh +4 -3
package/bin/cli.js
CHANGED
|
@@ -29,7 +29,8 @@ function readConfig() {
|
|
|
29
29
|
|
|
30
30
|
function writeConfig(cfg) {
|
|
31
31
|
if (!fs.existsSync(CLAUDE_DIR)) fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
32
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n");
|
|
32
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", { mode: 0o600 });
|
|
33
|
+
try { fs.chmodSync(CONFIG_FILE, 0o600); } catch {}
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
function banner() {
|
|
@@ -139,6 +140,8 @@ const QUALIA_HOOK_FILES = [
|
|
|
139
140
|
"migration-guard.js",
|
|
140
141
|
"pre-deploy-gate.js",
|
|
141
142
|
"pre-compact.js",
|
|
143
|
+
"git-guardrails.js",
|
|
144
|
+
"stop-session-log.js",
|
|
142
145
|
];
|
|
143
146
|
const QUALIA_LEGACY_HOOK_FILES = [
|
|
144
147
|
"block-env-edit.js", // removed in v3.2.0
|
|
@@ -157,10 +160,10 @@ const QUALIA_AGENT_FILES = [
|
|
|
157
160
|
];
|
|
158
161
|
|
|
159
162
|
// 3 Qualia bin scripts.
|
|
160
|
-
const QUALIA_BIN_FILES = ["state.js", "qualia-ui.js", "statusline.js"];
|
|
163
|
+
const QUALIA_BIN_FILES = ["state.js", "qualia-ui.js", "statusline.js", "knowledge.js", "knowledge-flush.js", "plan-contract.js", "agent-runs.js"];
|
|
161
164
|
|
|
162
|
-
//
|
|
163
|
-
const QUALIA_RULE_FILES = ["security.md", "frontend.md", "design-reference.md", "deployment.md", "infrastructure.md"];
|
|
165
|
+
// 6 Qualia rules.
|
|
166
|
+
const QUALIA_RULE_FILES = ["security.md", "frontend.md", "design-reference.md", "deployment.md", "infrastructure.md", "grounding.md"];
|
|
164
167
|
|
|
165
168
|
function promptYesNo(question, defaultYes) {
|
|
166
169
|
return new Promise((resolve) => {
|
|
@@ -551,7 +554,7 @@ function cmdMigrate() {
|
|
|
551
554
|
console.log(` ${DIM}Target version:${RESET} ${WHITE}${PKG.version}${RESET}`);
|
|
552
555
|
console.log("");
|
|
553
556
|
|
|
554
|
-
// 1. Ensure
|
|
557
|
+
// 1. Ensure the full v4.3 hook set is wired.
|
|
555
558
|
const hd = path.join(CLAUDE_DIR, "hooks");
|
|
556
559
|
const nodeCmd = (hookFile) => `node "${path.join(hd, hookFile)}"`;
|
|
557
560
|
|
|
@@ -562,10 +565,19 @@ function cmdMigrate() {
|
|
|
562
565
|
settings.hooks.SessionStart = [{ matcher: ".*", hooks: [{ type: "command", command: nodeCmd("session-start.js"), timeout: 5 }] }];
|
|
563
566
|
changes++;
|
|
564
567
|
console.log(` ${GREEN}+${RESET} Added SessionStart hook`);
|
|
568
|
+
} else {
|
|
569
|
+
const hasSessionStart = settings.hooks.SessionStart.some(e =>
|
|
570
|
+
Array.isArray(e.hooks) && e.hooks.some(h => typeof h.command === "string" && h.command.includes("session-start.js"))
|
|
571
|
+
);
|
|
572
|
+
if (!hasSessionStart) {
|
|
573
|
+
settings.hooks.SessionStart.push({ matcher: ".*", hooks: [{ type: "command", command: nodeCmd("session-start.js"), timeout: 5 }] });
|
|
574
|
+
changes++;
|
|
575
|
+
console.log(` ${GREEN}+${RESET} Wired session-start.js into SessionStart`);
|
|
576
|
+
}
|
|
565
577
|
}
|
|
566
578
|
|
|
567
579
|
// Check PreToolUse hooks — ensure all critical hooks are present
|
|
568
|
-
const requiredBashHooks = ["auto-update.js", "branch-guard.js", "pre-push.js", "pre-deploy-gate.js"];
|
|
580
|
+
const requiredBashHooks = ["auto-update.js", "git-guardrails.js", "branch-guard.js", "pre-push.js", "pre-deploy-gate.js"];
|
|
569
581
|
const requiredEditHooks = ["migration-guard.js"];
|
|
570
582
|
|
|
571
583
|
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
@@ -593,9 +605,10 @@ function cmdMigrate() {
|
|
|
593
605
|
const exists = bashEntry.hooks.some(h => extractScriptName(h.command) === targetName);
|
|
594
606
|
if (!exists) {
|
|
595
607
|
const hookDef = { type: "command", command: cmd, timeout: hookFile === "pre-deploy-gate.js" ? 180 : 5 };
|
|
596
|
-
if (hookFile === "
|
|
608
|
+
if (hookFile === "git-guardrails.js") hookDef.statusMessage = "⬢ Checking git safety...";
|
|
609
|
+
if (hookFile === "branch-guard.js") { hookDef.if = "Bash(git push*)"; hookDef.statusMessage = "⬢ Checking branch permissions..."; }
|
|
597
610
|
if (hookFile === "pre-push.js") { hookDef.if = "Bash(git push*)"; hookDef.timeout = 15; }
|
|
598
|
-
if (hookFile === "pre-deploy-gate.js") hookDef.if = "Bash(vercel --prod*)";
|
|
611
|
+
if (hookFile === "pre-deploy-gate.js") { hookDef.if = "Bash(vercel --prod*)"; hookDef.timeout = 180; hookDef.statusMessage = "⬢ Running quality gates..."; }
|
|
599
612
|
bashEntry.hooks.push(hookDef);
|
|
600
613
|
changes++;
|
|
601
614
|
console.log(` ${GREEN}+${RESET} Wired ${hookFile} into PreToolUse/Bash`);
|
|
@@ -616,7 +629,7 @@ function cmdMigrate() {
|
|
|
616
629
|
const exists = editEntry.hooks.some(h => extractScriptName(h.command) === targetName);
|
|
617
630
|
if (!exists) {
|
|
618
631
|
const hookDef = { type: "command", command: cmd, timeout: hookFile === "migration-guard.js" ? 10 : 5 };
|
|
619
|
-
if (hookFile === "migration-guard.js") hookDef.if = "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)";
|
|
632
|
+
if (hookFile === "migration-guard.js") { hookDef.if = "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)"; hookDef.statusMessage = "⬢ Checking migration safety..."; }
|
|
620
633
|
editEntry.hooks.push(hookDef);
|
|
621
634
|
changes++;
|
|
622
635
|
console.log(` ${GREEN}+${RESET} Wired ${hookFile} into PreToolUse/Edit|Write`);
|
|
@@ -624,10 +637,42 @@ function cmdMigrate() {
|
|
|
624
637
|
}
|
|
625
638
|
|
|
626
639
|
// Check PreCompact hook
|
|
627
|
-
if (!settings.hooks.PreCompact) {
|
|
640
|
+
if (!settings.hooks.PreCompact || !Array.isArray(settings.hooks.PreCompact)) {
|
|
628
641
|
settings.hooks.PreCompact = [{ matcher: "compact", hooks: [{ type: "command", command: nodeCmd("pre-compact.js"), timeout: 15 }] }];
|
|
629
642
|
changes++;
|
|
630
643
|
console.log(` ${GREEN}+${RESET} Added PreCompact hook`);
|
|
644
|
+
} else {
|
|
645
|
+
let compactEntry = settings.hooks.PreCompact.find(e => e.matcher === "compact");
|
|
646
|
+
if (!compactEntry) {
|
|
647
|
+
compactEntry = { matcher: "compact", hooks: [] };
|
|
648
|
+
settings.hooks.PreCompact.push(compactEntry);
|
|
649
|
+
}
|
|
650
|
+
if (!compactEntry.hooks) compactEntry.hooks = [];
|
|
651
|
+
const exists = compactEntry.hooks.some(h => extractScriptName(h.command) === "pre-compact.js");
|
|
652
|
+
if (!exists) {
|
|
653
|
+
compactEntry.hooks.push({ type: "command", command: nodeCmd("pre-compact.js"), timeout: 15, statusMessage: "⬢ Saving state..." });
|
|
654
|
+
changes++;
|
|
655
|
+
console.log(` ${GREEN}+${RESET} Wired pre-compact.js into PreCompact`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (!settings.hooks.Stop || !Array.isArray(settings.hooks.Stop)) {
|
|
660
|
+
settings.hooks.Stop = [{ matcher: ".*", hooks: [{ type: "command", command: nodeCmd("stop-session-log.js"), timeout: 5 }] }];
|
|
661
|
+
changes++;
|
|
662
|
+
console.log(` ${GREEN}+${RESET} Added Stop hook`);
|
|
663
|
+
} else {
|
|
664
|
+
let stopEntry = settings.hooks.Stop.find(e => e.matcher === ".*");
|
|
665
|
+
if (!stopEntry) {
|
|
666
|
+
stopEntry = { matcher: ".*", hooks: [] };
|
|
667
|
+
settings.hooks.Stop.push(stopEntry);
|
|
668
|
+
}
|
|
669
|
+
if (!stopEntry.hooks) stopEntry.hooks = [];
|
|
670
|
+
const exists = stopEntry.hooks.some(h => extractScriptName(h.command) === "stop-session-log.js");
|
|
671
|
+
if (!exists) {
|
|
672
|
+
stopEntry.hooks.push({ type: "command", command: nodeCmd("stop-session-log.js"), timeout: 5 });
|
|
673
|
+
changes++;
|
|
674
|
+
console.log(` ${GREEN}+${RESET} Wired stop-session-log.js into Stop`);
|
|
675
|
+
}
|
|
631
676
|
}
|
|
632
677
|
|
|
633
678
|
// 2. Ensure env vars are up to date
|
|
@@ -728,12 +773,12 @@ function cmdAnalytics() {
|
|
|
728
773
|
}
|
|
729
774
|
|
|
730
775
|
// Verification outcomes (from traces that include verification data)
|
|
731
|
-
const verifications = traces.filter(t => t.hook === "state-transition" && t.
|
|
732
|
-
const passes = verifications.filter(t => t.
|
|
733
|
-
const fails = verifications.filter(t => t.
|
|
776
|
+
const verifications = traces.filter(t => t.hook === "state-transition" && t.verification);
|
|
777
|
+
const passes = verifications.filter(t => t.verification === "pass").length;
|
|
778
|
+
const fails = verifications.filter(t => t.verification === "fail").length;
|
|
734
779
|
|
|
735
780
|
// Gap cycle data
|
|
736
|
-
const gapTraces = traces.filter(t => t.hook === "state-transition" && t.
|
|
781
|
+
const gapTraces = traces.filter(t => t.hook === "state-transition" && t.gap_closure);
|
|
737
782
|
const totalGapCycles = gapTraces.length;
|
|
738
783
|
|
|
739
784
|
// Display
|
|
@@ -779,8 +824,9 @@ function cmdErpPing() {
|
|
|
779
824
|
console.log("");
|
|
780
825
|
|
|
781
826
|
const cfg = readConfig();
|
|
827
|
+
const args = new Set(process.argv.slice(3));
|
|
782
828
|
const erpUrl = (cfg.erp && cfg.erp.url) || "https://portal.qualiasolutions.net";
|
|
783
|
-
|
|
829
|
+
let erpEnabled = !(cfg.erp && cfg.erp.enabled === false);
|
|
784
830
|
const keyFile = path.join(CLAUDE_DIR, ".erp-api-key");
|
|
785
831
|
|
|
786
832
|
console.log(` ${DIM}URL:${RESET} ${WHITE}${erpUrl}${RESET}`);
|
|
@@ -800,6 +846,19 @@ function cmdErpPing() {
|
|
|
800
846
|
console.log(` ${DIM}Key:${RESET} ${GREEN}present${RESET} ${DIM}(${apiKey.length} bytes)${RESET}`);
|
|
801
847
|
console.log("");
|
|
802
848
|
|
|
849
|
+
if (!erpEnabled && args.has("--enable")) {
|
|
850
|
+
cfg.erp = {
|
|
851
|
+
...(cfg.erp || {}),
|
|
852
|
+
enabled: true,
|
|
853
|
+
url: erpUrl,
|
|
854
|
+
api_key_file: ".erp-api-key",
|
|
855
|
+
};
|
|
856
|
+
writeConfig(cfg);
|
|
857
|
+
erpEnabled = true;
|
|
858
|
+
console.log(` ${GREEN}✓ ERP enabled in config.${RESET}`);
|
|
859
|
+
console.log("");
|
|
860
|
+
}
|
|
861
|
+
|
|
803
862
|
if (!erpEnabled) {
|
|
804
863
|
console.log(` ${YELLOW}ERP is disabled in config. Enable with:${RESET}`);
|
|
805
864
|
console.log(` ${DIM} qualia-framework erp-ping --enable${RESET}`);
|
|
@@ -872,6 +931,266 @@ function cmdErpPing() {
|
|
|
872
931
|
process.exit(1);
|
|
873
932
|
}
|
|
874
933
|
|
|
934
|
+
function cmdSetErpKey() {
|
|
935
|
+
banner();
|
|
936
|
+
console.log("");
|
|
937
|
+
|
|
938
|
+
const keyFile = path.join(CLAUDE_DIR, ".erp-api-key");
|
|
939
|
+
const rawArgs = process.argv.slice(3);
|
|
940
|
+
const clear = rawArgs.includes("--clear");
|
|
941
|
+
|
|
942
|
+
if (!fs.existsSync(CLAUDE_DIR)) fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
943
|
+
|
|
944
|
+
if (clear) {
|
|
945
|
+
try { fs.unlinkSync(keyFile); } catch {}
|
|
946
|
+
const cfg = readConfig();
|
|
947
|
+
cfg.erp = { ...(cfg.erp || {}), enabled: false, url: (cfg.erp && cfg.erp.url) || "https://portal.qualiasolutions.net", api_key_file: ".erp-api-key" };
|
|
948
|
+
writeConfig(cfg);
|
|
949
|
+
console.log(` ${GREEN}✓${RESET} ERP key removed and ERP disabled.`);
|
|
950
|
+
console.log("");
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
let key = rawArgs.find((a) => a && !a.startsWith("--")) || "";
|
|
955
|
+
if (!key && !process.stdin.isTTY) {
|
|
956
|
+
try { key = fs.readFileSync(0, "utf8").trim(); } catch {}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
key = String(key || "").trim();
|
|
960
|
+
if (!key) {
|
|
961
|
+
console.log(` ${RED}✗${RESET} Missing ERP API key.`);
|
|
962
|
+
console.log(` ${DIM}Usage:${RESET} qualia-framework set-erp-key <key>`);
|
|
963
|
+
console.log(` ${DIM}Safe shell history option:${RESET} printf '%s' "$QUALIA_ERP_KEY" | qualia-framework set-erp-key`);
|
|
964
|
+
console.log("");
|
|
965
|
+
process.exit(1);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if (key.length < 10) {
|
|
969
|
+
console.log(` ${YELLOW}!${RESET} Key looks short (${key.length} bytes). Saving anyway.`);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
fs.writeFileSync(keyFile, key, { mode: 0o600 });
|
|
973
|
+
try { fs.chmodSync(keyFile, 0o600); } catch {}
|
|
974
|
+
|
|
975
|
+
const cfg = readConfig();
|
|
976
|
+
cfg.erp = {
|
|
977
|
+
...(cfg.erp || {}),
|
|
978
|
+
enabled: true,
|
|
979
|
+
url: (cfg.erp && cfg.erp.url) || "https://portal.qualiasolutions.net",
|
|
980
|
+
api_key_file: ".erp-api-key",
|
|
981
|
+
};
|
|
982
|
+
writeConfig(cfg);
|
|
983
|
+
|
|
984
|
+
console.log(` ${GREEN}✓${RESET} ERP key saved to ${WHITE}${keyFile}${RESET}`);
|
|
985
|
+
console.log(` ${DIM}Verify with:${RESET} ${TEAL}qualia-framework erp-ping${RESET}`);
|
|
986
|
+
console.log("");
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// ─── Doctor: post-install health check ───────────────────
|
|
990
|
+
// Mirrors the spot-check that session-start.js runs once per 24h. Surfaces
|
|
991
|
+
// missing files, mis-wired hooks, stale settings.json, and version drift.
|
|
992
|
+
// Use whenever something feels off, before opening an issue, or after a
|
|
993
|
+
// version upgrade. Exits 0 if healthy, 1 if any issue is found.
|
|
994
|
+
// ─── Flush: convenience wrapper around knowledge-flush.js ───────
|
|
995
|
+
// Exposes the cron-runnable script as a top-level CLI command so users can
|
|
996
|
+
// run `qualia-framework flush` ad-hoc. All args after the command pass through.
|
|
997
|
+
function cmdFlush() {
|
|
998
|
+
const flushScript = path.join(CLAUDE_DIR, "bin", "knowledge-flush.js");
|
|
999
|
+
if (!fs.existsSync(flushScript)) {
|
|
1000
|
+
console.log(` ${RED}✗${RESET} knowledge-flush.js not installed at ${flushScript}`);
|
|
1001
|
+
console.log(` ${DIM}Run: npx qualia-framework@latest install${RESET}`);
|
|
1002
|
+
process.exit(1);
|
|
1003
|
+
}
|
|
1004
|
+
const args = process.argv.slice(3);
|
|
1005
|
+
const r = spawnSync(process.execPath, [flushScript, ...args], {
|
|
1006
|
+
stdio: "inherit",
|
|
1007
|
+
shell: false,
|
|
1008
|
+
});
|
|
1009
|
+
process.exit(typeof r.status === "number" ? r.status : 1);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function cmdDoctor() {
|
|
1013
|
+
banner();
|
|
1014
|
+
console.log("");
|
|
1015
|
+
|
|
1016
|
+
const issues = [];
|
|
1017
|
+
const checks = [];
|
|
1018
|
+
|
|
1019
|
+
function check(label, ok, hint) {
|
|
1020
|
+
checks.push({ label, ok, hint });
|
|
1021
|
+
if (!ok) issues.push({ label, hint });
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// ── Critical files (the same set session-start.js validates) ──
|
|
1025
|
+
const criticalFiles = [
|
|
1026
|
+
path.join(CLAUDE_DIR, "rules", "grounding.md"),
|
|
1027
|
+
path.join(CLAUDE_DIR, "rules", "security.md"),
|
|
1028
|
+
path.join(CLAUDE_DIR, "rules", "frontend.md"),
|
|
1029
|
+
path.join(CLAUDE_DIR, "rules", "deployment.md"),
|
|
1030
|
+
path.join(CLAUDE_DIR, "bin", "state.js"),
|
|
1031
|
+
path.join(CLAUDE_DIR, "bin", "qualia-ui.js"),
|
|
1032
|
+
path.join(CLAUDE_DIR, "bin", "statusline.js"),
|
|
1033
|
+
path.join(CLAUDE_DIR, "bin", "knowledge.js"),
|
|
1034
|
+
path.join(CLAUDE_DIR, "bin", "knowledge-flush.js"),
|
|
1035
|
+
path.join(CLAUDE_DIR, "CLAUDE.md"),
|
|
1036
|
+
CONFIG_FILE,
|
|
1037
|
+
];
|
|
1038
|
+
for (const f of criticalFiles) {
|
|
1039
|
+
check(
|
|
1040
|
+
`${path.relative(CLAUDE_DIR, f) || f}`,
|
|
1041
|
+
fs.existsSync(f),
|
|
1042
|
+
"run: npx qualia-framework@latest install",
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// ── Hooks ─────────────────────────────────────────────
|
|
1047
|
+
for (const h of QUALIA_HOOK_FILES) {
|
|
1048
|
+
check(
|
|
1049
|
+
`hooks/${h}`,
|
|
1050
|
+
fs.existsSync(path.join(CLAUDE_DIR, "hooks", h)),
|
|
1051
|
+
"reinstall: npx qualia-framework@latest install",
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// ── Knowledge layer ────────────────────────────────────
|
|
1056
|
+
const knowledgeFiles = [
|
|
1057
|
+
path.join(CLAUDE_DIR, "knowledge", "agents.md"),
|
|
1058
|
+
path.join(CLAUDE_DIR, "knowledge", "index.md"),
|
|
1059
|
+
path.join(CLAUDE_DIR, "knowledge", "daily-log"),
|
|
1060
|
+
];
|
|
1061
|
+
for (const f of knowledgeFiles) {
|
|
1062
|
+
check(
|
|
1063
|
+
`knowledge/${path.basename(f)}${fs.existsSync(f) && fs.statSync(f).isDirectory() ? "/" : ""}`,
|
|
1064
|
+
fs.existsSync(f),
|
|
1065
|
+
"reinstall to initialize the memory layer: npx qualia-framework@latest install",
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// ── settings.json hook wiring ──────────────────────────
|
|
1070
|
+
const settingsPath = path.join(CLAUDE_DIR, "settings.json");
|
|
1071
|
+
if (fs.existsSync(settingsPath)) {
|
|
1072
|
+
try {
|
|
1073
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
1074
|
+
const wantEvents = ["SessionStart", "PreToolUse", "PreCompact", "Stop"];
|
|
1075
|
+
for (const ev of wantEvents) {
|
|
1076
|
+
const blocks = (settings.hooks || {})[ev] || [];
|
|
1077
|
+
const hasQualia = blocks.some((b) =>
|
|
1078
|
+
(b.hooks || []).some((h) => typeof h.command === "string" && h.command.includes(".claude")),
|
|
1079
|
+
);
|
|
1080
|
+
check(`settings.json hooks.${ev}`, hasQualia, "reinstall to wire hooks");
|
|
1081
|
+
}
|
|
1082
|
+
} catch (e) {
|
|
1083
|
+
check("settings.json parseable", false, e.message);
|
|
1084
|
+
}
|
|
1085
|
+
} else {
|
|
1086
|
+
check("settings.json", false, "Claude Code never ran here? Open Claude once first");
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// ── Version vs. installed ──────────────────────────────
|
|
1090
|
+
const cfg = readConfig();
|
|
1091
|
+
if (cfg.installed_at) {
|
|
1092
|
+
check(`config installed_by=${cfg.installed_by || "?"} role=${cfg.role || "?"}`, true);
|
|
1093
|
+
} else {
|
|
1094
|
+
check("config has install metadata", false, "reinstall to record");
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// ── Render ────────────────────────────────────────────
|
|
1098
|
+
for (const c of checks) {
|
|
1099
|
+
const mark = c.ok ? `${GREEN}✓${RESET}` : `${RED}✗${RESET}`;
|
|
1100
|
+
console.log(` ${mark} ${c.label}`);
|
|
1101
|
+
}
|
|
1102
|
+
console.log("");
|
|
1103
|
+
if (issues.length === 0) {
|
|
1104
|
+
console.log(` ${GREEN}All checks passed. Framework is healthy.${RESET}`);
|
|
1105
|
+
console.log("");
|
|
1106
|
+
process.exit(0);
|
|
1107
|
+
}
|
|
1108
|
+
console.log(` ${RED}${issues.length} issue${issues.length === 1 ? "" : "s"} found:${RESET}`);
|
|
1109
|
+
for (const i of issues) {
|
|
1110
|
+
console.log(` ${DIM}•${RESET} ${i.label}${i.hint ? ` ${DIM}— ${i.hint}${RESET}` : ""}`);
|
|
1111
|
+
}
|
|
1112
|
+
console.log("");
|
|
1113
|
+
process.exit(1);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// ─── Agents: per-spawn telemetry (see docs/agent-runs.md) ─────────
|
|
1117
|
+
function cmdAgents() {
|
|
1118
|
+
banner();
|
|
1119
|
+
console.log("");
|
|
1120
|
+
|
|
1121
|
+
// Lazy require so the CLI works even if the lib is missing during dev.
|
|
1122
|
+
let lib;
|
|
1123
|
+
try { lib = require("./agent-runs.js"); }
|
|
1124
|
+
catch (e) {
|
|
1125
|
+
console.log(` ${RED}✗${RESET} agent-runs.js not available: ${e.message}`);
|
|
1126
|
+
console.log("");
|
|
1127
|
+
process.exit(1);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const args = process.argv.slice(3);
|
|
1131
|
+
const flags = new Set(args.filter((a) => a.startsWith("--")));
|
|
1132
|
+
const sub = args.find((a) => !a.startsWith("--"));
|
|
1133
|
+
|
|
1134
|
+
const cwd = process.cwd();
|
|
1135
|
+
|
|
1136
|
+
if (sub === "prune") {
|
|
1137
|
+
const idx = args.indexOf("--before");
|
|
1138
|
+
const before = idx >= 0 ? args[idx + 1] : null;
|
|
1139
|
+
if (!before) {
|
|
1140
|
+
console.log(` ${RED}✗${RESET} usage: qualia-framework agents prune --before YYYY-MM-DD`);
|
|
1141
|
+
console.log("");
|
|
1142
|
+
process.exit(1);
|
|
1143
|
+
}
|
|
1144
|
+
try {
|
|
1145
|
+
const r = lib.prune(cwd, before);
|
|
1146
|
+
console.log(` ${GREEN}✓${RESET} Pruned ${r.removed} run record(s), ${r.logs_removed} log file(s).`);
|
|
1147
|
+
console.log("");
|
|
1148
|
+
return;
|
|
1149
|
+
} catch (e) {
|
|
1150
|
+
console.log(` ${RED}✗${RESET} ${e.message}`);
|
|
1151
|
+
console.log("");
|
|
1152
|
+
process.exit(1);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const opts = { limit: 50 };
|
|
1157
|
+
if (flags.has("--failed")) opts.failed = true;
|
|
1158
|
+
const taskIdx = args.indexOf("--task");
|
|
1159
|
+
if (taskIdx >= 0) opts.task_id = args[taskIdx + 1];
|
|
1160
|
+
const phaseIdx = args.indexOf("--phase");
|
|
1161
|
+
if (phaseIdx >= 0) opts.phase = Number(args[phaseIdx + 1]);
|
|
1162
|
+
|
|
1163
|
+
const records = lib.read(cwd, opts);
|
|
1164
|
+
if (records.length === 0) {
|
|
1165
|
+
console.log(` ${DIM}No agent runs recorded yet${RESET} ${DIM}(or run from a project with .planning/)${RESET}`);
|
|
1166
|
+
console.log("");
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
console.log(` ${WHITE}Agent runs${RESET} ${DIM}(showing ${records.length}, newest last)${RESET}`);
|
|
1171
|
+
console.log(` ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
|
|
1172
|
+
console.log(` ${DIM}TIME AGENT PHASE TASK STATUS DURATION NOTE${RESET}`);
|
|
1173
|
+
for (const r of records) {
|
|
1174
|
+
const t = (r.finished_at || r.started_at || "").slice(11, 16);
|
|
1175
|
+
const agent = (r.agent_type || "").padEnd(11);
|
|
1176
|
+
const phase = String(r.phase ?? "-").padEnd(5);
|
|
1177
|
+
const task = (r.task_id || "-").padEnd(4);
|
|
1178
|
+
const stColor = r.status === "success" ? GREEN : r.status === "partial" ? YELLOW : RED;
|
|
1179
|
+
const status = `${stColor}${(r.status || "?").padEnd(9)}${RESET}`;
|
|
1180
|
+
const dur = r.duration_ms != null ? `${Math.round(r.duration_ms / 1000)}s`.padStart(7) : " —";
|
|
1181
|
+
const note = r.failure_reason
|
|
1182
|
+
? `${DIM}${r.failure_reason}${RESET}`
|
|
1183
|
+
: (r.commit_sha ? `${DIM}${r.commit_sha.slice(0, 7)}${RESET}` : "");
|
|
1184
|
+
console.log(` ${t} ${agent} ${phase} ${task} ${status} ${dur} ${note}`);
|
|
1185
|
+
}
|
|
1186
|
+
console.log("");
|
|
1187
|
+
const failed = records.filter((r) => r.status !== "success").length;
|
|
1188
|
+
if (failed > 0) {
|
|
1189
|
+
console.log(` ${YELLOW}${failed} non-success run(s).${RESET} ${DIM}qualia-framework agents --failed for details${RESET}`);
|
|
1190
|
+
console.log("");
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
875
1194
|
function cmdHelp() {
|
|
876
1195
|
banner();
|
|
877
1196
|
console.log("");
|
|
@@ -880,11 +1199,15 @@ function cmdHelp() {
|
|
|
880
1199
|
console.log(` qualia-framework ${TEAL}update${RESET} Update to the latest version`);
|
|
881
1200
|
console.log(` qualia-framework ${TEAL}version${RESET} Show installed version + check for updates`);
|
|
882
1201
|
console.log(` qualia-framework ${TEAL}uninstall${RESET} Clean removal from ~/.claude/ (${DIM}-y to skip prompts${RESET})`);
|
|
883
|
-
console.log(` qualia-framework ${TEAL}migrate${RESET}
|
|
1202
|
+
console.log(` qualia-framework ${TEAL}migrate${RESET} Wire current hook + env layout into ~/.claude/settings.json`);
|
|
884
1203
|
console.log(` qualia-framework ${TEAL}team${RESET} Manage team members (${DIM}list|add|remove${RESET})`);
|
|
885
1204
|
console.log(` qualia-framework ${TEAL}traces${RESET} View recent hook telemetry`);
|
|
886
1205
|
console.log(` qualia-framework ${TEAL}analytics${RESET} Show outcome scoring & gap cycle stats`);
|
|
1206
|
+
console.log(` qualia-framework ${TEAL}agents${RESET} Show per-agent run history (${DIM}--failed|--task ID|--phase N|prune --before${RESET})`);
|
|
1207
|
+
console.log(` qualia-framework ${TEAL}set-erp-key${RESET} Save/enable the ERP API key`);
|
|
887
1208
|
console.log(` qualia-framework ${TEAL}erp-ping${RESET} Verify ERP connectivity + API key`);
|
|
1209
|
+
console.log(` qualia-framework ${TEAL}doctor${RESET} Health-check the install (files, hooks, settings)`);
|
|
1210
|
+
console.log(` qualia-framework ${TEAL}flush${RESET} Promote daily-log → curated knowledge (memory layer)`);
|
|
888
1211
|
console.log("");
|
|
889
1212
|
console.log(` ${WHITE}After install:${RESET}`);
|
|
890
1213
|
console.log(` ${TG}/qualia${RESET} What should I do next?`);
|
|
@@ -933,14 +1256,30 @@ switch (cmd) {
|
|
|
933
1256
|
case "migrate":
|
|
934
1257
|
cmdMigrate();
|
|
935
1258
|
break;
|
|
1259
|
+
case "agents":
|
|
1260
|
+
cmdAgents();
|
|
1261
|
+
break;
|
|
936
1262
|
case "analytics":
|
|
937
1263
|
case "stats":
|
|
938
1264
|
cmdAnalytics();
|
|
939
1265
|
break;
|
|
1266
|
+
case "set-erp-key":
|
|
1267
|
+
case "erp-key":
|
|
1268
|
+
cmdSetErpKey();
|
|
1269
|
+
break;
|
|
940
1270
|
case "erp-ping":
|
|
941
1271
|
case "ping":
|
|
942
1272
|
cmdErpPing();
|
|
943
1273
|
break;
|
|
1274
|
+
case "doctor":
|
|
1275
|
+
case "health":
|
|
1276
|
+
case "health-check":
|
|
1277
|
+
cmdDoctor();
|
|
1278
|
+
break;
|
|
1279
|
+
case "flush":
|
|
1280
|
+
case "knowledge-flush":
|
|
1281
|
+
cmdFlush();
|
|
1282
|
+
break;
|
|
944
1283
|
default:
|
|
945
1284
|
cmdHelp();
|
|
946
1285
|
}
|
package/bin/install.js
CHANGED
|
@@ -294,8 +294,12 @@ async function main() {
|
|
|
294
294
|
const tmplDir = path.join(FRAMEWORK_DIR, "templates");
|
|
295
295
|
const tmplDest = path.join(CLAUDE_DIR, "qualia-templates");
|
|
296
296
|
if (!fs.existsSync(tmplDest)) fs.mkdirSync(tmplDest, { recursive: true });
|
|
297
|
+
// `knowledge/` is a sibling templates directory but installs to a different
|
|
298
|
+
// destination (~/.claude/knowledge/, not ~/.claude/qualia-templates/), so we
|
|
299
|
+
// skip it here and handle it in the dedicated "Knowledge layer" section below.
|
|
297
300
|
for (const entry of fs.readdirSync(tmplDir, { withFileTypes: true })) {
|
|
298
301
|
if (entry.name.startsWith(".")) continue;
|
|
302
|
+
if (entry.name === "knowledge") continue;
|
|
299
303
|
const srcPath = path.join(tmplDir, entry.name);
|
|
300
304
|
const destPath = path.join(tmplDest, entry.name);
|
|
301
305
|
try {
|
|
@@ -311,6 +315,38 @@ async function main() {
|
|
|
311
315
|
}
|
|
312
316
|
}
|
|
313
317
|
|
|
318
|
+
// ─── Knowledge layer (Karpathy-style raw → wiki memory tier) ──────
|
|
319
|
+
// Initializes ~/.claude/knowledge/ on first install. Never overwrites
|
|
320
|
+
// existing files — re-running the installer is safe for users who have
|
|
321
|
+
// already accumulated learnings.
|
|
322
|
+
printSection("Knowledge layer");
|
|
323
|
+
const knowledgeSrc = path.join(FRAMEWORK_DIR, "templates", "knowledge");
|
|
324
|
+
const knowledgeDest = path.join(CLAUDE_DIR, "knowledge");
|
|
325
|
+
if (!fs.existsSync(knowledgeDest)) fs.mkdirSync(knowledgeDest, { recursive: true });
|
|
326
|
+
const dailyLogDir = path.join(knowledgeDest, "daily-log");
|
|
327
|
+
if (!fs.existsSync(dailyLogDir)) {
|
|
328
|
+
fs.mkdirSync(dailyLogDir, { recursive: true });
|
|
329
|
+
ok("daily-log/ (created)");
|
|
330
|
+
} else {
|
|
331
|
+
log(`${DIM}daily-log/ (kept)${RESET}`);
|
|
332
|
+
}
|
|
333
|
+
if (fs.existsSync(knowledgeSrc)) {
|
|
334
|
+
for (const file of fs.readdirSync(knowledgeSrc)) {
|
|
335
|
+
const src = path.join(knowledgeSrc, file);
|
|
336
|
+
const dest = path.join(knowledgeDest, file);
|
|
337
|
+
try {
|
|
338
|
+
if (fs.existsSync(dest)) {
|
|
339
|
+
log(`${DIM}${file} (kept — user has customized)${RESET}`);
|
|
340
|
+
} else {
|
|
341
|
+
copy(src, dest);
|
|
342
|
+
ok(`${file} (initialized)`);
|
|
343
|
+
}
|
|
344
|
+
} catch (e) {
|
|
345
|
+
warn(`${file} — ${e.message}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
314
350
|
// ─── References (methodology docs loaded by skills at runtime) ────
|
|
315
351
|
printSection("References");
|
|
316
352
|
const refDir = path.join(FRAMEWORK_DIR, "references");
|
|
@@ -374,6 +410,28 @@ async function main() {
|
|
|
374
410
|
path.join(binDest, "statusline.js")
|
|
375
411
|
);
|
|
376
412
|
ok("statusline.js (status bar renderer)");
|
|
413
|
+
copy(
|
|
414
|
+
path.join(FRAMEWORK_DIR, "bin", "knowledge.js"),
|
|
415
|
+
path.join(binDest, "knowledge.js")
|
|
416
|
+
);
|
|
417
|
+
fs.chmodSync(path.join(binDest, "knowledge.js"), 0o755);
|
|
418
|
+
ok("knowledge.js (memory-layer loader)");
|
|
419
|
+
copy(
|
|
420
|
+
path.join(FRAMEWORK_DIR, "bin", "knowledge-flush.js"),
|
|
421
|
+
path.join(binDest, "knowledge-flush.js")
|
|
422
|
+
);
|
|
423
|
+
fs.chmodSync(path.join(binDest, "knowledge-flush.js"), 0o755);
|
|
424
|
+
ok("knowledge-flush.js (cron-runnable flush)");
|
|
425
|
+
copy(
|
|
426
|
+
path.join(FRAMEWORK_DIR, "bin", "plan-contract.js"),
|
|
427
|
+
path.join(binDest, "plan-contract.js")
|
|
428
|
+
);
|
|
429
|
+
ok("plan-contract.js (plan JSON validator)");
|
|
430
|
+
copy(
|
|
431
|
+
path.join(FRAMEWORK_DIR, "bin", "agent-runs.js"),
|
|
432
|
+
path.join(binDest, "agent-runs.js")
|
|
433
|
+
);
|
|
434
|
+
ok("agent-runs.js (agent telemetry writer)");
|
|
377
435
|
} catch (e) {
|
|
378
436
|
warn(`scripts — ${e.message}`);
|
|
379
437
|
}
|
|
@@ -538,8 +596,8 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
538
596
|
try { fs.chmodSync(configFile, 0o600); } catch {}
|
|
539
597
|
} catch {}
|
|
540
598
|
log(`${YELLOW}!${RESET} ERP key not configured — reports won't upload until set.`);
|
|
541
|
-
log(`${DIM} Set with:${RESET} ${TEAL}
|
|
542
|
-
log(`${DIM} or
|
|
599
|
+
log(`${DIM} Set with:${RESET} ${TEAL}qualia-framework set-erp-key <key>${RESET}`);
|
|
600
|
+
log(`${DIM} or:${RESET} ${TEAL}export QUALIA_ERP_KEY=...${RESET} ${DIM}then re-install.${RESET}`);
|
|
543
601
|
log(`${DIM} Get a key from Fawzi.${RESET}`);
|
|
544
602
|
}
|
|
545
603
|
|
|
@@ -562,6 +620,15 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
562
620
|
CLAUDE_CODE_DISABLE_AUTO_MEMORY: "0",
|
|
563
621
|
MAX_MCP_OUTPUT_TOKENS: "25000",
|
|
564
622
|
CLAUDE_CODE_NO_FLICKER: "1",
|
|
623
|
+
// v4.2.0 phase 3 — enable forked subagents (Anthropic, 2026-04).
|
|
624
|
+
// Forks inherit the full conversation history + share the prompt cache,
|
|
625
|
+
// so design fan-outs and discuss-context handoffs preserve nuance instead
|
|
626
|
+
// of compressing 50k tokens of taste discussion into a 2k subagent prompt.
|
|
627
|
+
// /qualia-design and the builder agent reach for /fork when discuss
|
|
628
|
+
// context exists in the current session; verifier and plan-checker still
|
|
629
|
+
// use blank-context spawns to avoid the "kid grading their own homework"
|
|
630
|
+
// failure mode.
|
|
631
|
+
CLAUDE_AGENT_FORK_ENABLED: "1",
|
|
565
632
|
});
|
|
566
633
|
|
|
567
634
|
// Status line
|
|
@@ -613,6 +680,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
613
680
|
const QUALIA_HOOK_SET = new Set([
|
|
614
681
|
"session-start.js", "auto-update.js", "branch-guard.js", "pre-push.js",
|
|
615
682
|
"pre-deploy-gate.js", "migration-guard.js", "pre-compact.js",
|
|
683
|
+
"git-guardrails.js", "stop-session-log.js",
|
|
616
684
|
]);
|
|
617
685
|
const isQualiaHookCmd = (cmd) => {
|
|
618
686
|
if (typeof cmd !== "string") return false;
|
|
@@ -635,6 +703,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
635
703
|
matcher: "Bash",
|
|
636
704
|
hooks: [
|
|
637
705
|
{ type: "command", command: nodeCmd("auto-update.js"), timeout: 5 },
|
|
706
|
+
{ type: "command", command: nodeCmd("git-guardrails.js"), timeout: 5, statusMessage: "⬢ Checking git safety..." },
|
|
638
707
|
{ type: "command", if: "Bash(git push*)", command: nodeCmd("branch-guard.js"), timeout: 5, statusMessage: "⬢ Checking branch permissions..." },
|
|
639
708
|
{ type: "command", if: "Bash(git push*)", command: nodeCmd("pre-push.js"), timeout: 15, statusMessage: "⬢ Syncing tracking..." },
|
|
640
709
|
{ type: "command", if: "Bash(vercel --prod*)", command: nodeCmd("pre-deploy-gate.js"), timeout: 180, statusMessage: "⬢ Running quality gates..." },
|
|
@@ -655,6 +724,14 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
655
724
|
],
|
|
656
725
|
},
|
|
657
726
|
],
|
|
727
|
+
Stop: [
|
|
728
|
+
{
|
|
729
|
+
matcher: ".*",
|
|
730
|
+
hooks: [
|
|
731
|
+
{ type: "command", command: nodeCmd("stop-session-log.js"), timeout: 5 },
|
|
732
|
+
],
|
|
733
|
+
},
|
|
734
|
+
],
|
|
658
735
|
};
|
|
659
736
|
|
|
660
737
|
// Merge user hooks: strip Qualia-owned commands, preserve everything else.
|
|
@@ -672,8 +749,8 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
672
749
|
settings.hooks[event] = [...cleaned, ...qualiaHooks[event]];
|
|
673
750
|
}
|
|
674
751
|
|
|
675
|
-
// Permissions
|
|
676
|
-
//
|
|
752
|
+
// Permissions stay permissive; Qualia policy enforcement happens in hooks so
|
|
753
|
+
// OWNER overrides and EMPLOYEE blocks can share one source of truth.
|
|
677
754
|
if (!settings.permissions) settings.permissions = {};
|
|
678
755
|
if (!settings.permissions.allow) settings.permissions.allow = [];
|
|
679
756
|
if (!settings.permissions.deny) settings.permissions.deny = [];
|
|
@@ -692,7 +769,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
692
769
|
|
|
693
770
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
694
771
|
|
|
695
|
-
ok("Hooks: session-start, auto-update, branch-guard, pre-push, migration-guard, deploy-gate, pre-compact");
|
|
772
|
+
ok("Hooks: session-start, auto-update, branch-guard, pre-push, migration-guard, deploy-gate, pre-compact, git-guardrails, stop-session-log");
|
|
696
773
|
ok("Status line + spinner configured");
|
|
697
774
|
ok("Environment variables + permissions");
|
|
698
775
|
|
|
@@ -709,7 +786,11 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
709
786
|
const ruleCount = fs.readdirSync(rulesDir).length;
|
|
710
787
|
const tmplCount = fs.readdirSync(tmplDir).length;
|
|
711
788
|
console.log(` ${DIM}Skills${RESET} ${TEAL}${skills.length}${RESET} ${DIM}Agents${RESET} ${TEAL}${agentCount}${RESET} ${DIM}Hooks${RESET} ${TEAL}${hookCount}${RESET}`);
|
|
712
|
-
|
|
789
|
+
const installedBinDir = path.join(CLAUDE_DIR, "bin");
|
|
790
|
+
const scriptCount = fs.existsSync(installedBinDir)
|
|
791
|
+
? fs.readdirSync(installedBinDir).filter(f => f.endsWith('.js')).length
|
|
792
|
+
: 0;
|
|
793
|
+
console.log(` ${DIM}Rules${RESET} ${TEAL}${ruleCount}${RESET} ${DIM}Scripts${RESET} ${TEAL}${scriptCount}${RESET} ${DIM}Templates${RESET} ${TEAL}${tmplCount}${RESET}`);
|
|
713
794
|
|
|
714
795
|
if (errors > 0) {
|
|
715
796
|
console.log("");
|