qualia-framework-v2 2.7.0 → 2.8.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 CHANGED
@@ -50,12 +50,16 @@ See `guide.md` for the full developer guide.
50
50
  - **19 skills** — slash commands from setup to handoff, plus debugging, design, review, knowledge, session management, and skill authoring
51
51
  - **4 agents** — planner, builder, verifier, qa-browser (each in fresh context)
52
52
  - **8 hooks** — session start, branch guard, pre-push tracking sync, env protection, migration guard, deploy gate, pre-compact state save, auto-update (all Node.js — cross-platform)
53
- - **3 rules** — security, frontend, deployment
53
+ - **4 rules** — security, frontend, design-reference, deployment
54
54
  - **5 templates** — tracking.json, state.md, project.md, plan.md, DESIGN.md
55
55
 
56
56
  ## Supported Platforms
57
57
 
58
- Works on **Windows 10/11, macOS, and Linux** (all distros). The only requirement is Node.js 18+. No Git Bash, no WSL, no bash dependency — every hook is pure Node.js.
58
+ Works on **Windows 10/11, macOS, and Linux**. Requires Node.js 18+ and Claude Code.
59
+
60
+ - Every hook and the status line are pure Node.js — no external bash, jq, or GNU coreutils required.
61
+ - Skills are executed by Claude Code's own Bash tool (which Claude Code provides on all platforms, including Windows).
62
+ - Tested on Fedora, EndeavourOS, macOS, and Windows 10/11.
59
63
 
60
64
  ## Why It Works
61
65
 
@@ -69,7 +73,7 @@ Splitting planner, builder, and verifier into separate agents with separate cont
69
73
 
70
74
  ### Production-Grade Hooks
71
75
 
72
- The `settings.json` hooks are real ops engineering, not theoretical:
76
+ All 8 hooks are real ops engineering, not theoretical. Highlights:
73
77
 
74
78
  - **Pre-deploy gate** — TypeScript, lint, tests, build, and `service_role` leak scan before `vercel --prod`
75
79
  - **Branch guard** — Role-aware: owner can push to main, employees can't
@@ -100,7 +104,7 @@ npx qualia-framework-v2 install
100
104
  ├── agents/ planner.md, builder.md, verifier.md, qa-browser.md
101
105
  ├── hooks/ 8 Node.js hooks — cross-platform (no bash dependency)
102
106
  ├── bin/ state.js (state machine) + qualia-ui.js (cosmetics library)
103
- ├── knowledge/ learned-patterns.md, common-fixes.md, client-prefs.md (auto-loaded by skills)
107
+ ├── knowledge/ learned-patterns.md, common-fixes.md, client-prefs.md (loaded by plan/debug/new)
104
108
  ├── rules/ security.md, frontend.md, deployment.md
105
109
  ├── qualia-templates/ tracking.json, state.md, project.md, plan.md, DESIGN.md
106
110
  ├── CLAUDE.md global instructions (role-configured per team member)
package/bin/cli.js CHANGED
@@ -94,13 +94,21 @@ function cmdUpdate() {
94
94
  console.log("");
95
95
 
96
96
  try {
97
- // Pull latest and reinstall with saved code
98
- execSync(
99
- `npx qualia-framework-v2@latest install <<< "${cfg.code}"`,
100
- { stdio: "inherit", shell: true, timeout: 60000 }
101
- );
97
+ const { spawnSync } = require("child_process");
98
+ const r = spawnSync("npx", ["qualia-framework-v2@latest", "install"], {
99
+ input: cfg.code + "\n",
100
+ stdio: ["pipe", "inherit", "inherit"],
101
+ shell: process.platform === "win32", // npx is a .cmd shim on Windows — must go through shell
102
+ timeout: 120000,
103
+ encoding: "utf8",
104
+ });
105
+ if (r.status !== 0) {
106
+ console.log(` ${RED}✗${RESET} Update failed. Run manually: npx qualia-framework-v2@latest install`);
107
+ process.exit(1);
108
+ }
102
109
  } catch (e) {
103
- console.log(` ${RED}✗${RESET} Update failed. Run manually: npx qualia-framework-v2@latest install`);
110
+ console.log(` ${RED}✗${RESET} Update failed: ${e.message}`);
111
+ console.log(` ${DIM}Run manually:${RESET} npx qualia-framework-v2@latest install`);
104
112
  process.exit(1);
105
113
  }
106
114
  }
package/bin/install.js CHANGED
@@ -170,12 +170,11 @@ async function main() {
170
170
  // ─── Status line ───────────────────────────────────────
171
171
  log(`${WHITE}Status line${RESET}`);
172
172
  try {
173
- const slDest = path.join(CLAUDE_DIR, "statusline.sh");
174
- copy(path.join(FRAMEWORK_DIR, "statusline.sh"), slDest);
175
- fs.chmodSync(slDest, 0o755);
176
- ok("statusline.sh");
173
+ const slDest = path.join(CLAUDE_DIR, "bin", "statusline.js");
174
+ copy(path.join(FRAMEWORK_DIR, "bin", "statusline.js"), slDest);
175
+ ok("statusline.js");
177
176
  } catch (e) {
178
- warn(`statusline.sh — ${e.message}`);
177
+ warn(`statusline.js — ${e.message}`);
179
178
  }
180
179
 
181
180
  // ─── Templates ─────────────────────────────────────────
@@ -224,6 +223,11 @@ async function main() {
224
223
  );
225
224
  fs.chmodSync(path.join(binDest, "qualia-ui.js"), 0o755);
226
225
  ok("qualia-ui.js (cosmetics library)");
226
+ copy(
227
+ path.join(FRAMEWORK_DIR, "bin", "statusline.js"),
228
+ path.join(binDest, "statusline.js")
229
+ );
230
+ ok("statusline.js (status bar renderer)");
227
231
  } catch (e) {
228
232
  warn(`scripts — ${e.message}`);
229
233
  }
@@ -303,7 +307,7 @@ async function main() {
303
307
  // Status line
304
308
  settings.statusLine = {
305
309
  type: "command",
306
- command: "~/.claude/statusline.sh",
310
+ command: `node "${path.join(CLAUDE_DIR, "bin", "statusline.js")}"`,
307
311
  };
308
312
 
309
313
  // Spinner
@@ -424,18 +428,6 @@ async function main() {
424
428
  ],
425
429
  },
