qualia-framework 6.8.1 → 6.9.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/CHANGELOG.md +22 -0
- package/bin/install.js +159 -9
- package/bin/state.js +70 -5
- package/docs/EMPLOYEE-QUICKSTART.md +162 -0
- package/package.json +2 -1
- package/skills/qualia-doctor/SKILL.md +62 -0
- package/skills/qualia-new/REFERENCE.md +7 -0
- package/skills/qualia-new/SKILL.md +42 -0
- package/skills/qualia-report/SKILL.md +13 -0
- package/templates/stacks/README.md +110 -0
- package/templates/stacks/ai-agent/env.required.json +44 -0
- package/templates/stacks/ai-agent/phases.md +53 -0
- package/templates/stacks/ai-agent/scaffold/.env.example +12 -0
- package/templates/stacks/ai-agent/scaffold/README.md +31 -0
- package/templates/stacks/ai-agent/scaffold/app/api/chat/route.ts +33 -0
- package/templates/stacks/ai-agent/scaffold/app/globals.css +13 -0
- package/templates/stacks/ai-agent/scaffold/app/layout.tsx +19 -0
- package/templates/stacks/ai-agent/scaffold/app/page.tsx +12 -0
- package/templates/stacks/ai-agent/scaffold/evals/cases.json +23 -0
- package/templates/stacks/ai-agent/scaffold/lib/openrouter/client.ts +51 -0
- package/templates/stacks/ai-agent/scaffold/lib/prompts/system.ts +6 -0
- package/templates/stacks/ai-agent/scaffold/lib/supabase/client.ts +10 -0
- package/templates/stacks/ai-agent/scaffold/lib/supabase/server.ts +28 -0
- package/templates/stacks/ai-agent/scaffold/next.config.mjs +7 -0
- package/templates/stacks/ai-agent/scaffold/package.json +29 -0
- package/templates/stacks/ai-agent/scaffold/postcss.config.mjs +7 -0
- package/templates/stacks/ai-agent/scaffold/supabase/migrations/0001_init.sql +41 -0
- package/templates/stacks/ai-agent/scaffold/tsconfig.json +21 -0
- package/templates/stacks/ai-agent/stack.json +9 -0
- package/templates/stacks/ai-agent/verify-checklist.md +31 -0
- package/templates/stacks/full-app/env.required.json +20 -0
- package/templates/stacks/full-app/phases.md +45 -0
- package/templates/stacks/full-app/scaffold/.env.example +7 -0
- package/templates/stacks/full-app/scaffold/README.md +28 -0
- package/templates/stacks/full-app/scaffold/app/globals.css +14 -0
- package/templates/stacks/full-app/scaffold/app/layout.tsx +19 -0
- package/templates/stacks/full-app/scaffold/app/page.tsx +20 -0
- package/templates/stacks/full-app/scaffold/lib/supabase/client.ts +10 -0
- package/templates/stacks/full-app/scaffold/lib/supabase/server.ts +31 -0
- package/templates/stacks/full-app/scaffold/next.config.mjs +7 -0
- package/templates/stacks/full-app/scaffold/package.json +29 -0
- package/templates/stacks/full-app/scaffold/postcss.config.mjs +7 -0
- package/templates/stacks/full-app/scaffold/supabase/migrations/0001_init.sql +27 -0
- package/templates/stacks/full-app/scaffold/tsconfig.json +21 -0
- package/templates/stacks/full-app/stack.json +9 -0
- package/templates/stacks/full-app/verify-checklist.md +32 -0
- package/templates/stacks/internal-tool/env.required.json +20 -0
- package/templates/stacks/internal-tool/phases.md +45 -0
- package/templates/stacks/internal-tool/scaffold/.env.example +7 -0
- package/templates/stacks/internal-tool/scaffold/README.md +29 -0
- package/templates/stacks/internal-tool/scaffold/app/globals.css +13 -0
- package/templates/stacks/internal-tool/scaffold/app/layout.tsx +20 -0
- package/templates/stacks/internal-tool/scaffold/app/page.tsx +22 -0
- package/templates/stacks/internal-tool/scaffold/lib/supabase/client.ts +9 -0
- package/templates/stacks/internal-tool/scaffold/lib/supabase/server.ts +28 -0
- package/templates/stacks/internal-tool/scaffold/next.config.mjs +6 -0
- package/templates/stacks/internal-tool/scaffold/package.json +29 -0
- package/templates/stacks/internal-tool/scaffold/postcss.config.mjs +7 -0
- package/templates/stacks/internal-tool/scaffold/supabase/migrations/0001_init.sql +28 -0
- package/templates/stacks/internal-tool/scaffold/tsconfig.json +21 -0
- package/templates/stacks/internal-tool/stack.json +9 -0
- package/templates/stacks/internal-tool/verify-checklist.md +31 -0
- package/templates/stacks/landing-page/env.required.json +8 -0
- package/templates/stacks/landing-page/phases.md +42 -0
- package/templates/stacks/landing-page/scaffold/.env.example +3 -0
- package/templates/stacks/landing-page/scaffold/README.md +25 -0
- package/templates/stacks/landing-page/scaffold/app/globals.css +14 -0
- package/templates/stacks/landing-page/scaffold/app/layout.tsx +19 -0
- package/templates/stacks/landing-page/scaffold/app/page.tsx +21 -0
- package/templates/stacks/landing-page/scaffold/next.config.mjs +7 -0
- package/templates/stacks/landing-page/scaffold/package.json +26 -0
- package/templates/stacks/landing-page/scaffold/postcss.config.mjs +7 -0
- package/templates/stacks/landing-page/scaffold/tsconfig.json +21 -0
- package/templates/stacks/landing-page/stack.json +9 -0
- package/templates/stacks/landing-page/verify-checklist.md +28 -0
- package/tests/bin.test.sh +3 -3
- package/tests/state.test.sh +83 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
> Note: git tags for historical versions were not retained; commit references are approximate
|
|
9
9
|
> and dates reflect commit history rather than npm publish timestamps.
|
|
10
10
|
|
|
11
|
+
## [6.9.0] - 2026-06-13 (employee on-ramps — stack presets, employee-mode install, shared knowledge pull)
|
|
12
|
+
|
|
13
|
+
Implements Part A of the 2026-06-13 restructure plan: turn the framework from an owner-only tool into something a new employee can take idea→deployed without tribal knowledge. **Additive only** — no skill, agent, hook, or rule was rewritten; these are on-ramps built *around* the existing loop.
|
|
14
|
+
|
|
15
|
+
### Added — project-type stack presets (`templates/stacks/`)
|
|
16
|
+
- **Four presets matching real Qualia work:** `landing-page` (Next.js SSG, no DB), `full-app` (Next.js + Supabase auth/RLS + Tailwind), `ai-agent` (adds OpenRouter + optional Retell/ElevenLabs/Telnyx voice), `internal-tool` (Supabase portal-style auth). Each preset ships `stack.json`, `env.required.json` (`{name, purpose, howToObtain, ownerIssued}`), `phases.md`, `verify-checklist.md`, and a minimal-but-runnable `scaffold/` (full-app/internal-tool include `lib/supabase` server+client adapters and an RLS-from-first-migration `0001_init.sql`; ai-agent adds a `lib/openrouter` adapter + `app/api/chat` route + `evals/`). `templates/stacks/README.md` documents the contract. This kills the "every project gets identical generic phases / no working skeleton" gaps.
|
|
17
|
+
|
|
18
|
+
### Changed — `/qualia-new` instantiates a preset
|
|
19
|
+
- Adds one project-type question, then lays down the chosen preset's scaffold, folds `phases.md` into the plan, and copies `env.required.json` to the new project's `.planning/env.required.json`. `--quick` skips it. Existing flow otherwise unchanged.
|
|
20
|
+
|
|
21
|
+
### Changed — `/qualia-doctor` validates the preset's env + CLI auth
|
|
22
|
+
- Reads `.planning/env.required.json` (fallback `./env.required.json`), checks each var (env or `.env.local`) and CLI logins (`vercel`, `supabase`, `gh`), and prints PASS/FAIL with the exact fix command. For `ownerIssued: true` vars it routes to "ask the OWNER (Fawzi)" instead of a self-serve command.
|
|
23
|
+
|
|
24
|
+
### Added — two-mode install (OWNER / EMPLOYEE)
|
|
25
|
+
- `npx qualia-framework install` accepts the explicit keyword `EMPLOYEE` at the install-code prompt: installs the full framework with **no team code**, `erp.enabled=false`. OWNER/team-code installs are byte-for-byte unchanged, and a genuinely bogus or empty code is still rejected (no silent self-elevation). `/qualia-report` degrades gracefully when no team code is set — it writes a **local** report file and says so, instead of failing. New `docs/EMPLOYEE-QUICKSTART.md` walks install→doctor→new→build/verify→ship→report with the credential each step needs.
|
|
26
|
+
|
|
27
|
+
### Added — shared knowledge pull (Part C bridge)
|
|
28
|
+
- On install/update the framework pulls a **read-only** copy of the `qualia-memory` vault's sanitized `wiki/_export/` (configurable via `QUALIA_KNOWLEDGE_SOURCE` / `QUALIA_KNOWLEDGE_SUBPATH`, git URL or local path) into the knowledge dir, skipping gracefully when unavailable. Freshness comes from the vault's nightly ERP-report ingest, not sync infra. The export is financial-data-sanitized at the source (see qualia-memory `scripts/export-team-wiki.py`).
|
|
29
|
+
|
|
30
|
+
### Fixed — test drift
|
|
31
|
+
- `tests/bin.test.sh` (Codex backup discipline #122/#123) now greps the `backups/` subdir, matching the v6.8.1 `bakPath()` relocation. The behavior was correct; the test path was stale.
|
|
32
|
+
|
|
11
33
|
## [6.8.1] - 2026-06-10 (installer hygiene — global hook sweep, bin purge, bak routing)
|
|
12
34
|
|
|
13
35
|
Found via a live audit of an installed `~/.claude`: the retired brain experiment's `brain-inject.js` was still wired under `UserPromptSubmit`, firing a failing node process on every user prompt, and had survived four releases because the settings merge never looked at that event.
|
package/bin/install.js
CHANGED
|
@@ -171,6 +171,95 @@ function copyTreeTransform(src, dest, transform) {
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
// ─── Shared knowledge pull (read-only mirror of qualia-memory) ───
|
|
175
|
+
// Copy-on-install/update only — NOT a sync engine. Pulls a read-only snapshot
|
|
176
|
+
// of the team's shared knowledge wiki into <home>/knowledge/shared/ so every
|
|
177
|
+
// install ships the same accumulated patterns. Source is configurable:
|
|
178
|
+
// QUALIA_KNOWLEDGE_SOURCE — a local path OR a git URL (https/ssh/file)
|
|
179
|
+
// QUALIA_KNOWLEDGE_SUBPATH — subdir inside the source to copy (default wiki/_export)
|
|
180
|
+
// Degrades GRACEFULLY: any failure (no git, no network, missing path, private-
|
|
181
|
+
// repo auth fail) warns and continues. Never throws, never exits the installer.
|
|
182
|
+
//
|
|
183
|
+
// Returns { status, detail } for the caller to log. status is one of:
|
|
184
|
+
// "copied" | "skipped" | "error" (all non-fatal).
|
|
185
|
+
const DEFAULT_KNOWLEDGE_SUBPATH = "wiki/_export";
|
|
186
|
+
|
|
187
|
+
function copyTreeReadOnly(src, dest) {
|
|
188
|
+
// Copy a tree and lock every FILE 0o444 — this mirror is read-only; local
|
|
189
|
+
// edits belong in the user's own knowledge files, not here. Directories stay
|
|
190
|
+
// writable (0o755) so the next install/update can replace the mirror cleanly
|
|
191
|
+
// (deleting an entry needs write on its parent dir, not on the entry).
|
|
192
|
+
if (!fs.existsSync(src)) return 0;
|
|
193
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
194
|
+
let count = 0;
|
|
195
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
196
|
+
if (entry.name.startsWith(".")) continue;
|
|
197
|
+
const s = path.join(src, entry.name);
|
|
198
|
+
const d = path.join(dest, entry.name);
|
|
199
|
+
if (entry.isDirectory()) {
|
|
200
|
+
count += copyTreeReadOnly(s, d);
|
|
201
|
+
} else if (entry.isFile()) {
|
|
202
|
+
fs.copyFileSync(s, d);
|
|
203
|
+
try { fs.chmodSync(d, 0o444); } catch {}
|
|
204
|
+
count++;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return count;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function pullSharedKnowledge(knowledgeDest) {
|
|
211
|
+
const source = (process.env.QUALIA_KNOWLEDGE_SOURCE || "").trim();
|
|
212
|
+
if (!source) {
|
|
213
|
+
return { status: "skipped", detail: "QUALIA_KNOWLEDGE_SOURCE not set — shared knowledge pull disabled" };
|
|
214
|
+
}
|
|
215
|
+
const subpath = (process.env.QUALIA_KNOWLEDGE_SUBPATH || DEFAULT_KNOWLEDGE_SUBPATH).trim();
|
|
216
|
+
const sharedDest = path.join(knowledgeDest, "shared");
|
|
217
|
+
const os = require("os");
|
|
218
|
+
let tmpClone = null;
|
|
219
|
+
try {
|
|
220
|
+
let sourceRoot;
|
|
221
|
+
const looksGit = /^(https?:\/\/|git@|ssh:\/\/|git:\/\/)/.test(source) || source.endsWith(".git");
|
|
222
|
+
if (looksGit) {
|
|
223
|
+
// Shallow clone into a temp dir. spawnSync with argv (no shell) so the
|
|
224
|
+
// URL can't be misinterpreted. Short timeout — never hang an install.
|
|
225
|
+
const { spawnSync } = require("child_process");
|
|
226
|
+
tmpClone = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-knowledge-"));
|
|
227
|
+
const r = spawnSync("git", ["clone", "--depth", "1", "--quiet", source, tmpClone], {
|
|
228
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
229
|
+
timeout: 30000,
|
|
230
|
+
encoding: "utf8",
|
|
231
|
+
});
|
|
232
|
+
if (r.status !== 0) {
|
|
233
|
+
const why = (r.stderr || "").trim().split("\n").pop() || `git exited ${r.status}`;
|
|
234
|
+
return { status: "skipped", detail: `clone failed (${why}) — skipped, continuing` };
|
|
235
|
+
}
|
|
236
|
+
sourceRoot = tmpClone;
|
|
237
|
+
} else {
|
|
238
|
+
// Local path source.
|
|
239
|
+
if (!fs.existsSync(source)) {
|
|
240
|
+
return { status: "skipped", detail: `source path not found: ${source} — skipped, continuing` };
|
|
241
|
+
}
|
|
242
|
+
sourceRoot = source;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const exportDir = path.join(sourceRoot, subpath);
|
|
246
|
+
if (!fs.existsSync(exportDir)) {
|
|
247
|
+
return { status: "skipped", detail: `subpath '${subpath}' absent in source — skipped, continuing` };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Replace the prior mirror wholesale (it's read-only and fully derived).
|
|
251
|
+
if (fs.existsSync(sharedDest)) {
|
|
252
|
+
try { fs.rmSync(sharedDest, { recursive: true, force: true }); } catch {}
|
|
253
|
+
}
|
|
254
|
+
const n = copyTreeReadOnly(exportDir, sharedDest);
|
|
255
|
+
return { status: "copied", detail: `${n} file(s) → knowledge/shared/ (read-only mirror)` };
|
|
256
|
+
} catch (e) {
|
|
257
|
+
return { status: "error", detail: `${e.message} — skipped, continuing` };
|
|
258
|
+
} finally {
|
|
259
|
+
if (tmpClone) { try { fs.rmSync(tmpClone, { recursive: true, force: true }); } catch {} }
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
174
263
|
function ensureCodexStatusLineConfig(existing) {
|
|
175
264
|
const statusLine = `status_line = ${JSON.stringify(CODEX_STATUS_LINE)}`;
|
|
176
265
|
const colors = "status_line_use_colors = true";
|
|
@@ -448,18 +537,42 @@ function askCode() {
|
|
|
448
537
|
printHeader();
|
|
449
538
|
const line = nextPipedLine();
|
|
450
539
|
// Echo the prompt + answer for log readability.
|
|
451
|
-
process.stdout.write(` ${WHITE}Enter install code:${RESET} ${line}\n`);
|
|
540
|
+
process.stdout.write(` ${WHITE}Enter install code (or "EMPLOYEE" for no-code install):${RESET} ${line}\n`);
|
|
452
541
|
resolve(String(line || "").trim());
|
|
453
542
|
return;
|
|
454
543
|
}
|
|
455
544
|
const rl = getRl();
|
|
456
545
|
printHeader();
|
|
457
|
-
|
|
546
|
+
console.log(` ${DIM}OWNER / team member? Enter your install code (QS-NAME-##).${RESET}`);
|
|
547
|
+
console.log(` ${DIM}New employee without a code? Type ${RESET}${TEAL}EMPLOYEE${RESET}${DIM} to install in employee mode.${RESET}`);
|
|
548
|
+
console.log("");
|
|
549
|
+
rl.question(` ${WHITE}Install code or "EMPLOYEE":${RESET} `, (answer) => {
|
|
458
550
|
resolve(String(answer || "").trim());
|
|
459
551
|
});
|
|
460
552
|
});
|
|
461
553
|
}
|
|
462
554
|
|
|
555
|
+
// ─── Employee mode (no team code) ────────────────────────
|
|
556
|
+
// A new employee may not have a QS-NAME-## code yet. Typing "EMPLOYEE" at the
|
|
557
|
+
// install-code prompt installs the full framework at the least-privilege role:
|
|
558
|
+
// feature branches only, no main pushes (enforced by branch-guard, which trusts
|
|
559
|
+
// the role bit in the 0o600 config). ERP reporting stays OFF until a real team
|
|
560
|
+
// code is set, so /qualia-report degrades to a local-only report.
|
|
561
|
+
//
|
|
562
|
+
// Defaulting to EMPLOYEE (not OWNER) on a missing/keyword code is the secure
|
|
563
|
+
// failure mode: a bogus code that is NOT the EMPLOYEE keyword is still rejected
|
|
564
|
+
// (see main()), so this can't be used to silently self-elevate.
|
|
565
|
+
const EMPLOYEE_KEYWORDS = new Set(["EMPLOYEE", "EMPLOYEE-MODE", "NO-CODE", "NOCODE"]);
|
|
566
|
+
const EMPLOYEE_MEMBER = {
|
|
567
|
+
name: "Qualia Employee",
|
|
568
|
+
role: "EMPLOYEE",
|
|
569
|
+
description: "Developer. Feature branches only. Cannot push to main or edit .env files.",
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
function isEmployeeKeyword(raw) {
|
|
573
|
+
return EMPLOYEE_KEYWORDS.has(String(raw || "").trim().toUpperCase());
|
|
574
|
+
}
|
|
575
|
+
|
|
463
576
|
// ─── Prompt for install target (Claude / Codex / Both) ──
|
|
464
577
|
// Backward-compat: a piped install with only the team code (single stdin
|
|
465
578
|
// line) closes stdin before this prompt; we silently default to "1"
|
|
@@ -525,12 +638,24 @@ async function main() {
|
|
|
525
638
|
}
|
|
526
639
|
const rawCode = await askCode();
|
|
527
640
|
const code = resolveTeamCode(rawCode);
|
|
528
|
-
|
|
641
|
+
let member = code ? TEAM[code] : null;
|
|
642
|
+
|
|
643
|
+
// Employee mode: the user typed the EMPLOYEE keyword instead of a team code.
|
|
644
|
+
// Synthesize a least-privilege EMPLOYEE member. The team code stays empty,
|
|
645
|
+
// which keeps ERP reporting off (set later via a real code or set-erp-key).
|
|
646
|
+
const employeeMode = !member && isEmployeeKeyword(rawCode);
|
|
647
|
+
if (employeeMode) {
|
|
648
|
+
member = EMPLOYEE_MEMBER;
|
|
649
|
+
}
|
|
529
650
|
|
|
530
651
|
if (!member) {
|
|
652
|
+
// A genuine bad code (not the EMPLOYEE keyword) is still rejected — typing
|
|
653
|
+
// garbage must never silently install. This preserves the install-code gate
|
|
654
|
+
// for team members while only the explicit EMPLOYEE keyword bypasses it.
|
|
531
655
|
console.log("");
|
|
532
656
|
log(`${RED}✗${RESET} Invalid code: "${rawCode}". Get your install code from Fawzi.`);
|
|
533
657
|
log(`${DIM} Tip: codes use digit zero, not letter O. Format: QS-NAME-##${RESET}`);
|
|
658
|
+
log(`${DIM} No code yet? Type ${RESET}${TEAL}EMPLOYEE${RESET}${DIM} to install in employee mode.${RESET}`);
|
|
534
659
|
console.log("");
|
|
535
660
|
process.exit(1);
|
|
536
661
|
}
|
|
@@ -539,6 +664,9 @@ async function main() {
|
|
|
539
664
|
const roleColor = member.role === "OWNER" ? TEAL : GREEN;
|
|
540
665
|
console.log(` ${GREEN}✓${RESET} ${WHITE}${BOLD}Welcome, ${member.name}${RESET}`);
|
|
541
666
|
console.log(` ${DIM} Role:${RESET} ${roleColor}${member.role}${RESET}`);
|
|
667
|
+
if (employeeMode) {
|
|
668
|
+
console.log(` ${DIM} Mode:${RESET} ${YELLOW}employee (no team code) — ERP reporting off until a code is set${RESET}`);
|
|
669
|
+
}
|
|
542
670
|
|
|
543
671
|
// ─── Ask install target (Claude / Codex / Both) ────────
|
|
544
672
|
const target = await askTarget();
|
|
@@ -552,7 +680,7 @@ async function main() {
|
|
|
552
680
|
if (!installClaude) {
|
|
553
681
|
// Codex-only path: skip the entire Claude install block. Jump straight
|
|
554
682
|
// to the Codex installer + final summary.
|
|
555
|
-
await installCodex(member, target);
|
|
683
|
+
await installCodex(member, target, employeeMode);
|
|
556
684
|
return;
|
|
557
685
|
}
|
|
558
686
|
|
|
@@ -779,6 +907,15 @@ async function main() {
|
|
|
779
907
|
}
|
|
780
908
|
}
|
|
781
909
|
|
|
910
|
+
// ─── Shared knowledge pull (read-only mirror of qualia-memory wiki) ──
|
|
911
|
+
// Copy-on-install only. Configure with QUALIA_KNOWLEDGE_SOURCE (git URL or
|
|
912
|
+
// local path). Skips gracefully when unset/unreachable — never blocks install.
|
|
913
|
+
{
|
|
914
|
+
const pull = pullSharedKnowledge(knowledgeDest);
|
|
915
|
+
if (pull.status === "copied") ok(`shared knowledge — ${pull.detail}`);
|
|
916
|
+
else log(`${DIM}shared knowledge — ${pull.detail}${RESET}`);
|
|
917
|
+
}
|
|
918
|
+
|
|
782
919
|
// ─── References (methodology docs loaded by skills at runtime) ────
|
|
783
920
|
printSection("References");
|
|
784
921
|
const refDir = path.join(FRAMEWORK_DIR, "references");
|
|
@@ -988,15 +1125,19 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
988
1125
|
}
|
|
989
1126
|
|
|
990
1127
|
// ─── Save config (for update command) ──────────────────
|
|
1128
|
+
// Employee mode (no team code): code is the empty string, and ERP reporting
|
|
1129
|
+
// is disabled by default — there's no team code to attribute reports to, so
|
|
1130
|
+
// /qualia-report degrades to a local-only report. A real team code (or
|
|
1131
|
+
// `qualia-framework set-erp-key`) flips ERP back on later.
|
|
991
1132
|
const configFile = path.join(CLAUDE_DIR, ".qualia-config.json");
|
|
992
1133
|
const config = {
|
|
993
|
-
code,
|
|
1134
|
+
code: code || "",
|
|
994
1135
|
installed_by: member.name,
|
|
995
1136
|
role: member.role,
|
|
996
1137
|
version: require("../package.json").version,
|
|
997
1138
|
installed_at: new Date().toISOString().split("T")[0],
|
|
998
1139
|
erp: {
|
|
999
|
-
enabled:
|
|
1140
|
+
enabled: !employeeMode,
|
|
1000
1141
|
url: "https://portal.qualiasolutions.net",
|
|
1001
1142
|
api_key_file: ".erp-api-key",
|
|
1002
1143
|
},
|
|
@@ -1297,7 +1438,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
1297
1438
|
|
|
1298
1439
|
// ─── Codex (optional second target) ──────────────────────
|
|
1299
1440
|
if (installCodexTarget) {
|
|
1300
|
-
await installCodex(member, target);
|
|
1441
|
+
await installCodex(member, target, employeeMode);
|
|
1301
1442
|
}
|
|
1302
1443
|
|
|
1303
1444
|
// ─── Summary ───────────────────────────────────────────
|
|
@@ -1391,7 +1532,7 @@ function printSummary({ member, target, claudeInstalled }) {
|
|
|
1391
1532
|
// Claude install. The only thing intentionally not claimed is a Claude-style
|
|
1392
1533
|
// persistent statusLine setting; Codex exposes hook status messages today, not
|
|
1393
1534
|
// an equivalent global status-line command.
|
|
1394
|
-
async function installCodex(member, target) {
|
|
1535
|
+
async function installCodex(member, target, employeeMode = false) {
|
|
1395
1536
|
console.log("");
|
|
1396
1537
|
console.log(` ${TEAL}▸${RESET} ${WHITE}${BOLD}Codex${RESET}`);
|
|
1397
1538
|
console.log(` ${DIM2}${"─".repeat(40)}${RESET}`);
|
|
@@ -1480,7 +1621,9 @@ async function installCodex(member, target) {
|
|
|
1480
1621
|
version: require("../package.json").version,
|
|
1481
1622
|
installed_at: new Date().toISOString().split("T")[0],
|
|
1482
1623
|
erp: {
|
|
1483
|
-
|
|
1624
|
+
// Employee mode (no team code) leaves ERP off so /qualia-report
|
|
1625
|
+
// degrades to a local-only report instead of failing.
|
|
1626
|
+
enabled: !employeeMode,
|
|
1484
1627
|
url: "https://portal.qualiasolutions.net",
|
|
1485
1628
|
api_key_file: ".erp-api-key",
|
|
1486
1629
|
},
|
|
@@ -1604,6 +1747,13 @@ async function installCodex(member, target) {
|
|
|
1604
1747
|
if (!fs.existsSync(out)) copyTextTransform(path.join(knowledgeSrc, file), out, codexText);
|
|
1605
1748
|
}
|
|
1606
1749
|
}
|
|
1750
|
+
// Shared knowledge mirror (read-only) — same copy-on-install semantics as
|
|
1751
|
+
// the Claude path. Skips gracefully when QUALIA_KNOWLEDGE_SOURCE is unset.
|
|
1752
|
+
{
|
|
1753
|
+
const pull = pullSharedKnowledge(knowledgeDest);
|
|
1754
|
+
if (pull.status === "copied") ok(`shared knowledge — ${pull.detail}`);
|
|
1755
|
+
else log(`${DIM}shared knowledge — ${pull.detail}${RESET}`);
|
|
1756
|
+
}
|
|
1607
1757
|
copyTextTransform(path.join(FRAMEWORK_DIR, "guide.md"), path.join(CODEX_DIR, "qualia-guide.md"), codexText);
|
|
1608
1758
|
ok("templates/ + knowledge/ + references/ + guide");
|
|
1609
1759
|
} catch (e) {
|
package/bin/state.js
CHANGED
|
@@ -729,6 +729,33 @@ function readNextMilestoneNameFromJourney(milestoneNum) {
|
|
|
729
729
|
}
|
|
730
730
|
}
|
|
731
731
|
|
|
732
|
+
// A milestone name is a "placeholder" when it's blank or the literal
|
|
733
|
+
// "Milestone N" fallback that close-milestone historically emitted when
|
|
734
|
+
// tracking.json carried no real name. Placeholders must never beat a real
|
|
735
|
+
// name from JOURNEY.md (observed in ERP reports as {"num":"9","name":"Milestone 9"}).
|
|
736
|
+
function isPlaceholderMilestoneName(name, num) {
|
|
737
|
+
const trimmed = String(name == null ? "" : name).trim();
|
|
738
|
+
if (!trimmed) return true;
|
|
739
|
+
return new RegExp(`^Milestone\\s+${num}$`, "i").test(trimmed);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Resolve the human name for milestone `num`. Preference order:
|
|
743
|
+
// 1. `candidate` (e.g. tracking.json milestone_name) when it's a real name
|
|
744
|
+
// 2. the `## Milestone N · Name` header in JOURNEY.md
|
|
745
|
+
// 3. the "Milestone N" placeholder — last resort only
|
|
746
|
+
function resolveMilestoneName(num, candidate) {
|
|
747
|
+
const trimmed = String(candidate == null ? "" : candidate).trim();
|
|
748
|
+
if (!isPlaceholderMilestoneName(trimmed, num)) return trimmed;
|
|
749
|
+
const fromJourney = readNextMilestoneNameFromJourney(num);
|
|
750
|
+
if (fromJourney) return fromJourney;
|
|
751
|
+
return `Milestone ${num}`;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Normalize a milestone name for dedupe comparisons.
|
|
755
|
+
function normalizeMilestoneName(name) {
|
|
756
|
+
return String(name == null ? "" : name).trim().toLowerCase();
|
|
757
|
+
}
|
|
758
|
+
|
|
732
759
|
function readState() {
|
|
733
760
|
try {
|
|
734
761
|
return fs.readFileSync(STATE_FILE, "utf8");
|
|
@@ -2166,7 +2193,9 @@ function cmdCloseMilestone(opts) {
|
|
|
2166
2193
|
}
|
|
2167
2194
|
ensureLifetime(t);
|
|
2168
2195
|
|
|
2169
|
-
|
|
2196
|
+
// parseInt — legacy tracking.json files carry milestone as a string ("9"),
|
|
2197
|
+
// which would corrupt `closedMilestone + 1` ("91") and break num dedupe.
|
|
2198
|
+
const closedMilestone = parseInt(t.milestone, 10) || 1;
|
|
2170
2199
|
if (
|
|
2171
2200
|
!opts.force &&
|
|
2172
2201
|
typeof t.lifetime.last_closed_milestone === "number" &&
|
|
@@ -2227,9 +2256,13 @@ function cmdCloseMilestone(opts) {
|
|
|
2227
2256
|
0,
|
|
2228
2257
|
(parseInt(t.lifetime && t.lifetime.tasks_completed) || 0) - priorMilestoneTasks
|
|
2229
2258
|
);
|
|
2259
|
+
// Resolve the REAL milestone name: non-placeholder tracking.json
|
|
2260
|
+
// milestone_name first, then the JOURNEY.md `## Milestone N · Name` header,
|
|
2261
|
+
// and only as a last resort the "Milestone N" placeholder.
|
|
2262
|
+
const milestoneName = resolveMilestoneName(closedMilestone, t.milestone_name);
|
|
2230
2263
|
const summary = {
|
|
2231
2264
|
num: closedMilestone,
|
|
2232
|
-
name:
|
|
2265
|
+
name: milestoneName,
|
|
2233
2266
|
total_phases: parseInt(t.total_phases) || s.phases.length || 0,
|
|
2234
2267
|
phases_completed: phasesCompleted,
|
|
2235
2268
|
tasks_completed: tasksCompletedThisMilestone,
|
|
@@ -2237,8 +2270,18 @@ function cmdCloseMilestone(opts) {
|
|
|
2237
2270
|
closed_at: new Date().toISOString(),
|
|
2238
2271
|
};
|
|
2239
2272
|
t.milestones = Array.isArray(t.milestones) ? t.milestones : [];
|
|
2240
|
-
// Idempotency:
|
|
2241
|
-
|
|
2273
|
+
// Idempotency + dedupe: update in place when the same milestone number is
|
|
2274
|
+
// already logged (num may be a string in legacy entries), OR when the same
|
|
2275
|
+
// real name is already logged under a different num (renumbering drift —
|
|
2276
|
+
// observed as duplicate names in ERP session_reports.milestones).
|
|
2277
|
+
let existing = t.milestones.findIndex(
|
|
2278
|
+
(m) => m && parseInt(m.num, 10) === closedMilestone
|
|
2279
|
+
);
|
|
2280
|
+
if (existing < 0 && !isPlaceholderMilestoneName(milestoneName, closedMilestone)) {
|
|
2281
|
+
existing = t.milestones.findIndex(
|
|
2282
|
+
(m) => m && normalizeMilestoneName(m.name) === normalizeMilestoneName(milestoneName)
|
|
2283
|
+
);
|
|
2284
|
+
}
|
|
2242
2285
|
if (existing >= 0) {
|
|
2243
2286
|
t.milestones[existing] = summary;
|
|
2244
2287
|
} else {
|
|
@@ -2533,13 +2576,35 @@ function cmdBackfillMilestones(opts) {
|
|
|
2533
2576
|
};
|
|
2534
2577
|
closedSummaries.push({ num: row.num, name: row.name, phases: phaseCount });
|
|
2535
2578
|
|
|
2536
|
-
|
|
2579
|
+
// Match by num first (parseInt — legacy entries store num as a string),
|
|
2580
|
+
// then by normalized real name under a different num (renumbering drift).
|
|
2581
|
+
let existing = t.milestones.findIndex(
|
|
2582
|
+
(mm) => mm && parseInt(mm.num, 10) === row.num
|
|
2583
|
+
);
|
|
2584
|
+
if (existing < 0 && !isPlaceholderMilestoneName(row.name, row.num)) {
|
|
2585
|
+
existing = t.milestones.findIndex(
|
|
2586
|
+
(mm) => mm && normalizeMilestoneName(mm.name) === normalizeMilestoneName(row.name)
|
|
2587
|
+
);
|
|
2588
|
+
}
|
|
2537
2589
|
if (existing >= 0) {
|
|
2538
2590
|
// Don't override entries that came from real /qualia-milestone close
|
|
2539
2591
|
// (they have richer data). Only overwrite previously-backfilled entries.
|
|
2540
2592
|
if (t.milestones[existing].backfilled) {
|
|
2541
2593
|
t.milestones[existing] = summary;
|
|
2542
2594
|
updated++;
|
|
2595
|
+
} else if (
|
|
2596
|
+
isPlaceholderMilestoneName(
|
|
2597
|
+
t.milestones[existing].name,
|
|
2598
|
+
parseInt(t.milestones[existing].num, 10)
|
|
2599
|
+
) ||
|
|
2600
|
+
parseInt(t.milestones[existing].num, 10) !== row.num
|
|
2601
|
+
) {
|
|
2602
|
+
// Real close entry but with a placeholder name ("Milestone 9") or a
|
|
2603
|
+
// stale num — repair the identity fields in place, keep the richer
|
|
2604
|
+
// close data (tasks_completed, shipped_url, closed_at).
|
|
2605
|
+
t.milestones[existing].num = row.num;
|
|
2606
|
+
t.milestones[existing].name = row.name;
|
|
2607
|
+
updated++;
|
|
2543
2608
|
}
|
|
2544
2609
|
} else {
|
|
2545
2610
|
t.milestones.push(summary);
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Qualia Framework — Employee Quickstart
|
|
2
|
+
|
|
3
|
+
A five-minute path from a fresh machine to a shipped, reported day of work. This is the route for a **new employee who does not have a team install code yet**. You can install and do real work in employee mode today; the OWNER (Fawzi) issues the keys that unlock ERP reporting and any direct provider integrations.
|
|
4
|
+
|
|
5
|
+
Who issues credentials: **the OWNER (Fawzi) issues every key** — team install codes, the ERP API key, OpenRouter / Supabase / Vercel / Retell / ElevenLabs / Telnyx credentials. If a step says "ask Fawzi", that is who to ask. Never share or reuse another person's key.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The path
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
install (employee mode) → /qualia-doctor → /qualia-new (pick preset) → build/verify loop → /qualia-ship → report
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. Install — employee mode (no team code needed)
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx qualia-framework@latest install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
At the prompt:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Install code or "EMPLOYEE":
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- Have a team code (`QS-NAME-##`)? Enter it — you install as that team member.
|
|
30
|
+
- **No code yet?** Type **`EMPLOYEE`**. You install at the least-privilege role: feature branches only, no pushes to `main` (enforced by the `branch-guard` hook). The full framework — skills, agents, hooks, knowledge — is installed exactly the same.
|
|
31
|
+
|
|
32
|
+
What employee mode changes vs. a coded install:
|
|
33
|
+
|
|
34
|
+
| | Coded install (team member) | Employee mode (no code) |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| Role | OWNER or EMPLOYEE per code | EMPLOYEE |
|
|
37
|
+
| Skills / agents / hooks | full | full |
|
|
38
|
+
| Push to `main` | OWNER only | blocked |
|
|
39
|
+
| ERP reporting | on (with API key) | **off** until a code/key is set |
|
|
40
|
+
| `/qualia-report` | uploads to ERP | saves a **local** report file |
|
|
41
|
+
|
|
42
|
+
**Credentials this step needs:** none. That's the point — you can start immediately.
|
|
43
|
+
|
|
44
|
+
To upgrade to a real team identity later: ask Fawzi for your `QS-NAME-##` code, then re-run `npx qualia-framework install` and enter it. That flips ERP reporting back on (you'll also need the ERP API key — see step 6).
|
|
45
|
+
|
|
46
|
+
### Optional: shared team knowledge mirror
|
|
47
|
+
|
|
48
|
+
The installer can pull a **read-only** copy of the team's shared knowledge wiki (`qualia-memory`) into `~/.claude/knowledge/shared/` so you start with the team's accumulated patterns and fixes. It is **opt-in and copy-on-install only** — not a live sync.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Local checkout of the knowledge repo:
|
|
52
|
+
QUALIA_KNOWLEDGE_SOURCE=/path/to/qualia-memory npx qualia-framework@latest install
|
|
53
|
+
|
|
54
|
+
# Or a git URL (ask Fawzi for access if the repo is private):
|
|
55
|
+
QUALIA_KNOWLEDGE_SOURCE=https://github.com/Qualiasolutions/qualia-memory.git \
|
|
56
|
+
npx qualia-framework@latest install
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- `QUALIA_KNOWLEDGE_SOURCE` — a local path **or** a git URL. Unset → the pull is skipped silently.
|
|
60
|
+
- `QUALIA_KNOWLEDGE_SUBPATH` — subdirectory inside the source to copy (default `wiki/_export`).
|
|
61
|
+
- The mirror lands at `~/.claude/knowledge/shared/` with every file marked read-only. Your own learnings still go through `/qualia-learn` into your personal knowledge files — the shared mirror is reference material, refreshed on each install/update.
|
|
62
|
+
- If the source is unreachable (no git, offline, missing path, private-repo auth fail) the installer **warns and continues** — it never blocks the install.
|
|
63
|
+
|
|
64
|
+
**Credentials this step needs:** read access to the `qualia-memory` repo if you use a private git URL — ask Fawzi.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 2. `/qualia-doctor` — confirm the install is healthy
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
/qualia-doctor
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Checks install integrity, hooks, project state, contracts, memory, and the ERP queue. In employee mode it will report ERP as disabled — that is expected and not an error. Fix anything it flags before starting real work.
|
|
75
|
+
|
|
76
|
+
**Credentials this step needs:** none.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 3. `/qualia-new` — start a project (pick a preset)
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
/qualia-new
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Runs the kickoff interview, researches the domain, and writes the planning substrate (`JOURNEY.md`, `REQUIREMENTS.md`, `ROADMAP.md`, `CONTEXT.md`). When prompted, pick the project **preset / type** that matches what you're building (e.g. landing page, full app, AI agent, internal tool). The preset seeds the milestone arc so you're not planning from a blank page.
|
|
87
|
+
|
|
88
|
+
**Credentials this step may need:**
|
|
89
|
+
- `OPENROUTER_API_KEY` — for any AI-assisted research/generation. Ask Fawzi for one.
|
|
90
|
+
- Provider keys (Supabase, Vercel, etc.) are not needed yet — they come in when you wire those services.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 4. Build / verify loop
|
|
95
|
+
|
|
96
|
+
Plan, build, and verify each phase:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
/qualia-plan # break the current phase into wave-grouped tasks
|
|
100
|
+
/qualia-build # execute the plan — builders + atomic commits
|
|
101
|
+
/qualia-verify # goal-backward check against acceptance criteria
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Iterate until the phase passes verification. Use `/qualia` at any point to ask "what's my next step?".
|
|
105
|
+
|
|
106
|
+
**Credentials this step needs:** whatever the feature touches —
|
|
107
|
+
- `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`, `SUPABASE_SERVICE_ROLE_KEY` for database work (service role key is **server-only**, never in client code).
|
|
108
|
+
- `OPENROUTER_API_KEY` for AI calls.
|
|
109
|
+
- Voice keys (`RETELL_API_KEY`, `ELEVENLABS_API_KEY`, `TELNYX_API_KEY`) for voice work.
|
|
110
|
+
|
|
111
|
+
Pull these into a project with `vercel env pull` once the project is linked. **Ask Fawzi for any key you don't have** — do not invent or hardcode keys, and never commit a `.env` file.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 5. `/qualia-ship` — deploy
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
/qualia-ship
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Runs the quality gates, commits, deploys (Vercel via CLI), and verifies. As an employee you ship through a **feature branch and review** — `branch-guard` blocks direct pushes to `main`. Deploys happen only through the CLI; GitHub auto-deploy is intentionally disabled.
|
|
122
|
+
|
|
123
|
+
**Credentials this step needs:**
|
|
124
|
+
- A Vercel login on the correct team (`vercel whoami`; `vercel link` if the project isn't linked). Ask Fawzi which Vercel team the project belongs to.
|
|
125
|
+
- Push access to the GitHub repo (org `QualiasolutionsCY` or `SakaniQualia`). Ask Fawzi to be added.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 6. Report — clock out
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
/qualia-report
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Generates your shift report, commits it to `.planning/reports/report-{date}.md`, and (when configured) uploads it to the ERP.
|
|
136
|
+
|
|
137
|
+
**In employee mode (no team code), the report degrades gracefully:** it is generated and committed **locally**, and you'll see a clear message that it was saved locally because no team code is set. Nothing fails. To start uploading reports to the ERP:
|
|
138
|
+
|
|
139
|
+
1. Get your team code (`QS-NAME-##`) from Fawzi and re-run `npx qualia-framework install` with it, **and**
|
|
140
|
+
2. Set the ERP API key (ask Fawzi for it):
|
|
141
|
+
```bash
|
|
142
|
+
printf '%s' "$QUALIA_ERP_KEY" | qualia-framework set-erp-key
|
|
143
|
+
```
|
|
144
|
+
Verify with `qualia-framework erp-ping`.
|
|
145
|
+
|
|
146
|
+
After that, `/qualia-report` uploads automatically and retries on transient ERP outages.
|
|
147
|
+
|
|
148
|
+
**Credentials this step needs:** a team code + the ERP API key — both issued by Fawzi. Until then, local reports are the expected behavior.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Quick reference
|
|
153
|
+
|
|
154
|
+
| Need | Command | Issued by |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| Install with no code | type `EMPLOYEE` at the prompt | — |
|
|
157
|
+
| Team code | re-run install, enter `QS-NAME-##` | Fawzi |
|
|
158
|
+
| ERP API key | `qualia-framework set-erp-key` (piped) | Fawzi |
|
|
159
|
+
| AI model access | `OPENROUTER_API_KEY` | Fawzi |
|
|
160
|
+
| Supabase / Vercel / Voice keys | `vercel env pull` once linked | Fawzi |
|
|
161
|
+
| Shared knowledge mirror | `QUALIA_KNOWLEDGE_SOURCE=... install` | repo access from Fawzi |
|
|
162
|
+
| "What's my next step?" | `/qualia` | — |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qualia-framework",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.9.0",
|
|
4
4
|
"description": "Claude Code and Codex workflow framework by Qualia Solutions. Plan, build, verify, ship.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"qualia-framework": "./bin/cli.js"
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"docs/release.md",
|
|
53
53
|
"docs/changelog-v6.html",
|
|
54
54
|
"docs/onboarding.html",
|
|
55
|
+
"docs/EMPLOYEE-QUICKSTART.md",
|
|
55
56
|
"CLAUDE.md",
|
|
56
57
|
"AGENTS.md",
|
|
57
58
|
"guide.md",
|