qualia-framework 3.1.0 → 3.2.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/bin/install.js CHANGED
@@ -13,11 +13,8 @@ const YELLOW = "\x1b[38;2;234;179;8m";
13
13
  const RED = "\x1b[38;2;239;68;68m";
14
14
  const RESET = "\x1b[0m";
15
15
 
16
- const CLAUDE_DIR = path.join(require("os").homedir(), ".claude");
17
- const FRAMEWORK_DIR = path.resolve(__dirname, "..");
18
-
19
16
  // ─── Team codes ──────────────────────────────────────────
20
- const DEFAULT_TEAM = {
17
+ const TEAM = {
21
18
  "QS-FAWZI-01": {
22
19
  name: "Fawzi Goussous",
23
20
  role: "OWNER",
@@ -45,21 +42,8 @@ const DEFAULT_TEAM = {
45
42
  },
46
43
  };
47
44
 
48
- // Load team from external file, fall back to embedded defaults.
49
- function loadTeam() {
50
- const teamFile = path.join(CLAUDE_DIR, ".qualia-team.json");
51
- try {
52
- if (fs.existsSync(teamFile)) {
53
- const external = JSON.parse(fs.readFileSync(teamFile, "utf8"));
54
- if (external && typeof external === "object" && Object.keys(external).length > 0) {
55
- return external;
56
- }
57
- }
58
- } catch {}
59
- return DEFAULT_TEAM;
60
- }
61
-
62
- const TEAM = loadTeam();
45
+ const CLAUDE_DIR = path.join(require("os").homedir(), ".claude");
46
+ const FRAMEWORK_DIR = path.resolve(__dirname, "..");
63
47
 
64
48
  let installed = 0;
65
49
  let errors = 0;
@@ -81,34 +65,14 @@ function copy(src, dest) {
81
65
  fs.copyFileSync(src, dest);
82
66
  }
83
67
 
84
- // ─── Branded Header ─────────────────────────────────────
85
- const BOLD = "\x1b[1m";
86
- const TEAL_GLOW = "\x1b[38;2;0;170;175m";
87
- const PKG_VERSION = require("../package.json").version;
88
- const RULE = "━".repeat(48);
89
-
90
- function printHeader() {
91
- console.log("");
92
- console.log("");
93
- console.log(` ${TEAL}${BOLD}⬢ Q U A L I A${RESET}`);
94
- console.log(` ${DIM}${RULE}${RESET}`);
95
- console.log(` ${WHITE}Framework v${PKG_VERSION}${RESET} ${DIM}·${RESET} ${TEAL_GLOW}Qualia Solutions${RESET}`);
96
- console.log(` ${DIM}Plan → Build → Verify → Ship${RESET}`);
97
- console.log(` ${DIM}${RULE}${RESET}`);
98
- console.log("");
99
- }
100
-
101
- function printSection(title) {
102
- console.log("");
103
- console.log(` ${TEAL}▸${RESET} ${WHITE}${BOLD}${title}${RESET}`);
104
- console.log(` ${DIM}${"─".repeat(40)}${RESET}`);
105
- }
106
-
107
68
  // ─── Prompt for code ─────────────────────────────────────
108
69
  function askCode() {
109
70
  return new Promise((resolve) => {
110
71
  const rl = createInterface({ input: process.stdin, output: process.stdout });
111
- printHeader();
72
+ console.log("");
73
+ console.log(`${TEAL} ⬢ Qualia Framework v2${RESET}`);
74
+ console.log(`${DIM} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
75
+ console.log("");
112
76
  rl.question(` ${WHITE}Enter install code:${RESET} `, (answer) => {
113
77
  rl.close();
114
78
  resolve(answer.trim());
@@ -147,9 +111,10 @@ async function main() {
147
111
  }
148
112
 
149
113
  console.log("");
150
- const roleColor = member.role === "OWNER" ? TEAL : GREEN;
151
- console.log(` ${GREEN}✓${RESET} ${WHITE}${BOLD}Welcome, ${member.name}${RESET}`);
152
- console.log(` ${DIM} Role:${RESET} ${roleColor}${member.role}${RESET} ${DIM}·${RESET} ${DIM}Target:${RESET} ${WHITE}${CLAUDE_DIR}${RESET}`);
114
+ log(`${GREEN}✓${RESET} ${WHITE}${member.name}${RESET} ${DIM}(${member.role})${RESET}`);
115
+ console.log("");
116
+ log(`Installing to ${WHITE}${CLAUDE_DIR}${RESET}`);
117
+ console.log("");
153
118
 
154
119
  // ─── Skills ──────────────────────────────────────────
155
120
  const skillsDir = path.join(FRAMEWORK_DIR, "skills");
@@ -157,7 +122,7 @@ async function main() {
157
122
  .readdirSync(skillsDir)
158
123
  .filter((d) => fs.statSync(path.join(skillsDir, d)).isDirectory());
159
124
 
160
- printSection("Skills");
125
+ log(`${WHITE}Skills${RESET}`);
161
126
  for (const skill of skills) {
162
127
  try {
163
128
  copy(
@@ -171,7 +136,7 @@ async function main() {
171
136
  }
172
137
 
173
138
  // ─── Agents ────────────────────────────────────────────
174
- printSection("Agents");
139
+ log(`${WHITE}Agents${RESET}`);
175
140
  const agentsDir = path.join(FRAMEWORK_DIR, "agents");
176
141
  for (const file of fs.readdirSync(agentsDir)) {
177
142
  try {
@@ -183,7 +148,7 @@ async function main() {
183
148
  }
184
149
 
185
150
  // ─── Rules ─────────────────────────────────────────────
186
- printSection("Rules");
151
+ log(`${WHITE}Rules${RESET}`);
187
152
  const rulesDir = path.join(FRAMEWORK_DIR, "rules");
188
153
  for (const file of fs.readdirSync(rulesDir)) {
189
154
  try {
@@ -195,15 +160,20 @@ async function main() {
195
160
  }
196
161
 
197
162
  // ─── Hooks ─────────────────────────────────────────────
198
- printSection("Hooks");
163
+ log(`${WHITE}Hooks${RESET}`);
199
164
  const hooksSource = path.join(FRAMEWORK_DIR, "hooks");
200
165
  const hooksDest = path.join(CLAUDE_DIR, "hooks");
201
166
  if (!fs.existsSync(hooksDest)) fs.mkdirSync(hooksDest, { recursive: true });
202
- // Clean up legacy .sh hooks from previous v2.5/v2.6 installs so no orphans
203
- // remain on disk after upgrading to the pure-Node v2.7+ hooks.
167
+ // Clean up legacy .sh hooks from previous installs so no orphans
168
+ // remain on disk after upgrading to the pure-Node hooks.
169
+ // Also purge explicitly-deprecated hooks by name (these are no longer
170
+ // shipped in framework/hooks/ but may linger on existing installs).
171
+ const DEPRECATED_HOOKS = [
172
+ "block-env-edit.js", // v3.2.0: team decision to unblock .env read/write
173
+ ];
204
174
  try {
205
175
  for (const f of fs.readdirSync(hooksDest)) {
206
- if (f.endsWith(".sh")) {
176
+ if (f.endsWith(".sh") || DEPRECATED_HOOKS.includes(f)) {
207
177
  try { fs.unlinkSync(path.join(hooksDest, f)); } catch {}
208
178
  }
209
179
  }
@@ -220,8 +190,18 @@ async function main() {
220
190
  }
221
191
  }
222
192
 
193
+ // ─── Status line ───────────────────────────────────────
194
+ log(`${WHITE}Status line${RESET}`);
195
+ try {
196
+ const slDest = path.join(CLAUDE_DIR, "bin", "statusline.js");
197
+ copy(path.join(FRAMEWORK_DIR, "bin", "statusline.js"), slDest);
198
+ ok("statusline.js");
199
+ } catch (e) {
200
+ warn(`statusline.js — ${e.message}`);
201
+ }
202
+
223
203
  // ─── Templates ─────────────────────────────────────────
224
- printSection("Templates");
204
+ log(`${WHITE}Templates${RESET}`);
225
205
  const tmplDir = path.join(FRAMEWORK_DIR, "templates");
226
206
  const tmplDest = path.join(CLAUDE_DIR, "qualia-templates");
227
207
  if (!fs.existsSync(tmplDest)) fs.mkdirSync(tmplDest, { recursive: true });
@@ -235,7 +215,7 @@ async function main() {
235
215
  }
236
216
 
237
217
  // ─── CLAUDE.md with role ───────────────────────────────
238
- printSection("Configuration");
218
+ log(`${WHITE}CLAUDE.md${RESET}`);
239
219
  try {
240
220
  let claudeMd = fs.readFileSync(
241
221
  path.join(FRAMEWORK_DIR, "CLAUDE.md"),
@@ -251,7 +231,7 @@ async function main() {
251
231
  }
252
232
 
253
233
  // ─── Scripts ─────────────────────────────────────────────
254
- printSection("Scripts");
234
+ log(`${WHITE}Scripts${RESET}`);
255
235
  try {
256
236
  const binDest = path.join(CLAUDE_DIR, "bin");
257
237
  if (!fs.existsSync(binDest)) fs.mkdirSync(binDest, { recursive: true });
@@ -287,7 +267,7 @@ async function main() {
287
267
  }
288
268
 
289
269
  // ─── Knowledge directory ─────────────────────────────────
290
- printSection("Knowledge Base");
270
+ log(`${WHITE}Knowledge${RESET}`);
291
271
  const knowledgeDir = path.join(CLAUDE_DIR, "knowledge");
292
272
  if (!fs.existsSync(knowledgeDir)) fs.mkdirSync(knowledgeDir, { recursive: true });
293
273
  const knowledgeFiles = {
@@ -353,7 +333,7 @@ Recurring issues and their solutions.
353
333
  **Symptom:** \`/qualia-ship\` is blocked with "service_role found in client code" for a file that's actually a Server Component (runs server-side only).
354
334
  **Cause:** pre-deploy-gate.js skips files matching \`.server.\` filename pattern OR \`server/\` directory path. If the Server Component is at \`app/admin/page.tsx\` (no .server. marker, not in a server/ dir), the scan flags it.
355
335
  **Workaround:** Rename to \`.server.tsx\` OR move to a \`server/\` subdirectory OR extract the service_role usage into a helper in \`lib/server/\`.
356
- **Framework version:** Known issue as of v2.8.1; better heuristic planned for v3.0.
336
+ **Framework version:** Known issue; better heuristic planned for a future release.
357
337
  `,
358
338
  "client-prefs.md": `# Client Preferences
359
339
 
@@ -388,16 +368,11 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
388
368
  role: member.role,
389
369
  version: require("../package.json").version,
390
370
  installed_at: new Date().toISOString().split("T")[0],
391
- erp: {
392
- enabled: true,
393
- url: "https://portal.qualiasolutions.net",
394
- api_key_file: ".erp-api-key",
395
- },
396
371
  };
397
372
  fs.writeFileSync(configFile, JSON.stringify(config, null, 2) + "\n");
398
373
 
399
374
  // ─── ERP API key (for report uploads) ──────────────────
400
- printSection("ERP Integration");
375
+ log(`${WHITE}ERP integration${RESET}`);
401
376
  const erpKeyFile = path.join(CLAUDE_DIR, ".erp-api-key");
402
377
  if (!fs.existsSync(erpKeyFile)) {
403
378
  fs.writeFileSync(erpKeyFile, "qualia-claude-2026", { mode: 0o600 });
@@ -499,7 +474,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
499
474
  type: "command",
500
475
  if: "Bash(git push*)",
501
476
  command: nodeCmd("branch-guard.js"),
502
- timeout: 5,
477
+ timeout: 10,
503
478
  statusMessage: "⬢ Checking branch permissions...",
504
479
  },
505
480
  {
@@ -521,12 +496,6 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
521
496
  {
522
497
  matcher: "Edit|Write",
523
498
  hooks: [
524
- {
525
- type: "command",
526
- command: nodeCmd("block-env-edit.js"),
527
- timeout: 5,
528
- statusMessage: "⬢ Checking file permissions...",
529
- },
530
499
  {
531
500
  type: "command",
532
501
  if: "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)",
@@ -552,44 +521,38 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
552
521
  ],
553
522
  };
554
523
 
555
- // Permissions — no restrictions on env files or branches.
556
- // Everyone can read/write .env, push to main.
524
+ // Permissions
557
525
  if (!settings.permissions) settings.permissions = {};
558
526
  if (!settings.permissions.allow) settings.permissions.allow = [];
559
- if (!settings.permissions.deny) settings.permissions.deny = [];
560
-
561
- // ─── Optional: next-devtools MCP ─────────────────────────
562
- // Wire next-devtools-mcp for runtime error visibility in Next.js projects
563
- if (!settings.mcpServers) settings.mcpServers = {};
564
- if (!settings.mcpServers["next-devtools"]) {
565
- settings.mcpServers["next-devtools"] = {
566
- command: "npx",
567
- args: ["next-devtools-mcp@0.3.10"],
568
- disabled: false,
569
- };
570
- ok("MCP: next-devtools (runtime error visibility for Next.js projects)");
527
+ if (!settings.permissions.deny) {
528
+ settings.permissions.deny = [
529
+ "Read(./.env)",
530
+ "Read(./.env.*)",
531
+ "Read(./secrets/**)",
532
+ ];
571
533
  }
572
534
 
573
535
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
574
536
 
575
- ok("Hooks: session-start, auto-update, branch-guard, pre-push, block-env-edit, migration-guard, deploy-gate, pre-compact");
537
+ ok("Hooks: session-start, auto-update, branch-guard, pre-push, env-block, migration-guard, deploy-gate, pre-compact");
576
538
  ok("Status line + spinner configured");
577
539
  ok("Environment variables + permissions");
578
540
 
579
541
  // ─── Summary ───────────────────────────────────────────
580
542
  console.log("");
581
- console.log(` ${DIM}${RULE}${RESET}`);
582
- console.log(` ${TEAL}${BOLD}⬢ INSTALLED${RESET}`);
583
- console.log(` ${DIM}${RULE}${RESET}`);
584
- console.log("");
585
- console.log(` ${WHITE}${BOLD}${member.name}${RESET} ${DIM}·${RESET} ${roleColor}${member.role}${RESET} ${DIM}·${RESET} ${DIM}v${PKG_VERSION}${RESET}`);
586
- console.log("");
543
+ console.log(`${TEAL} ⬢ Installed ✓${RESET}`);
544
+ console.log(`${DIM} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
545
+ console.log(` ${WHITE}${member.name}${RESET} ${DIM}(${member.role})${RESET}`);
546
+ console.log(` Skills: ${WHITE}${skills.length}${RESET}`);
587
547
  const agentCount = fs.readdirSync(agentsDir).filter(f => f.endsWith('.md')).length;
588
- const hookCount = fs.readdirSync(hooksSource).length;
589
- const ruleCount = fs.readdirSync(rulesDir).length;
590
- const tmplCount = fs.readdirSync(tmplDir).length;
591
- console.log(` ${DIM}Skills${RESET} ${TEAL}${skills.length}${RESET} ${DIM}Agents${RESET} ${TEAL}${agentCount}${RESET} ${DIM}Hooks${RESET} ${TEAL}${hookCount}${RESET}`);
592
- console.log(` ${DIM}Rules${RESET} ${TEAL}${ruleCount}${RESET} ${DIM}Scripts${RESET} ${TEAL}3${RESET} ${DIM}Templates${RESET} ${TEAL}${tmplCount}${RESET}`);
548
+ console.log(` Agents: ${WHITE}${agentCount}${RESET} ${DIM}(planner, builder, verifier, qa-browser)${RESET}`);
549
+ console.log(` Hooks: ${WHITE}8${RESET} ${DIM}(session-start, auto-update, branch-guard, pre-push, env-block, migration-guard, deploy-gate, pre-compact)${RESET}`);
550
+ console.log(` Rules: ${WHITE}${fs.readdirSync(rulesDir).length}${RESET} ${DIM}(security, frontend, design-reference, deployment)${RESET}`);
551
+ console.log(` Scripts: ${WHITE}3${RESET} ${DIM}(state.js, qualia-ui.js, statusline.js)${RESET}`);
552
+ console.log(` Knowledge: ${WHITE}3${RESET} ${DIM}(patterns, fixes, client prefs)${RESET}`);
553
+ console.log(` Templates: ${WHITE}${fs.readdirSync(tmplDir).length}${RESET}`);
554
+ console.log(` Status line: ${GREEN}✓${RESET}`);
555
+ console.log(` CLAUDE.md: ${GREEN}✓${RESET} ${DIM}(${member.role})${RESET}`);
593
556
 
594
557
  if (errors > 0) {
595
558
  console.log("");
@@ -597,22 +560,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
597
560
  }
598
561
 
599
562
  console.log("");
600
- console.log(` ${DIM}${RULE}${RESET}`);
601
- console.log(` ${WHITE}${BOLD}Quick Start${RESET}`);
602
- console.log(` ${DIM}${RULE}${RESET}`);
603
- console.log("");
604
- console.log(` ${TEAL}1.${RESET} ${WHITE}Restart Claude Code${RESET} ${DIM}(loads new settings)${RESET}`);
605
- console.log(` ${TEAL}2.${RESET} ${WHITE}cd into any project${RESET} ${DIM}and run${RESET} ${TEAL}claude${RESET}`);
606
- console.log(` ${TEAL}3.${RESET} ${WHITE}Type${RESET} ${TEAL}${BOLD}/qualia${RESET} ${DIM}— it tells you what to do next${RESET}`);
607
- console.log("");
608
- console.log(` ${DIM}New project?${RESET} ${TEAL}/qualia-new${RESET}`);
609
- console.log(` ${DIM}Quick fix?${RESET} ${TEAL}/qualia-quick${RESET}`);
610
- console.log(` ${DIM}End of day?${RESET} ${TEAL}/qualia-report${RESET} ${DIM}(mandatory)${RESET}`);
611
- console.log(` ${DIM}Stuck?${RESET} ${TEAL}/qualia${RESET}`);
612
- console.log("");
613
- console.log(` ${DIM}${RULE}${RESET}`);
614
- console.log(` ${TEAL}${BOLD}Welcome to the future with Qualia.${RESET}`);
615
- console.log(` ${DIM}${RULE}${RESET}`);
563
+ console.log(` Restart Claude Code, then type ${TEAL}/qualia${RESET} in any project.`);
616
564
  console.log("");
617
565
  }
618
566
 
package/bin/qualia-ui.js CHANGED
@@ -16,6 +16,7 @@
16
16
  // done <N> <title> [commit] — task line (completed)
17
17
  // next <command> — "Run: /qualia-X" footer
18
18
  // end <status> [next-command] — closing banner with optional next
19
+ // update <current> <latest> — sticky framework update banner
19
20
 
20
21
  const fs = require("fs");
21
22
  const path = require("path");
@@ -248,6 +249,19 @@ function cmdNext(cmd) {
248
249
  console.log("");
249
250
  }
250
251
 
252
+ function cmdUpdate(current, latest) {
253
+ if (!current || !latest) return;
254
+ console.log("");
255
+ console.log(` ${YELLOW}${BOLD}▲${RESET} ${WHITE}${BOLD}QUALIA FRAMEWORK UPDATE AVAILABLE${RESET}`);
256
+ console.log(` ${DIM2}${RULE}${RESET}`);
257
+ console.log(` ${pad(DIM + "Current" + RESET, 20)}${DIM}${current}${RESET}`);
258
+ console.log(` ${pad(DIM + "Latest" + RESET, 20)}${GREEN}${BOLD}${latest}${RESET}`);
259
+ console.log(` ${pad(DIM + "Update" + RESET, 20)}${TEAL}npx qualia-framework@latest install${RESET}`);
260
+ console.log(` ${DIM2}${RULE}${RESET}`);
261
+ console.log(` ${DIM}This notice shows every session until you update.${RESET}`);
262
+ console.log("");
263
+ }
264
+
251
265
  function cmdEnd(status, nextCmd) {
252
266
  console.log("");
253
267
  console.log(` ${TEAL}${BOLD}⬢${RESET} ${WHITE}${BOLD}${status || "DONE"}${RESET}`);
@@ -276,6 +290,7 @@ switch (cmd) {
276
290
  case "done": cmdDone(rest[0], rest[1], rest[2]); break;
277
291
  case "next": cmdNext(rest.join(" ")); break;
278
292
  case "end": cmdEnd(rest[0], rest.slice(1).join(" ")); break;
293
+ case "update": cmdUpdate(rest[0], rest[1]); break;
279
294
  default:
280
295
  console.error(
281
296
  `Usage: qualia-ui.js <banner|context|divider|ok|fail|warn|info|spawn|wave|task|done|next|end> [args]`
package/bin/state.js CHANGED
@@ -9,17 +9,6 @@ const PLANNING = ".planning";
9
9
  const STATE_FILE = path.join(PLANNING, "STATE.md");
10
10
  const TRACKING_FILE = path.join(PLANNING, "tracking.json");
11
11
 
12
- // ─── Trace ──────────────────────────────────────────────
13
- function _trace(event, data) {
14
- try {
15
- const traceDir = path.join(require("os").homedir(), ".claude", ".qualia-traces");
16
- if (!fs.existsSync(traceDir)) fs.mkdirSync(traceDir, { recursive: true });
17
- const entry = { hook: event, timestamp: new Date().toISOString(), ...data };
18
- const file = path.join(traceDir, `${new Date().toISOString().split("T")[0]}.jsonl`);
19
- fs.appendFileSync(file, JSON.stringify(entry) + "\n");
20
- } catch { /* trace failures must not disrupt state machine */ }
21
- }
22
-
23
12
  // ─── Arg Parsing ─────────────────────────────────────────
24
13
  function parseArgs(argv) {
25
14
  const args = {};
@@ -197,25 +186,6 @@ const VALID_FROM = {
197
186
  done: ["handed_off"],
198
187
  };
199
188
 
200
- // ─── Configurable Gap Cycle Limit ────────────────────────
201
- function getGapCycleLimit() {
202
- // Priority: tracking.json.gap_cycle_limit > PROJECT.md > default (2)
203
- try {
204
- const t = readTracking();
205
- if (t && typeof t.gap_cycle_limit === "number" && t.gap_cycle_limit > 0) {
206
- return t.gap_cycle_limit;
207
- }
208
- } catch {}
209
-
210
- try {
211
- const projectMd = fs.readFileSync(path.join(PLANNING, "PROJECT.md"), "utf8");
212
- const match = projectMd.match(/^gap_cycle_limit:\s*(\d+)/m);
213
- if (match) return parseInt(match[1]);
214
- } catch {}
215
-
216
- return 2; // default
217
- }
218
-
219
189
  function checkPreconditions(current, target, opts) {
220
190
  const phase = parseInt(opts.phase) || current.phase;
221
191
 
@@ -237,14 +207,6 @@ function checkPreconditions(current, target, opts) {
237
207
  const planFile = path.join(PLANNING, `phase-${phase}-plan.md`);
238
208
  if (!fs.existsSync(planFile))
239
209
  return fail("MISSING_FILE", `Plan file not found: ${planFile}`);
240
- // Validate plan content (not just existence)
241
- const planContent = fs.readFileSync(planFile, "utf8");
242
- const taskHeaders = planContent.match(/^## Task \d+/gm);
243
- if (!taskHeaders || taskHeaders.length === 0)
244
- return fail("INVALID_PLAN", "Plan file has no task headers (expected '## Task N')");
245
- const doneWhenCount = (planContent.match(/\*\*Done when:\*\*/g) || []).length;
246
- if (doneWhenCount < taskHeaders.length)
247
- return fail("INVALID_PLAN", `${taskHeaders.length} tasks but only ${doneWhenCount} 'Done when:' entries`);
248
210
  }
249
211
 
250
212
  if (target === "verified") {
@@ -266,15 +228,14 @@ function checkPreconditions(current, target, opts) {
266
228
  return fail("MISSING_FILE", `Handoff file not found: ${hFile}`);
267
229
  }
268
230
 
269
- // Gap-closure circuit breaker (configurable limit)
231
+ // Gap-closure circuit breaker
270
232
  if (target === "planned" && current.status === "verified") {
271
233
  const t = readTracking() || {};
272
234
  const cycles = (t.gap_cycles || {})[String(phase)] || 0;
273
- const limit = getGapCycleLimit();
274
- if (cycles >= limit) {
235
+ if (cycles >= 2) {
275
236
  return fail(
276
237
  "GAP_CYCLE_LIMIT",
277
- `Phase ${phase} has failed verification ${cycles} times (limit: ${limit}). Escalate to Fawzi or re-plan from scratch.`
238
+ `Phase ${phase} has failed verification ${cycles} times. Escalate to Fawzi or re-plan from scratch.`
278
239
  );
279
240
  }
280
241
  }
@@ -333,7 +294,6 @@ function cmdCheck(opts) {
333
294
  assigned_to: s.assigned_to,
334
295
  verification: t.verification || "pending",
335
296
  gap_cycles: (t.gap_cycles || {})[String(s.phase)] || 0,
336
- gap_cycle_limit: getGapCycleLimit(),
337
297
  tasks_done: t.tasks_done || 0,
338
298
  tasks_total: t.tasks_total || 0,
339
299
  deployed_url: t.deployed_url || "",
@@ -371,6 +331,7 @@ function cmdTransition(opts) {
371
331
 
372
332
  // Special: note/activity (no status change)
373
333
  if (target === "note" || target === "activity") {
334
+ const now = new Date().toISOString().split("T")[0];
374
335
  if (opts.notes) t.notes = opts.notes;
375
336
  t.last_updated = new Date().toISOString();
376
337
  writeTracking(t);
@@ -392,16 +353,7 @@ function cmdTransition(opts) {
392
353
  target,
393
354
  { ...opts, phase }
394
355
  );
395
- if (!check.ok) {
396
- // Force only bypasses status-ordering errors (PRECONDITION_FAILED, GAP_CYCLE_LIMIT).
397
- // Never bypass MISSING_FILE, MISSING_ARG, INVALID_PLAN — those cause broken state.
398
- const forceableErrors = ["PRECONDITION_FAILED", "GAP_CYCLE_LIMIT"];
399
- if (opts.force && forceableErrors.includes(check.error)) {
400
- console.error(`WARNING: Forcing transition despite: ${check.message}`);
401
- } else {
402
- return output(check);
403
- }
404
- }
356
+ if (!check.ok) return output(check);
405
357
 
406
358
  const prevStatus = s.status;
407
359
 
@@ -480,18 +432,6 @@ function cmdTransition(opts) {
480
432
  return output(fail("WRITE_ERROR", e.message));
481
433
  }
482
434
 
483
- // Skill outcome scoring — log transition for analytics
484
- _trace("state-transition", {
485
- result: "allow",
486
- phase: s.phase,
487
- status: s.status,
488
- previous_status: prevStatus,
489
- verification: t.verification,
490
- gap_closure: prevStatus === "verified" && target === "planned",
491
- duration_ms: 0,
492
- extra: { verification: t.verification, gap_closure: prevStatus === "verified" && target === "planned" }
493
- });
494
-
495
435
  output({
496
436
  ok: true,
497
437
  phase: s.phase,
@@ -657,137 +597,6 @@ function cmdFix(opts) {
657
597
  });
658
598
  }
659
599
 
660
- function cmdValidatePlan(opts) {
661
- const phase = parseInt(opts.phase) || 1;
662
- const planFile = path.join(PLANNING, `phase-${phase}-plan.md`);
663
-
664
- if (!fs.existsSync(planFile)) {
665
- return output(fail("MISSING_FILE", `Plan file not found: ${planFile}`));
666
- }
667
-
668
- const content = fs.readFileSync(planFile, "utf8");
669
- const errors = [];
670
-
671
- // Check frontmatter exists
672
- if (!/^---\n/.test(content)) {
673
- errors.push("Missing frontmatter (---) at start of file");
674
- }
675
-
676
- // Check task count > 0
677
- const taskHeaders = content.match(/^## Task \d+/gm);
678
- if (!taskHeaders || taskHeaders.length === 0) {
679
- errors.push("No task headers found (expected '## Task N — title')");
680
- }
681
-
682
- // Check "Done when" exists for each task
683
- const taskCount = taskHeaders ? taskHeaders.length : 0;
684
- const doneWhenCount = (content.match(/\*\*Done when:\*\*/g) || []).length;
685
- if (doneWhenCount < taskCount) {
686
- errors.push(
687
- `${taskCount} tasks but only ${doneWhenCount} 'Done when:' entries`
688
- );
689
- }
690
-
691
- // Check Success Criteria section exists
692
- if (!/## Success Criteria/m.test(content)) {
693
- errors.push("Missing '## Success Criteria' section");
694
- }
695
-
696
- // Check goal in frontmatter
697
- if (!/^goal:/m.test(content)) {
698
- errors.push("Missing 'goal:' in frontmatter");
699
- }
700
-
701
- // ─── Verification Contract Validation (non-blocking) ────
702
- const warnings = [];
703
- const VALID_CHECK_TYPES = ["file-exists", "grep-match", "command-exit", "behavioral"];
704
- let contractCount = 0;
705
-
706
- if (/^## Verification Contract/m.test(content)) {
707
- // Extract the contract section (from header to next ## or end of file)
708
- const contractSectionMatch = content.match(
709
- /^## Verification Contract\s*\n([\s\S]+)/m
710
- );
711
- if (contractSectionMatch) {
712
- // Trim at the next ## heading that isn't ### (i.e., a new top-level section)
713
- let contractSection = contractSectionMatch[1];
714
- const nextH2 = contractSection.search(/\n## (?!#)/);
715
- if (nextH2 !== -1) contractSection = contractSection.substring(0, nextH2);
716
- // Each contract starts with ### Contract for Task N
717
- const contractBlocks = contractSection.match(/^### Contract for Task \d+/gm);
718
- contractCount = contractBlocks ? contractBlocks.length : 0;
719
-
720
- if (contractCount === 0) {
721
- warnings.push("Verification Contract section exists but contains no contract blocks (expected '### Contract for Task N')");
722
- } else {
723
- // Split into individual contract blocks for validation
724
- const blockSplits = contractSection.split(/^(?=### Contract for Task \d+)/m).filter(Boolean);
725
- for (const block of blockSplits) {
726
- const taskNumMatch = block.match(/^### Contract for Task (\d+)/);
727
- if (!taskNumMatch) continue;
728
- const taskNum = taskNumMatch[1];
729
-
730
- const checkTypeMatch = block.match(/\*\*Check type:\*\*\s*(.+)/);
731
- const hasCommand = /\*\*Command:\*\*/.test(block);
732
- const hasExpected = /\*\*Expected:\*\*/.test(block);
733
- const hasFailIf = /\*\*Fail if:\*\*/.test(block);
734
-
735
- if (!checkTypeMatch) {
736
- warnings.push(`Contract for Task ${taskNum}: missing 'Check type'`);
737
- } else {
738
- const checkType = checkTypeMatch[1].trim().toLowerCase();
739
- if (!VALID_CHECK_TYPES.includes(checkType)) {
740
- warnings.push(
741
- `Contract for Task ${taskNum}: invalid check type '${checkType}' (valid: ${VALID_CHECK_TYPES.join(", ")})`
742
- );
743
- }
744
- // behavioral type doesn't require Command or Expected
745
- const isBehavioral = checkType === "behavioral";
746
- if (!isBehavioral && !hasCommand) {
747
- warnings.push(`Contract for Task ${taskNum}: missing 'Command' (required for ${checkType})`);
748
- }
749
- if (!isBehavioral && !hasExpected) {
750
- warnings.push(`Contract for Task ${taskNum}: missing 'Expected' (required for ${checkType})`);
751
- }
752
- }
753
-
754
- if (!hasFailIf) {
755
- warnings.push(`Contract for Task ${taskNum}: missing 'Fail if'`);
756
- }
757
- }
758
- }
759
-
760
- // Warn if contract count < task count
761
- if (taskCount > 0 && contractCount > 0 && contractCount < taskCount) {
762
- warnings.push(
763
- `Only ${contractCount} contract(s) for ${taskCount} task(s) — not all tasks have verification contracts`
764
- );
765
- }
766
- }
767
- }
768
-
769
- if (errors.length > 0) {
770
- return output({
771
- ok: false,
772
- error: "PLAN_VALIDATION_FAILED",
773
- phase,
774
- errors,
775
- warnings: warnings.length > 0 ? warnings : undefined,
776
- message: `Plan file has ${errors.length} issue(s)`,
777
- });
778
- }
779
-
780
- output({
781
- ok: true,
782
- action: "validate-plan",
783
- phase,
784
- task_count: taskCount,
785
- done_when_count: doneWhenCount,
786
- contract_count: contractCount,
787
- warnings: warnings.length > 0 ? warnings : undefined,
788
- });
789
- }
790
-
791
600
  // ─── Output ──────────────────────────────────────────────
792
601
  function output(obj) {
793
602
  console.log(JSON.stringify(obj, null, 2));
@@ -811,14 +620,11 @@ switch (cmd) {
811
620
  case "fix":
812
621
  cmdFix(opts);
813
622
  break;
814
- case "validate-plan":
815
- cmdValidatePlan(opts);
816
- break;
817
623
  default:
818
624
  output(
819
625
  fail(
820
626
  "UNKNOWN_COMMAND",
821
- `Usage: state.js <check|transition|init|fix|validate-plan> [--options]`
627
+ `Usage: state.js <check|transition|init|fix> [--options]`
822
628
  )
823
629
  );
824
630
  }
package/bin/statusline.js CHANGED
@@ -224,11 +224,11 @@ try {
224
224
  if (AGENT) LINE1 += ` ${DIM}│${RESET} ${TEAL}⚡${AGENT}${RESET}`;
225
225
  if (WORKTREE) LINE1 += ` ${DIM}│${RESET} ${TEAL_DIM}⎇ ${WORKTREE}${RESET}`;
226
226
  if (PHASE_INFO) LINE1 += ` ${DIM}│${RESET} ${PHASE_INFO}`;
227
- // Memory, hooks, skills — context indicators with labels
227
+ // Memory, hooks, skills — context indicators
228
228
  const contextParts = [];
229
- if (MEMORY_COUNT > 0) contextParts.push(`${DIM}mem${RESET} ${TEAL}${MEMORY_COUNT}${RESET}`);
230
- if (HOOKS_COUNT > 0) contextParts.push(`${DIM}hooks${RESET} ${TEAL_GLOW}${HOOKS_COUNT}${RESET}`);
231
- if (SKILLS_COUNT > 0) contextParts.push(`${DIM}skills${RESET} ${TEAL_DIM}${SKILLS_COUNT}${RESET}`);
229
+ if (MEMORY_COUNT > 0) contextParts.push(`${TEAL}⊙${RESET}${DIM}${MEMORY_COUNT}${RESET}`);
230
+ if (HOOKS_COUNT > 0) contextParts.push(`${TEAL_GLOW}⚙${RESET}${DIM}${HOOKS_COUNT}${RESET}`);
231
+ if (SKILLS_COUNT > 0) contextParts.push(`${TEAL_DIM}✦${RESET}${DIM}${SKILLS_COUNT}${RESET}`);
232
232
  if (contextParts.length > 0) {
233
233
  LINE1 += ` ${DIM}│${RESET} ${contextParts.join(` ${DIM}·${RESET} `)}`;
234
234
  }