426
430
  ],
427
- SubagentStart: [
428
- {
429
- matcher: ".*",
430
- hooks: [
431
- {
432
- type: "command",
433
- command:
434
- 'echo \'{"additionalContext": "◆ Qualia agent spawned"}\'',
435
- },
436
- ],
437
- },
438
- ],
439
431
  };
440
432
 
441
433
  // Permissions
@@ -449,8 +441,6 @@ async function main() {
449
441
  ];
450
442
  }
451
443
 
452
- settings.effortLevel = "high";
453
-
454
444
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
455
445
 
456
446
  ok("Hooks: session-start, auto-update, branch-guard, pre-push, env-block, migration-guard, deploy-gate, pre-compact");
@@ -467,7 +457,7 @@ async function main() {
467
457
  console.log(` Agents: ${WHITE}${agentCount}${RESET} ${DIM}(planner, builder, verifier, qa-browser)${RESET}`);
468
458
  console.log(` Hooks: ${WHITE}8${RESET} ${DIM}(session-start, auto-update, branch-guard, pre-push, env-block, migration-guard, deploy-gate, pre-compact)${RESET}`);
469
459
  console.log(` Rules: ${WHITE}${fs.readdirSync(rulesDir).length}${RESET} ${DIM}(security, frontend, design-reference, deployment)${RESET}`);
470
- console.log(` Scripts: ${WHITE}2${RESET} ${DIM}(state.js, qualia-ui.js)${RESET}`);
460
+ console.log(` Scripts: ${WHITE}3${RESET} ${DIM}(state.js, qualia-ui.js, statusline.js)${RESET}`);
471
461
  console.log(` Knowledge: ${WHITE}3${RESET} ${DIM}(patterns, fixes, client prefs)${RESET}`);
472
462
  console.log(` Templates: ${WHITE}${fs.readdirSync(tmplDir).length}${RESET}`);
473
463
  console.log(` Status line: ${GREEN}✓${RESET}`);
package/bin/qualia-ui.js CHANGED
@@ -20,7 +20,7 @@
20
20
  const fs = require("fs");
21
21
  const path = require("path");
22
22
  const os = require("os");
23
- const { execSync } = require("child_process");
23
+ const { spawnSync } = require("child_process");
24
24
 
25
25
  // ─── Colors ──────────────────────────────────────────────
26
26
  const TEAL = "\x1b[38;2;0;206;209m";
@@ -64,11 +64,14 @@ const ACTIONS = {
64
64
  // ─── State Reading ───────────────────────────────────────
65
65
  function readState() {
66
66
  try {
67
- const out = execSync(`node ${path.join(os.homedir(), ".claude", "bin", "state.js")} check 2>/dev/null`, {
67
+ const statePath = path.join(os.homedir(), ".claude", "bin", "state.js");
68
+ const r = spawnSync(process.execPath, [statePath, "check"], {
68
69
  encoding: "utf8",
69
70
  timeout: 3000,
71
+ stdio: ["ignore", "pipe", "ignore"],
70
72
  });
71
- return JSON.parse(out);
73
+ if (r.status !== 0 || !r.stdout) return null;
74
+ return JSON.parse(r.stdout);
72
75
  } catch {
73
76
  return null;
74
77
  }
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+ // Qualia status line — teal branded, shows phase + context + git.
3
+ // Pure Node.js port of the original statusline.sh. Cross-platform
4
+ // (Windows/macOS/Linux). No jq, no GNU stat, no /tmp hardcoding.
5
+ //
6
+ // Reads JSON from stdin (Claude Code status line schema), prints
7
+ // two ANSI-formatted lines to stdout. Never throws — every section
8
+ // is wrapped so missing data degrades gracefully.
9
+
10
+ const fs = require("fs");
11
+ const os = require("os");
12
+ const path = require("path");
13
+ const { spawnSync } = require("child_process");
14
+
15
+ // ─── Colors (matches bin/qualia-ui.js palette) ───────────
16
+ const TEAL = "\x1b[38;2;0;206;209m";
17
+ const TEAL_GLOW = "\x1b[38;2;0;170;175m";
18
+ const TEAL_DIM = "\x1b[38;2;0;130;135m";
19
+ const WHITE = "\x1b[38;2;220;225;230m";
20
+ const DIM = "\x1b[38;2;80;90;100m";
21
+ const GREEN = "\x1b[38;2;52;211;153m";
22
+ const YELLOW = "\x1b[38;2;234;179;8m";
23
+ const RED = "\x1b[38;2;239;68;68m";
24
+ const RESET = "\x1b[0m";
25
+
26
+ // ─── Read input ──────────────────────────────────────────
27
+ function readInput() {
28
+ try {
29
+ const raw = fs.readFileSync(0, "utf8");
30
+ return JSON.parse(raw);
31
+ } catch {
32
+ return {};
33
+ }
34
+ }
35
+
36
+ const input = readInput();
37
+
38
+ function pick(obj, keypath, fallback) {
39
+ try {
40
+ let cur = obj;
41
+ for (const k of keypath.split(".")) {
42
+ if (cur == null) return fallback;
43
+ cur = cur[k];
44
+ }
45
+ return cur == null ? fallback : cur;
46
+ } catch {
47
+ return fallback;
48
+ }
49
+ }
50
+
51
+ const MODEL = String(pick(input, "model.display_name", ""));
52
+ const DIR = String(pick(input, "workspace.current_dir", process.cwd()));
53
+ const PCT_RAW = Number(pick(input, "context_window.used_percentage", 0)) || 0;
54
+ const PCT = Math.floor(PCT_RAW);
55
+ const COST = Number(pick(input, "cost.total_cost_usd", 0)) || 0;
56
+ const DURATION_MS = Number(pick(input, "cost.total_duration_ms", 0)) || 0;
57
+ const AGENT = String(pick(input, "agent.name", "") || "");
58
+ const WORKTREE = String(pick(input, "worktree.name", "") || "");
59
+
60
+ // ─── Context bar ─────────────────────────────────────────
61
+ let BAR = "";
62
+ let BAR_COLOR = TEAL;
63
+ try {
64
+ if (PCT >= 80) BAR_COLOR = RED;
65
+ else if (PCT >= 50) BAR_COLOR = YELLOW;
66
+ else BAR_COLOR = TEAL;
67
+
68
+ const BAR_WIDTH = 10;
69
+ const filled = Math.max(0, Math.min(BAR_WIDTH, Math.floor((PCT * BAR_WIDTH) / 100)));
70
+ const empty = BAR_WIDTH - filled;
71
+ BAR = "━".repeat(filled) + "╌".repeat(empty);
72
+ } catch {
73
+ BAR = "╌".repeat(10);
74
+ }
75
+
76
+ // ─── Git branch (cached, cross-platform) ─────────────────
77
+ let BRANCH = "";
78
+ let CHANGES = 0;
79
+ try {
80
+ const username = (os.userInfo().username || "anon").replace(/[^a-zA-Z0-9_-]/g, "_");
81
+ const cacheFile = path.join(os.tmpdir(), `qualia-git-cache-${username}`);
82
+
83
+ let fresh = false;
84
+ try {
85
+ const st = fs.statSync(cacheFile);
86
+ if (Date.now() - st.mtimeMs <= 3000) fresh = true;
87
+ } catch {
88
+ fresh = false;
89
+ }
90
+
91
+ if (!fresh) {
92
+ let branch = "";
93
+ let changes = 0;
94
+ try {
95
+ const dirCheck = spawnSync("git", ["rev-parse", "--git-dir"], {
96
+ cwd: DIR,
97
+ encoding: "utf8",
98
+ timeout: 1000,
99
+ stdio: ["ignore", "pipe", "ignore"],
100
+ });
101
+ if (dirCheck.status === 0) {
102
+ const br = spawnSync("git", ["branch", "--show-current"], {
103
+ cwd: DIR,
104
+ encoding: "utf8",
105
+ timeout: 1000,
106
+ stdio: ["ignore", "pipe", "ignore"],
107
+ });
108
+ if (br.status === 0) branch = (br.stdout || "").trim();
109
+
110
+ const st = spawnSync("git", ["status", "--porcelain"], {
111
+ cwd: DIR,
112
+ encoding: "utf8",
113
+ timeout: 1000,
114
+ stdio: ["ignore", "pipe", "ignore"],
115
+ });
116
+ if (st.status === 0) {
117
+ const out = (st.stdout || "").trim();
118
+ changes = out ? out.split("\n").length : 0;
119
+ }
120
+ }
121
+ } catch {}
122
+ try {
123
+ fs.writeFileSync(cacheFile, `${branch}|${changes}`);
124
+ } catch {}
125
+ }
126
+
127
+ try {
128
+ const cached = fs.readFileSync(cacheFile, "utf8");
129
+ const [b, c] = cached.split("|");
130
+ BRANCH = b || "";
131
+ CHANGES = parseInt(c, 10) || 0;
132
+ } catch {}
133
+ } catch {}
134
+
135
+ // ─── Phase info from .planning/tracking.json ─────────────
136
+ let PHASE_INFO = "";
137
+ try {
138
+ const trackingPath = path.join(DIR, ".planning", "tracking.json");
139
+ if (fs.existsSync(trackingPath)) {
140
+ const tracking = JSON.parse(fs.readFileSync(trackingPath, "utf8"));
141
+ const phase = Number(tracking.phase || 0) || 0;
142
+ const total = Number(tracking.total_phases || 0) || 0;
143
+ const status = String(tracking.status || "");
144
+ if (total > 0) {
145
+ const pdone = Math.floor((phase * 100) / total);
146
+ const pfill = Math.max(0, Math.min(4, Math.floor(pdone / 25)));
147
+ const pempt = 4 - pfill;
148
+ const pbar = "●".repeat(pfill) + "○".repeat(pempt);
149
+ PHASE_INFO = `${TEAL}${pbar}${RESET} ${WHITE}P${phase}/${total}${RESET} ${TEAL_GLOW}${status}${RESET}`;
150
+ }
151
+ }
152
+ } catch {}
153
+
154
+ // ─── Duration ────────────────────────────────────────────
155
+ let DUR = "0s";
156
+ try {
157
+ if (DURATION_MS >= 60000) {
158
+ DUR = `${Math.floor(DURATION_MS / 60000)}m`;
159
+ } else {
160
+ DUR = `${Math.floor(DURATION_MS / 1000)}s`;
161
+ }
162
+ } catch {}
163
+
164
+ // ─── Cost ────────────────────────────────────────────────
165
+ let COST_FMT = "$0.00";
166
+ try {
167
+ COST_FMT = `$${COST.toFixed(2)}`;
168
+ } catch {}
169
+
170
+ // ─── Line 1: Project + Git + Agent + Worktree + Phase ────
171
+ let LINE1 = "";
172
+ try {
173
+ const dirBase = path.basename(DIR) || DIR;
174
+ LINE1 = `${TEAL}◆${RESET} ${WHITE}${dirBase}${RESET}`;
175
+ if (BRANCH) {
176
+ if (CHANGES > 0) {
177
+ LINE1 += ` ${DIM}on${RESET} ${TEAL_GLOW}${BRANCH}${RESET} ${YELLOW}~${CHANGES}${RESET}`;
178
+ } else {
179
+ LINE1 += ` ${DIM}on${RESET} ${TEAL_GLOW}${BRANCH}${RESET}`;
180
+ }
181
+ }
182
+ if (AGENT) LINE1 += ` ${DIM}│${RESET} ${TEAL}⚡${AGENT}${RESET}`;
183
+ if (WORKTREE) LINE1 += ` ${DIM}│${RESET} ${TEAL_DIM}⎇ ${WORKTREE}${RESET}`;
184
+ if (PHASE_INFO) LINE1 += ` ${DIM}│${RESET} ${PHASE_INFO}`;
185
+ } catch {
186
+ LINE1 = `${TEAL}◆${RESET} ${WHITE}qualia${RESET}`;
187
+ }
188
+
189
+ // ─── Line 2: Context bar + Cost + Duration + Model ───────
190
+ let LINE2 = "";
191
+ try {
192
+ LINE2 =
193
+ `${BAR_COLOR}${BAR}${RESET} ${DIM}${PCT}%${RESET} ` +
194
+ `${DIM}│${RESET} ${DIM}${COST_FMT}${RESET} ` +
195
+ `${DIM}│${RESET} ${DIM}${DUR}${RESET} ` +
196
+ `${DIM}│${RESET} ${TEAL_DIM}${MODEL}${RESET}`;
197
+ } catch {
198
+ LINE2 = `${DIM}${PCT}%${RESET}`;
199
+ }
200
+
201
+ process.stdout.write(LINE1 + "\n");
202
+ process.stdout.write(LINE2 + "\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qualia-framework-v2",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "description": "Claude Code workflow framework by Qualia Solutions. Plan, build, verify, ship.",
5
5
  "bin": {
6
6
  "qualia-framework-v2": "./bin/cli.js"
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "homepage": "https://github.com/qualia-solutions/qualia-framework-v2#readme",
25
25
  "scripts": {
26
- "test": "bash tests/hooks.test.sh"
26
+ "test": "bash tests/hooks.test.sh && bash tests/state.test.sh"
27
27
  },
28
28
  "files": [
29
29
  "bin/",
@@ -34,8 +34,7 @@
34
34
  "templates/",
35
35
  "tests/",
36
36
  "CLAUDE.md",
37
- "guide.md",
38
- "statusline.sh"
37
+ "guide.md"
39
38
  ],
40
39
  "engines": {
41
40
  "node": ">=18"
@@ -29,9 +29,27 @@ options:
29
29
  description: "This is a subagent role, not a slash command. Creates agents/{name}.md."
30
30
  ```
31
31
 
32
- **Framework** target: `/home/qualia/Projects/qualia/qualia-framework-v2/skills/{name}/SKILL.md`
32
+ ### 1a. Resolve framework directory
33
+
34
+ If the user chose **Framework skill** or a framework-scoped **Agent**, resolve `${FRAMEWORK_DIR}` — the checkout path of this user's qualia-framework-v2 repo — BEFORE computing any target paths. Never hardcode `/home/<user>/...`; different teammates and operating systems have different paths.
35
+
36
+ ```bash
37
+ # Priority order: env var → git detection → ask user
38
+ FRAMEWORK_DIR="${QUALIA_FRAMEWORK_DIR:-}"
39
+ if [ -z "$FRAMEWORK_DIR" ] && git -C . rev-parse --show-toplevel >/dev/null 2>&1; then
40
+ ORIGIN=$(git -C . config --get remote.origin.url 2>/dev/null)
41
+ case "$ORIGIN" in
42
+ *qualia-framework-v2*) FRAMEWORK_DIR=$(git -C . rev-parse --show-toplevel) ;;
43
+ esac
44
+ fi
45
+ echo "${FRAMEWORK_DIR:-UNRESOLVED}"
46
+ ```
47
+
48
+ If the command prints `UNRESOLVED`, ask the user: *"Where is your qualia-framework-v2 checkout? (absolute path, or type 'local' to save only to ~/.claude/)"*. If they type `local`, downgrade the scope to Local. Otherwise store the answer as `${FRAMEWORK_DIR}` for the rest of the session.
49
+
50
+ **Framework** → target: `${FRAMEWORK_DIR}/skills/{name}/SKILL.md`
33
51
  **Local** → target: `~/.claude/skills/{name}/SKILL.md`
34
- **Agent** → target: `/home/qualia/Projects/qualia/qualia-framework-v2/agents/{name}.md` (framework) or `~/.claude/agents/{name}.md` (local)
52
+ **Agent** → target: `${FRAMEWORK_DIR}/agents/{name}.md` (framework) or `~/.claude/agents/{name}.md` (local)
35
53
 
36
54
  ### 2. Gather Requirements
37
55
 
@@ -119,10 +137,11 @@ Fix any ambiguity the test agent found.
119
137
 
120
138
  ```bash
121
139
  # Framework skill — copy to local .claude for immediate testing
122
- cp /home/qualia/Projects/qualia/qualia-framework-v2/skills/{name}/SKILL.md ~/.claude/skills/{name}/SKILL.md
140
+ mkdir -p ~/.claude/skills/{name}
141
+ cp "${FRAMEWORK_DIR}/skills/{name}/SKILL.md" ~/.claude/skills/{name}/SKILL.md
123
142
 
124
143
  # Verify it parses
125
- node -e "const fs=require('fs');const c=fs.readFileSync('/home/qualia/.claude/skills/{name}/SKILL.md','utf8');if(!c.includes('---'))throw new Error('missing frontmatter');if(!c.match(/^name:\s*\S/m))throw new Error('missing name');if(!c.match(/^description:\s*\S/m))throw new Error('missing description');console.log('OK')"
144
+ node -e "const fs=require('fs');const os=require('os');const path=require('path');const c=fs.readFileSync(path.join(os.homedir(),'.claude/skills/{name}/SKILL.md'),'utf8');if(!c.includes('---'))throw new Error('missing frontmatter');if(!c.match(/^name:\s*\S/m))throw new Error('missing name');if(!c.match(/^description:\s*\S/m))throw new Error('missing description');console.log('OK')"
126
145
  ```
127
146
 
128
147
  ### 7. Commit (framework skills only)
@@ -131,7 +150,7 @@ Do NOT commit unless the user explicitly says "commit" or "ship it".
131
150
 
132
151
  When they do:
133
152
  ```bash
134
- cd /home/qualia/Projects/qualia/qualia-framework-v2
153
+ cd "${FRAMEWORK_DIR}"
135
154
  git add skills/{name}/
136
155
  git commit -m "feat: add /{name} skill"
137
156
  ```
@@ -0,0 +1,398 @@
1
+ #!/bin/bash
2
+ # Qualia Framework v2 — state.js behavioral tests
3
+ # Run: bash tests/state.test.sh
4
+
5
+ PASS=0
6
+ FAIL=0
7
+ # Resolve STATE_JS to an ABSOLUTE path so `cd` inside subshells doesn't break it.
8
+ STATE_JS="$(cd "$(dirname "$0")/../bin" && pwd)/state.js"
9
+ NODE="${NODE:-node}"
10
+
11
+ # Track tmp dirs we create so we can clean them up on exit
12
+ TMP_DIRS=()
13
+ cleanup() {
14
+ for d in "${TMP_DIRS[@]}"; do
15
+ [ -d "$d" ] && rm -rf "$d"
16
+ done
17
+ }
18
+ trap cleanup EXIT
19
+
20
+ # Make a fresh temp project with 2 phases, already initialized.
21
+ # Prints the absolute path to the new tmp dir (does NOT cd).
22
+ make_project() {
23
+ local TMP
24
+ TMP=$(mktemp -d)
25
+ TMP_DIRS+=("$TMP")
26
+ (
27
+ cd "$TMP" || exit 1
28
+ $NODE "$STATE_JS" init \
29
+ --project "TestProject" \
30
+ --phases '[{"name":"Foundation","goal":"Auth"},{"name":"Core","goal":"Features"}]' \
31
+ >/dev/null 2>&1
32
+ )
33
+ echo "$TMP"
34
+ }
35
+
36
+ # pass "name" — record a passing assertion
37
+ pass() {
38
+ echo " ✓ $1"
39
+ PASS=$((PASS + 1))
40
+ }
41
+
42
+ # fail "name" "detail"
43
+ fail_case() {
44
+ echo " ✗ $1${2:+ — $2}"
45
+ FAIL=$((FAIL + 1))
46
+ }
47
+
48
+ echo "=== state.js Behavioral Tests ==="
49
+ echo ""
50
+
51
+ # Sanity check
52
+ if [ ! -f "$STATE_JS" ]; then
53
+ echo "FATAL: state.js not found at $STATE_JS"
54
+ exit 1
55
+ fi
56
+
57
+ # ─── Basic I/O ───────────────────────────────────────────
58
+ echo "basic I/O:"
59
+
60
+ # 1. cmdInit produces valid tracking.json + STATE.md
61
+ TMP=$(mktemp -d); TMP_DIRS+=("$TMP")
62
+ (
63
+ cd "$TMP" || exit 1
64
+ $NODE "$STATE_JS" init \
65
+ --project "TestProject" \
66
+ --phases '[{"name":"Foundation","goal":"Auth"},{"name":"Core","goal":"Features"}]' \
67
+ >/tmp/qualia-state-test.out 2>&1
68
+ )
69
+ INIT_EXIT=$?
70
+ if [ "$INIT_EXIT" -eq 0 ] \
71
+ && [ -f "$TMP/.planning/tracking.json" ] \
72
+ && [ -f "$TMP/.planning/STATE.md" ] \
73
+ && grep -q '"ok": true' /tmp/qualia-state-test.out \
74
+ && grep -q '"action": "init"' /tmp/qualia-state-test.out; then
75
+ pass "cmdInit creates tracking.json + STATE.md"
76
+ else
77
+ fail_case "cmdInit creates tracking.json + STATE.md" "exit=$INIT_EXIT"
78
+ fi
79
+
80
+ # tracking.json content sanity
81
+ if grep -q '"project": "TestProject"' "$TMP/.planning/tracking.json" \
82
+ && grep -q '"total_phases": 2' "$TMP/.planning/tracking.json" \
83
+ && grep -q '"phase": 1' "$TMP/.planning/tracking.json" \
84
+ && grep -q '"status": "setup"' "$TMP/.planning/tracking.json"; then
85
+ pass "cmdInit tracking.json has correct fields"
86
+ else
87
+ fail_case "cmdInit tracking.json fields"
88
+ fi
89
+
90
+ # STATE.md content sanity
91
+ if grep -q 'Phase: 1 of 2 — Foundation' "$TMP/.planning/STATE.md" \
92
+ && grep -q 'Status: setup' "$TMP/.planning/STATE.md"; then
93
+ pass "cmdInit STATE.md has correct header"
94
+ else
95
+ fail_case "cmdInit STATE.md header"
96
+ fi
97
+
98
+ # 2. cmdCheck reads back init state
99
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" check 2>&1)
100
+ CHECK_EXIT=$?
101
+ if [ "$CHECK_EXIT" -eq 0 ] \
102
+ && echo "$OUT" | grep -q '"ok": true' \
103
+ && echo "$OUT" | grep -q '"phase": 1' \
104
+ && echo "$OUT" | grep -q '"status": "setup"' \
105
+ && echo "$OUT" | grep -q '"total_phases": 2'; then
106
+ pass "cmdCheck returns phase=1 status=setup total_phases=2"
107
+ else
108
+ fail_case "cmdCheck returns init state" "exit=$CHECK_EXIT"
109
+ fi
110
+
111
+ # 3. cmdCheck with no project → ok:false NO_PROJECT, exit 1
112
+ TMP2=$(mktemp -d); TMP_DIRS+=("$TMP2")
113
+ OUT=$(cd "$TMP2" && $NODE "$STATE_JS" check 2>&1)
114
+ CHECK_EXIT=$?
115
+ if [ "$CHECK_EXIT" -eq 1 ] \
116
+ && echo "$OUT" | grep -q '"ok": false' \
117
+ && echo "$OUT" | grep -q '"error": "NO_PROJECT"'; then
118
+ pass "cmdCheck without .planning → NO_PROJECT, exit 1"
119
+ else
120
+ fail_case "cmdCheck NO_PROJECT" "exit=$CHECK_EXIT"
121
+ fi
122
+
123
+ # ─── Happy path transitions ──────────────────────────────
124
+ echo ""
125
+ echo "happy path transitions:"
126
+
127
+ # 4. setup → planned (with plan file)
128
+ TMP=$(make_project)
129
+ touch "$TMP/.planning/phase-1-plan.md"
130
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1)
131
+ EXIT=$?
132
+ if [ "$EXIT" -eq 0 ] \
133
+ && echo "$OUT" | grep -q '"ok": true' \
134
+ && echo "$OUT" | grep -q '"status": "planned"' \
135
+ && echo "$OUT" | grep -q '"previous_status": "setup"'; then
136
+ pass "setup → planned succeeds with plan file"
137
+ else
138
+ fail_case "setup → planned" "exit=$EXIT out=$OUT"
139
+ fi
140
+
141
+ # 5. planned → built (records tasks_done/tasks_total)
142
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 5 --tasks-total 5 2>&1)
143
+ EXIT=$?
144
+ if [ "$EXIT" -eq 0 ] \
145
+ && echo "$OUT" | grep -q '"ok": true' \
146
+ && echo "$OUT" | grep -q '"status": "built"' \
147
+ && grep -q '"tasks_done": 5' "$TMP/.planning/tracking.json" \
148
+ && grep -q '"tasks_total": 5' "$TMP/.planning/tracking.json"; then
149
+ pass "planned → built records tasks_done/tasks_total"
150
+ else
151
+ fail_case "planned → built" "exit=$EXIT"
152
+ fi
153
+
154
+ # 6. built → verified(pass) auto-advances to phase 2, resets status to setup
155
+ touch "$TMP/.planning/phase-1-verification.md"
156
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass 2>&1)
157
+ EXIT=$?
158
+ if [ "$EXIT" -eq 0 ] \
159
+ && echo "$OUT" | grep -q '"ok": true' \
160
+ && echo "$OUT" | grep -q '"phase": 2' \
161
+ && echo "$OUT" | grep -q '"status": "setup"'; then
162
+ pass "built → verified(pass) auto-advances phase and resets to setup"
163
+ else
164
+ fail_case "built → verified(pass) auto-advance" "exit=$EXIT out=$OUT"
165
+ fi
166
+
167
+ # 7. built → verified(fail) stays on phase 1, records verification=fail
168
+ TMP=$(make_project)
169
+ touch "$TMP/.planning/phase-1-plan.md"
170
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
171
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 3 --tasks-total 5 >/dev/null 2>&1)
172
+ touch "$TMP/.planning/phase-1-verification.md"
173
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification fail 2>&1)
174
+ EXIT=$?
175
+ if [ "$EXIT" -eq 0 ] \
176
+ && echo "$OUT" | grep -q '"ok": true' \
177
+ && echo "$OUT" | grep -q '"phase": 1' \
178
+ && echo "$OUT" | grep -q '"status": "verified"' \
179
+ && echo "$OUT" | grep -q '"verification": "fail"'; then
180
+ pass "built → verified(fail) stays on phase 1"
181
+ else
182
+ fail_case "built → verified(fail)" "exit=$EXIT out=$OUT"
183
+ fi
184
+
185
+ # ─── Precondition failures ───────────────────────────────
186
+ echo ""
187
+ echo "precondition failures:"
188
+
189
+ # 8. setup → built fails with PRECONDITION_FAILED
190
+ TMP=$(make_project)
191
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to built 2>&1)
192
+ EXIT=$?
193
+ if [ "$EXIT" -eq 1 ] \
194
+ && echo "$OUT" | grep -q '"ok": false' \
195
+ && echo "$OUT" | grep -q '"error": "PRECONDITION_FAILED"' \
196
+ && echo "$OUT" | grep -q "Cannot go from 'setup' to 'built'"; then
197
+ pass "setup → built fails with PRECONDITION_FAILED"
198
+ else
199
+ fail_case "setup → built precondition" "exit=$EXIT out=$OUT"
200
+ fi
201
+
202
+ # 9. planned → verified fails (requires status=built)
203
+ TMP=$(make_project)
204
+ touch "$TMP/.planning/phase-1-plan.md"
205
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
206
+ touch "$TMP/.planning/phase-1-verification.md"
207
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass 2>&1)
208
+ EXIT=$?
209
+ if [ "$EXIT" -eq 1 ] \
210
+ && echo "$OUT" | grep -q '"error": "PRECONDITION_FAILED"' \
211
+ && echo "$OUT" | grep -q "Cannot go from 'planned' to 'verified'"; then
212
+ pass "planned → verified fails (requires built)"
213
+ else
214
+ fail_case "planned → verified precondition" "exit=$EXIT out=$OUT"
215
+ fi
216
+
217
+ # 10. planned with missing plan file → MISSING_FILE
218
+ TMP=$(make_project)
219
+ # no phase-1-plan.md created
220
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1)
221
+ EXIT=$?
222
+ if [ "$EXIT" -eq 1 ] \
223
+ && echo "$OUT" | grep -q '"error": "MISSING_FILE"' \
224
+ && echo "$OUT" | grep -q "phase-1-plan.md"; then
225
+ pass "setup → planned fails without plan file (MISSING_FILE)"
226
+ else
227
+ fail_case "setup → planned MISSING_FILE" "exit=$EXIT out=$OUT"
228
+ fi
229
+
230
+ # 11. built → verified with missing verification file → MISSING_FILE
231
+ TMP=$(make_project)
232
+ touch "$TMP/.planning/phase-1-plan.md"
233
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
234
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
235
+ # NO verification file
236
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass 2>&1)
237
+ EXIT=$?
238
+ if [ "$EXIT" -eq 1 ] \
239
+ && echo "$OUT" | grep -q '"error": "MISSING_FILE"' \
240
+ && echo "$OUT" | grep -q "phase-1-verification.md"; then
241
+ pass "built → verified fails without verification file (MISSING_FILE)"
242
+ else
243
+ fail_case "built → verified MISSING_FILE" "exit=$EXIT out=$OUT"
244
+ fi
245
+
246
+ # 12. built → verified without --verification → MISSING_ARG
247
+ TMP=$(make_project)
248
+ touch "$TMP/.planning/phase-1-plan.md"
249
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
250
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
251
+ touch "$TMP/.planning/phase-1-verification.md"
252
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified 2>&1)
253
+ EXIT=$?
254
+ if [ "$EXIT" -eq 1 ] \
255
+ && echo "$OUT" | grep -q '"error": "MISSING_ARG"' \
256
+ && echo "$OUT" | grep -q "verification"; then
257
+ pass "built → verified without --verification → MISSING_ARG"
258
+ else
259
+ fail_case "built → verified MISSING_ARG" "exit=$EXIT out=$OUT"
260
+ fi
261
+
262
+ # 13. → shipped without --deployed-url → MISSING_ARG
263
+ # Must go through polished first, so fabricate state by transitioning through the full path.
264
+ TMP=$(make_project)
265
+ touch "$TMP/.planning/phase-1-plan.md"
266
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
267
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
268
+ touch "$TMP/.planning/phase-1-verification.md"
269
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass >/dev/null 2>&1)
270
+ # Now on phase 2, status=setup. Run phase 2 to completion.
271
+ touch "$TMP/.planning/phase-2-plan.md"
272
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
273
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
274
+ touch "$TMP/.planning/phase-2-verification.md"
275
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass >/dev/null 2>&1)
276
+ # Status should now be "verified" on last phase (no auto-advance past last phase)
277
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to polished >/dev/null 2>&1)
278
+ # Now try ship without deployed-url
279
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to shipped 2>&1)
280
+ EXIT=$?
281
+ if [ "$EXIT" -eq 1 ] \
282
+ && echo "$OUT" | grep -q '"error": "MISSING_ARG"' \
283
+ && echo "$OUT" | grep -q "deployed-url"; then
284
+ pass "→ shipped without --deployed-url → MISSING_ARG"
285
+ else
286
+ fail_case "→ shipped MISSING_ARG" "exit=$EXIT out=$OUT"
287
+ fi
288
+
289
+ # 14. Unknown target --to frobnicate → INVALID_STATUS
290
+ TMP=$(make_project)
291
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to frobnicate 2>&1)
292
+ EXIT=$?
293
+ if [ "$EXIT" -eq 1 ] \
294
+ && echo "$OUT" | grep -q '"error": "INVALID_STATUS"'; then
295
+ pass "--to frobnicate → INVALID_STATUS"
296
+ else
297
+ fail_case "invalid target" "exit=$EXIT out=$OUT"
298
+ fi
299
+
300
+ # ─── Gap cycle circuit breaker ───────────────────────────
301
+ echo ""
302
+ echo "gap cycle circuit breaker:"
303
+
304
+ # 15. First gap closure: verified(fail) → planned, gap_cycles[1]=1
305
+ TMP=$(make_project)
306
+ touch "$TMP/.planning/phase-1-plan.md"
307
+ touch "$TMP/.planning/phase-1-verification.md"
308
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
309
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
310
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification fail >/dev/null 2>&1)
311
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1)
312
+ EXIT=$?
313
+ if [ "$EXIT" -eq 0 ] \
314
+ && echo "$OUT" | grep -q '"ok": true' \
315
+ && echo "$OUT" | grep -q '"gap_cycles": 1'; then
316
+ pass "first gap closure: verified(fail) → planned, gap_cycles=1"
317
+ else
318
+ fail_case "first gap closure" "exit=$EXIT out=$OUT"
319
+ fi
320
+
321
+ # 16. Second gap closure: gap_cycles[1]=2
322
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
323
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification fail >/dev/null 2>&1)
324
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1)
325
+ EXIT=$?
326
+ if [ "$EXIT" -eq 0 ] \
327
+ && echo "$OUT" | grep -q '"ok": true' \
328
+ && echo "$OUT" | grep -q '"gap_cycles": 2'; then
329
+ pass "second gap closure: gap_cycles=2"
330
+ else
331
+ fail_case "second gap closure" "exit=$EXIT out=$OUT"
332
+ fi
333
+
334
+ # 17. Third gap closure attempt → GAP_CYCLE_LIMIT
335
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
336
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification fail >/dev/null 2>&1)
337
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1)
338
+ EXIT=$?
339
+ if [ "$EXIT" -eq 1 ] \
340
+ && echo "$OUT" | grep -q '"error": "GAP_CYCLE_LIMIT"'; then
341
+ pass "third gap closure attempt blocked (GAP_CYCLE_LIMIT)"
342
+ else
343
+ fail_case "gap cycle limit" "exit=$EXIT out=$OUT"
344
+ fi
345
+
346
+ # 18. verified(pass) resets gap_cycles[1] to 0
347
+ # Set up a fresh project, do ONE failed cycle, then pass on the next attempt.
348
+ TMP=$(make_project)
349
+ touch "$TMP/.planning/phase-1-plan.md"
350
+ touch "$TMP/.planning/phase-1-verification.md"
351
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
352
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
353
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification fail >/dev/null 2>&1)
354
+ # gap_cycles[1] is now 0 before the gap closure; becomes 1 after
355
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
356
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
357
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass >/dev/null 2>&1)
358
+ # After pass, gap_cycles[1] should be reset to 0 in tracking.json
359
+ if grep -q '"1": 0' "$TMP/.planning/tracking.json"; then
360
+ pass "verified(pass) resets gap_cycles[1] to 0"
361
+ else
362
+ fail_case "gap cycle reset on pass"
363
+ fi
364
+
365
+ # ─── Special transitions ─────────────────────────────────
366
+ echo ""
367
+ echo "special transitions:"
368
+
369
+ # 19. --to note --notes "foo" succeeds, records notes
370
+ TMP=$(make_project)
371
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to note --notes "hello world" 2>&1)
372
+ EXIT=$?
373
+ if [ "$EXIT" -eq 0 ] \
374
+ && echo "$OUT" | grep -q '"ok": true' \
375
+ && echo "$OUT" | grep -q '"action": "note"' \
376
+ && echo "$OUT" | grep -q '"status": "setup"' \
377
+ && grep -q '"notes": "hello world"' "$TMP/.planning/tracking.json"; then
378
+ pass "--to note records notes, status unchanged"
379
+ else
380
+ fail_case "--to note" "exit=$EXIT out=$OUT"
381
+ fi
382
+
383
+ # 20. --to activity succeeds without status change
384
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to activity 2>&1)
385
+ EXIT=$?
386
+ if [ "$EXIT" -eq 0 ] \
387
+ && echo "$OUT" | grep -q '"ok": true' \
388
+ && echo "$OUT" | grep -q '"action": "activity"' \
389
+ && echo "$OUT" | grep -q '"status": "setup"'; then
390
+ pass "--to activity succeeds without status change"
391
+ else
392
+ fail_case "--to activity" "exit=$EXIT out=$OUT"
393
+ fi
394
+
395
+ # ─── Summary ─────────────────────────────────────────────
396
+ echo ""
397
+ echo "=== Results: $PASS passed, $FAIL failed ==="
398
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1
package/statusline.sh DELETED
@@ -1,93 +0,0 @@
1
- #!/bin/bash
2
- # Qualia status line — teal branded, shows phase + context + git
3
- input=$(cat)
4
-
5
- MODEL=$(echo "$input" | jq -r '.model.display_name')
6
- DIR=$(echo "$input" | jq -r '.workspace.current_dir')
7
- PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
8
- COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
9
- DURATION_MS=$(echo "$input" | jq -r '.cost.total_duration_ms // 0')
10
- AGENT=$(echo "$input" | jq -r '.agent.name // empty')
11
- WORKTREE=$(echo "$input" | jq -r '.worktree.name // empty')
12
-
13
- # Teal palette
14
- T='\033[38;2;0;206;209m' # Primary teal
15
- TG='\033[38;2;0;170;175m' # Teal glow (darker)
16
- TD='\033[38;2;0;130;135m' # Teal dim
17
- W='\033[38;2;220;225;230m' # White
18
- DIM='\033[38;2;80;90;100m' # Dim gray
19
- GREEN='\033[38;2;52;211;153m' # Success green
20
- YELLOW='\033[38;2;234;179;8m' # Warning
21
- RED='\033[38;2;239;68;68m' # Error
22
- RESET='\033[0m'
23
-
24
- # Context bar with teal gradient
25
- if [ "$PCT" -ge 80 ]; then BAR_COLOR="$RED"
26
- elif [ "$PCT" -ge 50 ]; then BAR_COLOR="$YELLOW"
27
- else BAR_COLOR="$T"; fi
28
-
29
- BAR_WIDTH=10
30
- FILLED=$((PCT * BAR_WIDTH / 100))
31
- EMPTY=$((BAR_WIDTH - FILLED))
32
- BAR=""
33
- [ "$FILLED" -gt 0 ] && printf -v FILL "%${FILLED}s" && BAR="${FILL// /━}"
34
- [ "$EMPTY" -gt 0 ] && printf -v PAD "%${EMPTY}s" && BAR="${BAR}${PAD// /╌}"
35
-
36
- # Git branch (cached for speed)
37
- CACHE="/tmp/qualia-git-cache"
38
- if [ ! -f "$CACHE" ] || [ $(($(date +%s) - $(stat -c %Y "$CACHE" 2>/dev/null || echo 0))) -gt 3 ]; then
39
- BRANCH=""
40
- CHANGES=0
41
- if git rev-parse --git-dir > /dev/null 2>&1; then
42
- BRANCH=$(git branch --show-current 2>/dev/null)
43
- CHANGES=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
44
- fi
45
- echo "$BRANCH|$CHANGES" > "$CACHE"
46
- fi
47
- IFS='|' read -r BRANCH CHANGES < "$CACHE"
48
-
49
- # Qualia phase from tracking.json
50
- PHASE_INFO=""
51
- TRACKING=".planning/tracking.json"
52
- if [ -f "$TRACKING" ]; then
53
- PHASE=$(jq -r '.phase // 0' "$TRACKING" 2>/dev/null)
54
- TOTAL=$(jq -r '.total_phases // 0' "$TRACKING" 2>/dev/null)
55
- STATUS=$(jq -r '.status // ""' "$TRACKING" 2>/dev/null)
56
- if [ "$TOTAL" -gt 0 ]; then
57
- # Phase progress mini-bar
58
- PDONE=$((PHASE * 100 / TOTAL))
59
- PFILL=$((PDONE / 25))
60
- PEMPT=$((4 - PFILL))
61
- PBAR=""
62
- [ "$PFILL" -gt 0 ] && printf -v PF "%${PFILL}s" && PBAR="${PF// /●}"
63
- [ "$PEMPT" -gt 0 ] && printf -v PE "%${PEMPT}s" && PBAR="${PBAR}${PE// /○}"
64
- PHASE_INFO="${T}${PBAR}${RESET} ${W}P${PHASE}/${TOTAL}${RESET} ${TG}${STATUS}${RESET}"
65
- fi
66
- fi
67
-
68
- # Duration
69
- MINS=$((DURATION_MS / 60000))
70
- SECS=$(((DURATION_MS % 60000) / 1000))
71
- [ "$MINS" -gt 0 ] && DUR="${MINS}m" || DUR="${SECS}s"
72
-
73
- # Cost
74
- COST_FMT=$(printf '$%.2f' "$COST")
75
-
76
- # Line 1: Project + Git + Phase
77
- LINE1="${T}◆${RESET} ${W}${DIR##*/}${RESET}"
78
- if [ -n "$BRANCH" ]; then
79
- if [ "$CHANGES" -gt 0 ]; then
80
- LINE1="${LINE1} ${DIM}on${RESET} ${TG}${BRANCH}${RESET} ${YELLOW}~${CHANGES}${RESET}"
81
- else
82
- LINE1="${LINE1} ${DIM}on${RESET} ${TG}${BRANCH}${RESET}"
83
- fi
84
- fi
85
- [ -n "$AGENT" ] && LINE1="${LINE1} ${DIM}│${RESET} ${T}⚡${AGENT}${RESET}"
86
- [ -n "$WORKTREE" ] && LINE1="${LINE1} ${DIM}│${RESET} ${TD}⎇ ${WORKTREE}${RESET}"
87
- [ -n "$PHASE_INFO" ] && LINE1="${LINE1} ${DIM}│${RESET} ${PHASE_INFO}"
88
-
89
- # Line 2: Context bar + Cost + Duration + Model
90
- LINE2="${BAR_COLOR}${BAR}${RESET} ${DIM}${PCT}%${RESET} ${DIM}│${RESET} ${DIM}${COST_FMT}${RESET} ${DIM}│${RESET} ${DIM}${DUR}${RESET} ${DIM}│${RESET} ${TD}${MODEL}${RESET}"
91
-
92
- printf '%b\n' "$LINE1"
93
- printf '%b\n' "$LINE2"