qualia-framework 5.3.0 → 5.5.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 +54 -30
- package/agents/builder.md +33 -8
- package/agents/plan-checker.md +60 -3
- package/agents/planner.md +26 -2
- package/agents/qa-browser.md +10 -0
- package/agents/research-synthesizer.md +10 -0
- package/agents/researcher.md +38 -2
- package/agents/roadmapper.md +10 -0
- package/agents/verifier.md +15 -3
- package/agents/visual-evaluator.md +1 -1
- package/bin/install.js +44 -2
- package/bin/plan-contract.js +32 -1
- package/bin/state.js +155 -133
- package/docs/archive/v4.0.0-review.md +288 -0
- package/docs/erp-contract.md +11 -0
- package/guide.md +14 -7
- package/hooks/session-start.js +1 -1
- package/package.json +5 -2
- package/rules/architecture.md +125 -0
- package/rules/infrastructure.md +1 -2
- package/rules/speed.md +55 -0
- package/skills/qualia-discuss/SKILL.md +17 -3
- package/skills/qualia-help/SKILL.md +1 -1
- package/skills/qualia-map/SKILL.md +1 -1
- package/skills/qualia-milestone/SKILL.md +1 -1
- package/skills/qualia-new/SKILL.md +2 -2
- package/skills/qualia-optimize/REFERENCE.md +2 -2
- package/skills/qualia-optimize/SKILL.md +1 -1
- package/skills/qualia-polish/SKILL.md +3 -3
- package/skills/qualia-polish-loop/REFERENCE.md +1 -1
- package/skills/qualia-polish-loop/SKILL.md +3 -3
- package/skills/qualia-polish-loop/fixtures/broken.html +2 -2
- package/skills/qualia-polish-loop/scripts/score.mjs +1 -1
- package/skills/qualia-postmortem/SKILL.md +1 -1
- package/skills/qualia-quick/SKILL.md +1 -1
- package/skills/qualia-report/SKILL.md +8 -6
- package/skills/qualia-research/SKILL.md +5 -3
- package/skills/qualia-road/SKILL.md +15 -5
- package/skills/qualia-task/SKILL.md +1 -1
- package/templates/CONTEXT.md +3 -2
- package/templates/PRODUCT.md +1 -1
- package/templates/help.html +1 -1
- package/templates/phase-context.md +5 -4
- package/tests/bin.test.sh +33 -3
- package/tests/lib.test.sh +21 -0
- package/tests/skills.test.sh +143 -0
- package/tests/slop-detect.test.sh +160 -0
- package/docs/install-redesign-builder-prompt.md +0 -290
- package/docs/install-redesign-pilot.md +0 -234
- package/docs/journey-demo.html +0 -1008
- package/docs/playwright-loop-builder-prompt.md +0 -185
- package/docs/playwright-loop-design-notes.md +0 -108
- package/docs/playwright-loop-tester-prompt.md +0 -213
- package/docs/polish-loop-supervised-run.md +0 -111
- /package/{rules → qualia-design}/design-brand.md +0 -0
- /package/{rules → qualia-design}/design-laws.md +0 -0
- /package/{rules → qualia-design}/design-product.md +0 -0
- /package/{rules → qualia-design}/design-reference.md +0 -0
- /package/{rules → qualia-design}/design-rubric.md +0 -0
- /package/{rules → qualia-design}/frontend.md +0 -0
package/bin/plan-contract.js
CHANGED
|
@@ -21,6 +21,28 @@ const CHECK_TYPES = new Set([
|
|
|
21
21
|
"file-exists", "grep-match", "command-exit", "behavioral",
|
|
22
22
|
]);
|
|
23
23
|
|
|
24
|
+
// Scope-reduction detection — phrases that signal an LLM has watered down the
|
|
25
|
+
// spec. The plan-checker agent does the same scan on the markdown plan; this
|
|
26
|
+
// function does it on the JSON contract's free-text fields (action +
|
|
27
|
+
// acceptance_criteria) so both paths catch the same failure mode.
|
|
28
|
+
const SCOPE_REDUCTION_PHRASES = [
|
|
29
|
+
/\bv1\b/i, /\bv2\b/i, /simplified version/i, /static for now/i,
|
|
30
|
+
/hardcoded for now/i, /\bplaceholder\b/i, /basic version/i,
|
|
31
|
+
/minimal implementation/i, /will be wired later/i,
|
|
32
|
+
/dynamic in future phase/i, /skip for now/i, /\bstub\b/i,
|
|
33
|
+
/mock for now/i, /we can improve this later/i, /quick win for now/i,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function findScopeReductionPhrases(text) {
|
|
37
|
+
if (typeof text !== "string") return [];
|
|
38
|
+
const hits = [];
|
|
39
|
+
for (const re of SCOPE_REDUCTION_PHRASES) {
|
|
40
|
+
const m = text.match(re);
|
|
41
|
+
if (m) hits.push(m[0]);
|
|
42
|
+
}
|
|
43
|
+
return hits;
|
|
44
|
+
}
|
|
45
|
+
|
|
24
46
|
function isStringArray(v) {
|
|
25
47
|
return Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
26
48
|
}
|
|
@@ -98,7 +120,15 @@ function validateTask(task, idx, allIds) {
|
|
|
98
120
|
errs.push(`${where}.acceptance_criteria: must be a non-empty string[]`);
|
|
99
121
|
}
|
|
100
122
|
if (typeof task.action !== "string") errs.push(`${where}.action: required string`);
|
|
101
|
-
else
|
|
123
|
+
else {
|
|
124
|
+
if (task.action.length > 500) errs.push(`${where}.action: must be ≤ 500 characters (got ${task.action.length})`);
|
|
125
|
+
const actionHits = findScopeReductionPhrases(task.action);
|
|
126
|
+
if (actionHits.length) errs.push(`${where}.action: scope-reduction phrase(s) detected: ${actionHits.join(", ")} — rewrite to deliver the actual spec, or split via locked-decision channel`);
|
|
127
|
+
}
|
|
128
|
+
for (let i = 0; i < (task.acceptance_criteria || []).length; i++) {
|
|
129
|
+
const acHits = findScopeReductionPhrases(task.acceptance_criteria[i]);
|
|
130
|
+
if (acHits.length) errs.push(`${where}.acceptance_criteria[${i}]: scope-reduction phrase(s) detected: ${acHits.join(", ")}`);
|
|
131
|
+
}
|
|
102
132
|
if (!isStringArray(task.context_files || [])) errs.push(`${where}.context_files: must be string[]`);
|
|
103
133
|
if (!Array.isArray(task.verification) || task.verification.length === 0) {
|
|
104
134
|
errs.push(`${where}.verification: must be a non-empty array`);
|
|
@@ -217,4 +247,5 @@ module.exports = {
|
|
|
217
247
|
parseSafely,
|
|
218
248
|
hashPlan,
|
|
219
249
|
checkDrift,
|
|
250
|
+
findScopeReductionPhrases,
|
|
220
251
|
};
|
package/bin/state.js
CHANGED
|
@@ -537,6 +537,139 @@ function cmdCheck(opts) {
|
|
|
537
537
|
});
|
|
538
538
|
}
|
|
539
539
|
|
|
540
|
+
// ─── cmdTransition helpers ──────────────────────────────────────────────
|
|
541
|
+
// cmdTransition was a 195-line orchestrator that mixed validation, the
|
|
542
|
+
// note/activity short-circuit, per-status mutations, atomic write, and
|
|
543
|
+
// trace+output into one block. Split into focused helpers below; the
|
|
544
|
+
// orchestrator now reads top-to-bottom in ~50 lines. Behavior is
|
|
545
|
+
// preserved exactly — verified by the 59 state.test.sh assertions.
|
|
546
|
+
|
|
547
|
+
// Handles the note/activity short-circuit. Returns the success payload
|
|
548
|
+
// when the target is note/activity (caller should `return output(...)`),
|
|
549
|
+
// or null if not handled (caller proceeds with normal transition).
|
|
550
|
+
function applyNoteOrActivity(target, s, t, opts) {
|
|
551
|
+
if (target !== "note" && target !== "activity") return null;
|
|
552
|
+
if (opts.notes) t.notes = opts.notes;
|
|
553
|
+
// Count tasks from quick/task work toward lifetime
|
|
554
|
+
if (opts.tasks_done) {
|
|
555
|
+
const count = parseInt(opts.tasks_done) || 0;
|
|
556
|
+
if (count > 0) {
|
|
557
|
+
ensureLifetime(t);
|
|
558
|
+
t.lifetime.tasks_completed += count;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
t.last_updated = new Date().toISOString();
|
|
562
|
+
writeTracking(t);
|
|
563
|
+
s.last_activity = opts.notes || "Activity logged";
|
|
564
|
+
writeStateMd(s);
|
|
565
|
+
return {
|
|
566
|
+
ok: true,
|
|
567
|
+
phase: s.phase,
|
|
568
|
+
status: s.status,
|
|
569
|
+
action: target,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Per-status mutations. Each helper mutates `s` and `t` in place (JS
|
|
574
|
+
// objects are by-reference) and returns nothing. They DO NOT write to
|
|
575
|
+
// disk — the orchestrator commits everything atomically at the end.
|
|
576
|
+
|
|
577
|
+
function applyPlannedTransition(s, t, prevStatus, phase) {
|
|
578
|
+
// Gap closure: increment counter if coming from verified(fail).
|
|
579
|
+
if (prevStatus === "verified") {
|
|
580
|
+
if (!t.gap_cycles) t.gap_cycles = {};
|
|
581
|
+
t.gap_cycles[String(phase)] = (t.gap_cycles[String(phase)] || 0) + 1;
|
|
582
|
+
s.last_activity = `Gap closure #${t.gap_cycles[String(phase)]} planned (phase ${phase})`;
|
|
583
|
+
}
|
|
584
|
+
if (s.phases[phase - 1]) s.phases[phase - 1].status = "planned";
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function applyBuiltTransition(s, t, opts, phase) {
|
|
588
|
+
t.tasks_done = parseInt(opts.tasks_done) || 0;
|
|
589
|
+
t.tasks_total = parseInt(opts.tasks_total) || 0;
|
|
590
|
+
t.wave = parseInt(opts.wave) || 0;
|
|
591
|
+
t.build_count = (parseInt(t.build_count) || 0) + 1;
|
|
592
|
+
s.last_activity = `Phase ${phase} built (${t.tasks_done}/${t.tasks_total} tasks)`;
|
|
593
|
+
if (s.phases[phase - 1]) s.phases[phase - 1].status = "built";
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function applyVerifiedTransition(s, t, opts, phase) {
|
|
597
|
+
t.verification = opts.verification;
|
|
598
|
+
s.last_activity = `Phase ${phase} verified — ${opts.verification}`;
|
|
599
|
+
if (s.phases[phase - 1]) {
|
|
600
|
+
s.phases[phase - 1].status =
|
|
601
|
+
opts.verification === "pass" ? "verified" : "failed";
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (opts.verification !== "pass") return;
|
|
605
|
+
|
|
606
|
+
// Auto-advance on pass: accumulate into lifetime BEFORE resetting
|
|
607
|
+
// current counters so the totals are correct.
|
|
608
|
+
ensureLifetime(t);
|
|
609
|
+
t.lifetime.tasks_completed += (t.tasks_done || 0);
|
|
610
|
+
t.lifetime.phases_completed += 1;
|
|
611
|
+
|
|
612
|
+
if (phase < s.total_phases) {
|
|
613
|
+
s.phase = phase + 1;
|
|
614
|
+
s.phase_name = s.phases[phase]?.name || `Phase ${phase + 1}`;
|
|
615
|
+
s.status = "setup";
|
|
616
|
+
t.phase = s.phase;
|
|
617
|
+
t.phase_name = s.phase_name;
|
|
618
|
+
t.status = "setup";
|
|
619
|
+
t.verification = "pending";
|
|
620
|
+
t.tasks_done = 0;
|
|
621
|
+
t.tasks_total = 0;
|
|
622
|
+
s.last_activity = `Phase ${phase} passed — advancing to phase ${s.phase}`;
|
|
623
|
+
}
|
|
624
|
+
// Reset gap counter for the passed phase.
|
|
625
|
+
if (t.gap_cycles) t.gap_cycles[String(phase)] = 0;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function applyPolishedTransition(s) {
|
|
629
|
+
// Polish is a whole-project pass. Mark every already-passed phase as
|
|
630
|
+
// polished. (Previously only the last row was touched and was set to
|
|
631
|
+
// "verified" — wrong status string, lost current-phase context.)
|
|
632
|
+
for (const p of s.phases) {
|
|
633
|
+
const st = (p.status || "").toLowerCase();
|
|
634
|
+
if (st === "verified" || st === "polished" || st === "completed" || st === "complete") {
|
|
635
|
+
p.status = "polished";
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function applyShippedTransition(t, opts) {
|
|
641
|
+
t.deployed_url = opts.deployed_url || "";
|
|
642
|
+
t.deploy_count = (parseInt(t.deploy_count) || 0) + 1;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Atomically commit the new state + tracking. Writes a journal snapshot
|
|
646
|
+
// of the pre-transition files first; if the process dies between the
|
|
647
|
+
// two real writes, the next invocation restores both from the journal.
|
|
648
|
+
// Each individual write is torn-write safe (tmp + rename); the journal
|
|
649
|
+
// closes the gap between the two writes.
|
|
650
|
+
//
|
|
651
|
+
// Returns null on success, or a `fail(...)` object on write error
|
|
652
|
+
// (caller should pass through to `output(...)`).
|
|
653
|
+
function commitTransitionAtomic(s, t) {
|
|
654
|
+
const backupState = readState();
|
|
655
|
+
const backupTracking = (() => {
|
|
656
|
+
try { return fs.readFileSync(TRACKING_FILE, "utf8"); } catch { return null; }
|
|
657
|
+
})();
|
|
658
|
+
try {
|
|
659
|
+
writeJournal(backupState, backupTracking);
|
|
660
|
+
writeStateMd(s);
|
|
661
|
+
writeTracking(t);
|
|
662
|
+
clearJournal();
|
|
663
|
+
return null;
|
|
664
|
+
} catch (e) {
|
|
665
|
+
// Revert whichever file is out of sync with pre-transition state.
|
|
666
|
+
try { if (backupState) atomicWrite(STATE_FILE, backupState); } catch {}
|
|
667
|
+
try { if (backupTracking) atomicWrite(TRACKING_FILE, backupTracking); } catch {}
|
|
668
|
+
clearJournal();
|
|
669
|
+
return fail("WRITE_ERROR", e.message);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
540
673
|
function cmdTransition(opts) {
|
|
541
674
|
const target = opts.to;
|
|
542
675
|
if (!target) return output(fail("MISSING_ARG", "--to is required"));
|
|
@@ -544,9 +677,7 @@ function cmdTransition(opts) {
|
|
|
544
677
|
const t = readTracking();
|
|
545
678
|
const s = parseStateMd(readState());
|
|
546
679
|
if (!t || !s) {
|
|
547
|
-
return output(
|
|
548
|
-
fail("NO_PROJECT", "No .planning/ found. Run /qualia-new.")
|
|
549
|
-
);
|
|
680
|
+
return output(fail("NO_PROJECT", "No .planning/ found. Run /qualia-new."));
|
|
550
681
|
}
|
|
551
682
|
|
|
552
683
|
// Refuse transitions if STATE.md has schema errors (severity=error)
|
|
@@ -559,47 +690,21 @@ function cmdTransition(opts) {
|
|
|
559
690
|
);
|
|
560
691
|
}
|
|
561
692
|
|
|
562
|
-
//
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
// Count tasks from quick/task work toward lifetime
|
|
566
|
-
if (opts.tasks_done) {
|
|
567
|
-
const count = parseInt(opts.tasks_done) || 0;
|
|
568
|
-
if (count > 0) {
|
|
569
|
-
ensureLifetime(t);
|
|
570
|
-
t.lifetime.tasks_completed += count;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
t.last_updated = new Date().toISOString();
|
|
574
|
-
writeTracking(t);
|
|
575
|
-
s.last_activity = opts.notes || "Activity logged";
|
|
576
|
-
writeStateMd(s);
|
|
577
|
-
return output({
|
|
578
|
-
ok: true,
|
|
579
|
-
phase: s.phase,
|
|
580
|
-
status: s.status,
|
|
581
|
-
action: target,
|
|
582
|
-
});
|
|
583
|
-
}
|
|
693
|
+
// Note/activity short-circuit (no status change, no precondition check)
|
|
694
|
+
const noteResult = applyNoteOrActivity(target, s, t, opts);
|
|
695
|
+
if (noteResult) return output(noteResult);
|
|
584
696
|
|
|
585
697
|
const phase = parseInt(opts.phase) || s.phase;
|
|
586
698
|
|
|
587
699
|
// Precondition check
|
|
588
|
-
const check = checkPreconditions(
|
|
589
|
-
{ ...s, phase },
|
|
590
|
-
target,
|
|
591
|
-
{ ...opts, phase }
|
|
592
|
-
);
|
|
700
|
+
const check = checkPreconditions({ ...s, phase }, target, { ...opts, phase });
|
|
593
701
|
if (!check.ok) {
|
|
594
|
-
//
|
|
595
|
-
// is retroactive bookkeeping: a phase was built without /qualia-plan and
|
|
596
|
-
// user is catching STATE.md up to reality.
|
|
597
|
-
// or MISSING_ARG — those would leave the state machine
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
"GAP_CYCLE_LIMIT",
|
|
601
|
-
"INVALID_PLAN",
|
|
602
|
-
];
|
|
702
|
+
// --force bypasses status-ordering and plan-content errors. The use case
|
|
703
|
+
// is retroactive bookkeeping: a phase was built without /qualia-plan and
|
|
704
|
+
// the user is catching STATE.md up to reality. --force never bypasses
|
|
705
|
+
// MISSING_FILE or MISSING_ARG — those would leave the state machine
|
|
706
|
+
// pointing at nothing.
|
|
707
|
+
const forceableErrors = ["PRECONDITION_FAILED", "GAP_CYCLE_LIMIT", "INVALID_PLAN"];
|
|
603
708
|
if (opts.force && forceableErrors.includes(check.error)) {
|
|
604
709
|
console.error(`WARNING: Forcing transition despite: ${check.message}`);
|
|
605
710
|
} else {
|
|
@@ -609,109 +714,26 @@ function cmdTransition(opts) {
|
|
|
609
714
|
|
|
610
715
|
const prevStatus = s.status;
|
|
611
716
|
|
|
612
|
-
// Apply transition
|
|
717
|
+
// Apply common transition fields
|
|
613
718
|
s.status = target;
|
|
614
719
|
s.last_activity = `${target} (phase ${phase})`;
|
|
615
|
-
|
|
616
|
-
// Update tracking fields
|
|
617
720
|
t.status = target;
|
|
618
721
|
t.phase = phase;
|
|
619
722
|
t.phase_name = s.phases[phase - 1]?.name || s.phase_name;
|
|
620
723
|
t.last_updated = new Date().toISOString();
|
|
621
724
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
}
|
|
629
|
-
// Update roadmap
|
|
630
|
-
if (s.phases[phase - 1]) s.phases[phase - 1].status = "planned";
|
|
631
|
-
}
|
|
725
|
+
// Per-status mutations
|
|
726
|
+
if (target === "planned") applyPlannedTransition(s, t, prevStatus, phase);
|
|
727
|
+
if (target === "built") applyBuiltTransition(s, t, opts, phase);
|
|
728
|
+
if (target === "verified") applyVerifiedTransition(s, t, opts, phase);
|
|
729
|
+
if (target === "polished") applyPolishedTransition(s);
|
|
730
|
+
if (target === "shipped") applyShippedTransition(t, opts);
|
|
632
731
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
t.wave = parseInt(opts.wave) || 0;
|
|
637
|
-
t.build_count = (parseInt(t.build_count) || 0) + 1;
|
|
638
|
-
s.last_activity = `Phase ${phase} built (${t.tasks_done}/${t.tasks_total} tasks)`;
|
|
639
|
-
if (s.phases[phase - 1]) s.phases[phase - 1].status = "built";
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
if (target === "verified") {
|
|
643
|
-
t.verification = opts.verification;
|
|
644
|
-
s.last_activity = `Phase ${phase} verified — ${opts.verification}`;
|
|
645
|
-
if (s.phases[phase - 1])
|
|
646
|
-
s.phases[phase - 1].status =
|
|
647
|
-
opts.verification === "pass" ? "verified" : "failed";
|
|
648
|
-
|
|
649
|
-
// Auto-advance on pass
|
|
650
|
-
if (opts.verification === "pass") {
|
|
651
|
-
// Accumulate into lifetime BEFORE resetting current counters
|
|
652
|
-
ensureLifetime(t);
|
|
653
|
-
t.lifetime.tasks_completed += (t.tasks_done || 0);
|
|
654
|
-
t.lifetime.phases_completed += 1;
|
|
655
|
-
|
|
656
|
-
if (phase < s.total_phases) {
|
|
657
|
-
s.phase = phase + 1;
|
|
658
|
-
s.phase_name = s.phases[phase]?.name || `Phase ${phase + 1}`;
|
|
659
|
-
s.status = "setup";
|
|
660
|
-
t.phase = s.phase;
|
|
661
|
-
t.phase_name = s.phase_name;
|
|
662
|
-
t.status = "setup";
|
|
663
|
-
t.verification = "pending";
|
|
664
|
-
t.tasks_done = 0;
|
|
665
|
-
t.tasks_total = 0;
|
|
666
|
-
s.last_activity = `Phase ${phase} passed — advancing to phase ${s.phase}`;
|
|
667
|
-
}
|
|
668
|
-
// Reset gap counter for the passed phase
|
|
669
|
-
if (t.gap_cycles) t.gap_cycles[String(phase)] = 0;
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
if (target === "polished") {
|
|
674
|
-
// Mark every passed phase as polished (polish is a whole-project pass).
|
|
675
|
-
// Previously only the last roadmap row was touched, and was set to
|
|
676
|
-
// "verified" — which both lost current-phase context and used the wrong
|
|
677
|
-
// status string. Now we use "polished" on every row that's already at
|
|
678
|
-
// verified or polished or completed.
|
|
679
|
-
for (const p of s.phases) {
|
|
680
|
-
const st = (p.status || "").toLowerCase();
|
|
681
|
-
if (st === "verified" || st === "polished" || st === "completed" || st === "complete") {
|
|
682
|
-
p.status = "polished";
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
if (target === "shipped") {
|
|
688
|
-
t.deployed_url = opts.deployed_url || "";
|
|
689
|
-
t.deploy_count = (parseInt(t.deploy_count) || 0) + 1;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// Write both files. We write a journal snapshot of the pre-transition
|
|
693
|
-
// STATE.md + tracking.json first; if the process dies between the two
|
|
694
|
-
// real writes, the next invocation will see the journal and restore both
|
|
695
|
-
// files to the pre-transition state. Each individual write is torn-write
|
|
696
|
-
// safe (tmp + rename); the journal closes the gap between the two.
|
|
697
|
-
const backupState = readState();
|
|
698
|
-
const backupTracking = (() => {
|
|
699
|
-
try { return fs.readFileSync(TRACKING_FILE, "utf8"); } catch { return null; }
|
|
700
|
-
})();
|
|
701
|
-
try {
|
|
702
|
-
writeJournal(backupState, backupTracking);
|
|
703
|
-
writeStateMd(s);
|
|
704
|
-
writeTracking(t);
|
|
705
|
-
clearJournal();
|
|
706
|
-
} catch (e) {
|
|
707
|
-
// Revert whichever file is out of sync with pre-transition state.
|
|
708
|
-
try { if (backupState) atomicWrite(STATE_FILE, backupState); } catch {}
|
|
709
|
-
try { if (backupTracking) atomicWrite(TRACKING_FILE, backupTracking); } catch {}
|
|
710
|
-
clearJournal();
|
|
711
|
-
return output(fail("WRITE_ERROR", e.message));
|
|
712
|
-
}
|
|
732
|
+
// Atomic commit
|
|
733
|
+
const writeError = commitTransitionAtomic(s, t);
|
|
734
|
+
if (writeError) return output(writeError);
|
|
713
735
|
|
|
714
|
-
//
|
|
736
|
+
// Trace transition for analytics
|
|
715
737
|
_trace("state-transition", "allow", {
|
|
716
738
|
phase: s.phase,
|
|
717
739
|
status: s.status,
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# v4.0.0 — Review & Publish Handoff
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-04-18
|
|
4
|
+
**Branch:** `feature/full-journey`
|
|
5
|
+
**Release tag:** v4.0.0 (not yet tagged, not yet pushed)
|
|
6
|
+
**Status:** Ready for review. Tests 156/156 green. Needs human eyeball before `git push` + `npm publish`.
|
|
7
|
+
|
|
8
|
+
This document is the single source of truth for what shipped in v4.0.0 and what the next agent/reviewer needs to verify before the release goes public.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## TL;DR
|
|
13
|
+
|
|
14
|
+
The Qualia Framework v4.0.0 makes `/qualia-new` produce the **entire project journey** (all milestones → Handoff) upfront, enables **end-to-end auto-chaining** of the Road with `--auto`, locks down the **milestone/phase/task hierarchy**, and **rebuilds `/qualia-idk` as a real diagnostician** instead of a router alias.
|
|
15
|
+
|
|
16
|
+
10 commits on `feature/full-journey` (8 feature + 1 docs + 1 release-state fix). ~28 files touched. 156/156 tests green.
|
|
17
|
+
|
|
18
|
+
Two local branches, neither pushed:
|
|
19
|
+
|
|
20
|
+
| Branch | HEAD | Ships |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `feature/story-file-plans` | `8ae5b0e` | v3.7.0 — story-file plan format (independent, useful on its own) |
|
|
23
|
+
| `feature/full-journey` | `f790554` | v4.0.0 — full journey, auto-chain, diagnostic idk (includes v3.7.0) |
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## The commits on `feature/full-journey`
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
94bc119 fix(v4): correct v4.0.0 — fold qualia-idk changelog entries into initial release
|
|
31
|
+
ea9e7f3 docs(v4): README, guide.md, SESSION_REPORT updated for v4.0.0 + V4_REVIEW.md handoff
|
|
32
|
+
f790554 release(v4.0.0): full journey — kickoff to handoff on rails
|
|
33
|
+
f62e753 fix(v4/phase-g): milestone summary cumulative task count + smoke tests
|
|
34
|
+
b41a52d feat(v4/phase-f): build_count + deploy_count bump, ERP v4 payload, handoff 4-deliverables
|
|
35
|
+
74dd26e feat(v4/phase-d-e): builder pre-inline + journey visualization
|
|
36
|
+
400cd17 feat(v4/phase-c): auto-chain wiring across Road skills
|
|
37
|
+
87af253 feat(v4/phase-b): roadmapper + qualia-new full-journey output
|
|
38
|
+
2e371c2 feat(v4/phase-a): model foundation — milestones[] + readiness guards
|
|
39
|
+
8ae5b0e release(v3.7.0): story-file plan format (from feature/story-file-plans)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Release-point:** `94bc119` is the canonical v4.0.0 HEAD (package.json = "4.0.0", CHANGELOG has a single [4.0.0] section with `/qualia-idk` folded in). Tag v4.0.0 at this commit.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## What v4.0.0 actually changes
|
|
47
|
+
|
|
48
|
+
### Hierarchy (locked down)
|
|
49
|
+
```
|
|
50
|
+
Project
|
|
51
|
+
└─ Journey (all milestones defined upfront)
|
|
52
|
+
└─ Milestone (a release — 2-5 total, Handoff is always last)
|
|
53
|
+
└─ Phase (2-5 tasks per phase)
|
|
54
|
+
└─ Task (one commit, one verification contract)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- Hard floor: 2 milestones. Hard ceiling: 5.
|
|
58
|
+
- Final milestone is **always literally named "Handoff"** with 4 fixed phases (Polish, Content + SEO, Final QA, Handoff).
|
|
59
|
+
- Every non-Handoff milestone needs **≥ 2 phases** (guarded by `state.js close-milestone`).
|
|
60
|
+
- Milestone numbering is **contiguous** — no skipped numbers.
|
|
61
|
+
|
|
62
|
+
### New artifact: `.planning/JOURNEY.md`
|
|
63
|
+
The North Star. Lists every milestone with why-now + exit criteria + phase sketches. Written once during `/qualia-new`, consulted at every milestone boundary.
|
|
64
|
+
|
|
65
|
+
### Auto mode (`--auto` flag)
|
|
66
|
+
```
|
|
67
|
+
/qualia-new --auto
|
|
68
|
+
→ research runs automatically
|
|
69
|
+
→ JOURNEY.md written
|
|
70
|
+
→ SINGLE human approval on the whole journey
|
|
71
|
+
→ plan 1 → build 1 → verify 1 → plan 2 → build 2 → verify 2 → ...
|
|
72
|
+
→ [milestone boundary — HUMAN GATE: "Continue to M{N+1}?"]
|
|
73
|
+
→ ... repeat until Handoff last phase ...
|
|
74
|
+
→ /qualia-ship → /qualia-handoff → /qualia-report → DONE
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Two human gates total per project** (journey approval + each milestone boundary). Plus one halt if gap-cycle limit exceeded on a failed phase.
|
|
78
|
+
|
|
79
|
+
### `/qualia-idk` now diagnostic (not a router alias)
|
|
80
|
+
Spawns two isolated `Explore` subagents in parallel:
|
|
81
|
+
1. **Plan view** — reads only `.planning/*`, reports what plan says we are + what should be TRUE
|
|
82
|
+
2. **Code view** — reads only source code, reports what's built + what compiles + what's stubbed, cites file:line
|
|
83
|
+
|
|
84
|
+
Main skill synthesizes: "What I see / The mismatch / What I think is happening / What to do next."
|
|
85
|
+
|
|
86
|
+
### `/qualia` description scoped back to state routing
|
|
87
|
+
Previously claimed "idk / stuck / lost / confused" triggers. Those now route to `/qualia-idk`. `/qualia` keeps "what next / what now / next command."
|
|
88
|
+
|
|
89
|
+
### Schema changes (all additive, backward compatible)
|
|
90
|
+
`tracking.json` gains:
|
|
91
|
+
- `milestone_name` — human name of current milestone
|
|
92
|
+
- `milestones[]` — array of closed milestone summaries (for ERP tree render)
|
|
93
|
+
- `build_count`, `deploy_count` — now actually incremented on transitions
|
|
94
|
+
|
|
95
|
+
`state.js` check output gains `milestone_name` + `milestones`.
|
|
96
|
+
|
|
97
|
+
`qualia-report` ERP payload now includes all v4 fields.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## How to verify before publishing
|
|
102
|
+
|
|
103
|
+
### 1. Run the test suite
|
|
104
|
+
```bash
|
|
105
|
+
cd /home/qualiasolutions/qualia-framework
|
|
106
|
+
node --test tests/runner.js
|
|
107
|
+
```
|
|
108
|
+
Expected: `# pass 156 / # fail 0`.
|
|
109
|
+
|
|
110
|
+
### 2. Visual smoke test of new UI commands
|
|
111
|
+
```bash
|
|
112
|
+
# Make a fake project
|
|
113
|
+
TMP=/tmp/qualia-v4-visual && rm -rf $TMP && mkdir -p $TMP/.planning
|
|
114
|
+
cat > $TMP/.planning/JOURNEY.md <<'EOF'
|
|
115
|
+
---
|
|
116
|
+
project: "Demo"
|
|
117
|
+
---
|
|
118
|
+
## Milestone 1 · Foundation
|
|
119
|
+
**Why now:** Base infra.
|
|
120
|
+
**Phases:**
|
|
121
|
+
1. **Setup**
|
|
122
|
+
2. **Auth**
|
|
123
|
+
## Milestone 2 · Core Features
|
|
124
|
+
**Why now:** Value delivery.
|
|
125
|
+
**Phases:**
|
|
126
|
+
1. **Feature A**
|
|
127
|
+
2. **Feature B**
|
|
128
|
+
## Milestone 3 · Handoff
|
|
129
|
+
**Phases:**
|
|
130
|
+
1. **Polish**
|
|
131
|
+
2. **Content + SEO**
|
|
132
|
+
3. **Final QA**
|
|
133
|
+
4. **Handoff**
|
|
134
|
+
EOF
|
|
135
|
+
cat > $TMP/.planning/tracking.json <<'EOF'
|
|
136
|
+
{"milestone":2,"milestone_name":"Core Features","milestones":[],"phase":1,"total_phases":2,"status":"built","lifetime":{"tasks_completed":4,"phases_completed":2,"milestones_completed":1}}
|
|
137
|
+
EOF
|
|
138
|
+
cat > $TMP/.planning/STATE.md <<'EOF'
|
|
139
|
+
## Current Position
|
|
140
|
+
Phase: 1 of 2 — Feature A
|
|
141
|
+
Status: built
|
|
142
|
+
Assigned to: Reviewer
|
|
143
|
+
|
|
144
|
+
## Roadmap
|
|
145
|
+
| # | Phase | Goal | Status |
|
|
146
|
+
|---|-------|------|--------|
|
|
147
|
+
| 1 | Feature A | x | built |
|
|
148
|
+
| 2 | Feature B | x | — |
|
|
149
|
+
EOF
|
|
150
|
+
(cd $TMP && node /home/qualiasolutions/qualia-framework/bin/qualia-ui.js journey-tree .planning/JOURNEY.md)
|
|
151
|
+
node /home/qualiasolutions/qualia-framework/bin/qualia-ui.js milestone-complete 1 "Foundation" "Core Features"
|
|
152
|
+
node /home/qualiasolutions/qualia-framework/bin/qualia-ui.js milestone-complete 3 "Handoff" ""
|
|
153
|
+
rm -rf $TMP
|
|
154
|
+
```
|
|
155
|
+
Expected:
|
|
156
|
+
- Journey tree shows green dot on M1, teal diamond on M2 (CURRENT), dim circle on M3 (FINAL).
|
|
157
|
+
- First milestone-complete banner shows `Next: Core Features`.
|
|
158
|
+
- Second shows `PROJECT COMPLETE · last milestone reached`.
|
|
159
|
+
|
|
160
|
+
### 3. State-machine end-to-end smoke
|
|
161
|
+
Covered by test case `tests/runner.js` "milestone summary captures cumulative tasks_completed" and "build_count bumps on each 'built' transition". Both pass.
|
|
162
|
+
|
|
163
|
+
### 4. Manual eyeball of key files
|
|
164
|
+
Review these for v4 correctness:
|
|
165
|
+
|
|
166
|
+
| File | Why |
|
|
167
|
+
|---|---|
|
|
168
|
+
| `templates/journey.md` | New artifact schema — is the format clear and complete? |
|
|
169
|
+
| `agents/roadmapper.md` | Biggest agent rewrite — does it correctly describe the 3-file output (JOURNEY + REQUIREMENTS + ROADMAP)? |
|
|
170
|
+
| `skills/qualia-new/SKILL.md` | Core flow — is the 14-step process coherent? Is `--auto` wiring clear? |
|
|
171
|
+
| `skills/qualia-idk/SKILL.md` | New diagnostic — does the 2-pass isolation make sense? Is the synthesis format useful? |
|
|
172
|
+
| `skills/qualia-verify/SKILL.md` | Auto-chain decision table (PASS → next phase / last phase → milestone / last milestone → ship) |
|
|
173
|
+
| `skills/qualia-milestone/SKILL.md` | Reads next milestone from JOURNEY.md now, not user prompt |
|
|
174
|
+
| `skills/qualia-handoff/SKILL.md` | 4 mandatory deliverables enforced |
|
|
175
|
+
| `bin/state.js` lines ~975-1050 | close-milestone readiness guards + milestones[] summary append |
|
|
176
|
+
| `bin/qualia-ui.js` journey-tree + milestone-complete | New visualizations |
|
|
177
|
+
| `CHANGELOG.md` [4.0.0] section | Full feature list + migration notes |
|
|
178
|
+
|
|
179
|
+
### 5. Backward-compat verification
|
|
180
|
+
The v4 changes are designed additive. Verify by:
|
|
181
|
+
```bash
|
|
182
|
+
# tracking.json from an older project (no milestones[], no milestone_name)
|
|
183
|
+
# should still work with state.js check
|
|
184
|
+
echo '{"milestone":1,"phase":1,"total_phases":3,"status":"setup","lifetime":{"tasks_completed":0,"phases_completed":0,"milestones_completed":0,"total_phases":0}}' > /tmp/old-tracking.json
|
|
185
|
+
mkdir -p /tmp/old-project/.planning && mv /tmp/old-tracking.json /tmp/old-project/.planning/tracking.json
|
|
186
|
+
cat > /tmp/old-project/.planning/STATE.md <<'EOF'
|
|
187
|
+
Phase: 1 of 3 — Setup
|
|
188
|
+
Status: setup
|
|
189
|
+
|
|
190
|
+
## Roadmap
|
|
191
|
+
| # | Phase | Goal | Status |
|
|
192
|
+
|---|-------|------|--------|
|
|
193
|
+
| 1 | Setup | x | ready |
|
|
194
|
+
| 2 | Feature | x | — |
|
|
195
|
+
| 3 | Ship | x | — |
|
|
196
|
+
EOF
|
|
197
|
+
(cd /tmp/old-project && node /home/qualiasolutions/qualia-framework/bin/state.js check | head -20)
|
|
198
|
+
rm -rf /tmp/old-project
|
|
199
|
+
```
|
|
200
|
+
Expected: `check` succeeds, output includes `milestones: []` and `milestone_name: ""` (hydrated by `ensureLifetime`).
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Publish checklist
|
|
205
|
+
|
|
206
|
+
Once review passes:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
# 1. Push both branches
|
|
210
|
+
git push -u origin feature/story-file-plans
|
|
211
|
+
git push -u origin feature/full-journey
|
|
212
|
+
|
|
213
|
+
# 2. Merge v3.7.0 first (recommended — gives it a distinct tag in history)
|
|
214
|
+
git checkout main
|
|
215
|
+
git merge --ff-only feature/story-file-plans
|
|
216
|
+
git tag v3.7.0
|
|
217
|
+
git push origin main --tags
|
|
218
|
+
|
|
219
|
+
# 3. Merge v4.0.0
|
|
220
|
+
git merge --ff-only feature/full-journey
|
|
221
|
+
# If fast-forward fails (unlikely since v4 branched off story-file-plans HEAD):
|
|
222
|
+
# git merge feature/full-journey # creates a merge commit — also fine
|
|
223
|
+
git tag v4.0.0
|
|
224
|
+
git push origin main --tags
|
|
225
|
+
|
|
226
|
+
# 4. npm publish
|
|
227
|
+
npm publish
|
|
228
|
+
# Expected: qualia-framework@4.0.0 goes live. Auto-update hook notifies
|
|
229
|
+
# every team member's installed client on next session.
|
|
230
|
+
|
|
231
|
+
# 5. Verify
|
|
232
|
+
npm view qualia-framework version # should return 4.0.0
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Alternative: one combined release
|
|
236
|
+
If the team would rather have a single v4.0.0 release tag (skip the v3.7.0 tag):
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
git checkout main
|
|
240
|
+
git merge --ff-only feature/full-journey
|
|
241
|
+
git tag v4.0.0
|
|
242
|
+
git push origin main --tags
|
|
243
|
+
npm publish
|
|
244
|
+
```
|
|
245
|
+
(v3.7.0 work is still in the history, just not tagged as its own release.)
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Risk areas (eyeball these)
|
|
250
|
+
|
|
251
|
+
1. **Auto-chain logic in `/qualia-verify`** — the decision table (PASS → next phase / last phase of milestone / last phase of Handoff / FAIL gap / FAIL limit) is described in the SKILL.md but relies on Claude to parse JOURNEY.md at runtime to detect "last phase of milestone." Smoke test didn't actually run the auto-chain because that requires live subagent invocation. **Recommended first test on merge**: run `/qualia-new --auto` on a throwaway project and watch how it handles the milestone boundary.
|
|
252
|
+
|
|
253
|
+
2. **Roadmapper's new three-file output** — the rewritten agent prompt is long and complex. The roadmapper must:
|
|
254
|
+
- Generate JOURNEY.md with all milestones
|
|
255
|
+
- Assign requirements to milestones
|
|
256
|
+
- Only detail Milestone 1 in ROADMAP.md
|
|
257
|
+
- Pass `--milestone_name` to `state.js init`
|
|
258
|
+
Smoke test didn't exercise the roadmapper directly — it's only triggered by `/qualia-new`.
|
|
259
|
+
|
|
260
|
+
3. **`/qualia-idk`'s two-pass isolation** — depends on `Explore` subagent correctly NOT crossing its scope. If an Explore agent reads .planning/ when told "code only" or vice versa, the diagnosis degrades. The SKILL.md has explicit "DO NOT read..." instructions but this is prompt-level enforcement, not hard boundary.
|
|
261
|
+
|
|
262
|
+
4. **Builder pre-inline context** — saves tool calls but inflates prompt size. On very large projects (PROJECT.md + DESIGN.md + 5 context files could easily be 10k+ tokens), the builder's prompt might balloon. Acceptable for most projects; watch for issues on monorepos.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Known limitations / deferred
|
|
267
|
+
|
|
268
|
+
- **No visual / mockup generation** (gstack's `/design-shotgun` or Superpowers' Visual Companion). Not in v4 — could be a v4.1 feature.
|
|
269
|
+
- **No cross-model review** (gstack's `/codex`). Not in v4.
|
|
270
|
+
- **No cross-project vector memory** (Claude-Flow's claude-mem pattern). Current `knowledge/` is still hand-authored markdown.
|
|
271
|
+
- **No IDE integration** (VS Code extension, JetBrains plugin). Claude Code CLI only.
|
|
272
|
+
- **No token-budget compression** (SuperClaude's ~70% compression claim). Qualia relies on fresh-context agents instead of compression.
|
|
273
|
+
|
|
274
|
+
These are all discussed in the competitive gap analysis done earlier in the session — see the chat transcript or memory for details.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Questions for Fawzi before publish
|
|
279
|
+
|
|
280
|
+
1. **Single v4.0.0 release or v3.7.0 + v4.0.0?** Default: both tags (v3.7.0 as a step release). Cleaner history. Alternative: fold into single v4.0.0.
|
|
281
|
+
2. **Alpha/beta tag on npm?** `npm publish` without `--tag` goes to `latest` and every installed client auto-updates on next session. If you want a slower rollout, publish with `--tag beta` and the team opts in manually with `npm install qualia-framework@beta install`.
|
|
282
|
+
3. **Do you want to dogfood v4 on an existing project before publish?** Running `npx qualia-framework@4.0.0-alpha install` locally on Sakani or another project is the safest way to verify end-to-end — especially the auto-chain which isn't covered by the unit tests.
|
|
283
|
+
|
|
284
|
+
Contact for questions: Fawzi Goussous — fawzi@qualiasolutions.net
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
*Generated 2026-04-18 by the v4 build session. Review this doc + CHANGELOG.md [4.0.0] before publishing.*
|