qualia-framework 2.5.1 → 3.1.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/CLAUDE.md +63 -0
- package/README.md +108 -30
- package/agents/builder.md +110 -0
- package/agents/planner.md +186 -0
- package/agents/qa-browser.md +186 -0
- package/agents/verifier.md +369 -0
- package/bin/cli.js +706 -417
- package/bin/install.js +622 -0
- package/bin/qualia-ui.js +284 -0
- package/bin/state.js +824 -0
- package/bin/statusline.js +252 -0
- package/docs/erp-contract.md +161 -0
- package/guide.md +63 -0
- package/hooks/auto-update.js +117 -0
- package/hooks/block-env-edit.js +52 -0
- package/hooks/branch-guard.js +68 -0
- package/hooks/migration-guard.js +83 -0
- package/hooks/pre-compact.js +52 -0
- package/hooks/pre-deploy-gate.js +149 -0
- package/hooks/pre-push.js +53 -0
- package/hooks/session-start.js +126 -0
- package/package.json +31 -17
- package/rules/design-reference.md +179 -0
- package/rules/frontend.md +126 -0
- package/rules/infrastructure.md +87 -0
- package/skills/qualia/SKILL.md +88 -0
- package/skills/qualia-build/SKILL.md +115 -0
- package/skills/qualia-debug/SKILL.md +87 -0
- package/skills/qualia-design/SKILL.md +99 -0
- package/skills/qualia-handoff/SKILL.md +66 -0
- package/skills/qualia-help/SKILL.md +60 -0
- package/skills/qualia-idk/SKILL.md +8 -0
- package/skills/qualia-learn/SKILL.md +111 -0
- package/skills/qualia-new/SKILL.md +323 -0
- package/skills/qualia-pause/SKILL.md +63 -0
- package/skills/qualia-plan/SKILL.md +101 -0
- package/skills/qualia-polish/SKILL.md +207 -0
- package/skills/qualia-quick/SKILL.md +37 -0
- package/skills/qualia-report/SKILL.md +114 -0
- package/skills/qualia-resume/SKILL.md +49 -0
- package/skills/qualia-review/SKILL.md +161 -0
- package/skills/qualia-ship/SKILL.md +90 -0
- package/skills/qualia-skill-new/SKILL.md +167 -0
- package/skills/qualia-task/SKILL.md +91 -0
- package/skills/qualia-test/SKILL.md +134 -0
- package/skills/qualia-verify/SKILL.md +113 -0
- package/templates/DESIGN.md +475 -0
- package/templates/help.html +476 -0
- package/templates/plan.md +42 -0
- package/templates/project.md +22 -0
- package/templates/state.md +27 -0
- package/templates/tracking.json +20 -0
- package/tests/bin.test.sh +687 -0
- package/tests/hooks.test.sh +384 -0
- package/tests/runner.js +1956 -0
- package/tests/state.test.sh +713 -0
- package/tests/statusline.test.sh +243 -0
- package/bin/collect-metrics.sh +0 -62
- package/framework/.claudeignore +0 -51
- package/framework/CLAUDE.md +0 -51
- package/framework/MCP_SETUP.md +0 -229
- package/framework/agents/architecture-strategist.md +0 -53
- package/framework/agents/backend-agent.md +0 -150
- package/framework/agents/code-simplicity-reviewer.md +0 -86
- package/framework/agents/frontend-agent.md +0 -111
- package/framework/agents/kieran-typescript-reviewer.md +0 -96
- package/framework/agents/performance-oracle.md +0 -111
- package/framework/agents/qualia-codebase-mapper.md +0 -761
- package/framework/agents/qualia-debugger.md +0 -1204
- package/framework/agents/qualia-executor.md +0 -882
- package/framework/agents/qualia-integration-checker.md +0 -424
- package/framework/agents/qualia-phase-researcher.md +0 -457
- package/framework/agents/qualia-plan-checker.md +0 -700
- package/framework/agents/qualia-planner.md +0 -1245
- package/framework/agents/qualia-project-researcher.md +0 -603
- package/framework/agents/qualia-research-synthesizer.md +0 -200
- package/framework/agents/qualia-roadmapper.md +0 -606
- package/framework/agents/qualia-verifier.md +0 -686
- package/framework/agents/red-team-qa.md +0 -130
- package/framework/agents/security-auditor.md +0 -72
- package/framework/agents/team-orchestrator.md +0 -229
- package/framework/agents/teams/framework-audit-team.md +0 -66
- package/framework/agents/teams/full-stack-team.md +0 -48
- package/framework/agents/teams/optimize-team.md +0 -53
- package/framework/agents/teams/review-team.md +0 -70
- package/framework/agents/teams/ship-team.md +0 -86
- package/framework/agents/test-agent.md +0 -182
- package/framework/hooks/auto-format.sh +0 -54
- package/framework/hooks/block-env-edit.sh +0 -42
- package/framework/hooks/branch-guard.sh +0 -43
- package/framework/hooks/confirm-delete.sh +0 -59
- package/framework/hooks/migration-validate.sh +0 -77
- package/framework/hooks/notification-speak.sh +0 -16
- package/framework/hooks/pre-commit.sh +0 -100
- package/framework/hooks/pre-compact.sh +0 -56
- package/framework/hooks/pre-deploy-gate.sh +0 -160
- package/framework/hooks/qualia-colors.sh +0 -32
- package/framework/hooks/retention-cleanup.sh +0 -62
- package/framework/hooks/save-session-state.sh +0 -185
- package/framework/hooks/session-context-loader.sh +0 -96
- package/framework/hooks/session-learn.sh +0 -32
- package/framework/hooks/skill-announce.sh +0 -123
- package/framework/hooks/tool-error-announce.sh +0 -27
- package/framework/install.ps1 +0 -323
- package/framework/install.sh +0 -313
- package/framework/qualia-framework/VERSION +0 -1
- package/framework/qualia-framework/assets/qualia-logo.png +0 -0
- package/framework/qualia-framework/bin/collect-metrics.sh +0 -67
- package/framework/qualia-framework/bin/generate-report-docx.py +0 -429
- package/framework/qualia-framework/bin/qualia-tools.js +0 -2201
- package/framework/qualia-framework/bin/qualia-tools.test.js +0 -1054
- package/framework/qualia-framework/references/checkpoints.md +0 -775
- package/framework/qualia-framework/references/completion-checklists.md +0 -359
- package/framework/qualia-framework/references/continuation-format.md +0 -249
- package/framework/qualia-framework/references/continuation-prompt.md +0 -97
- package/framework/qualia-framework/references/decimal-phase-calculation.md +0 -65
- package/framework/qualia-framework/references/design-quality.md +0 -56
- package/framework/qualia-framework/references/employee-guide.md +0 -167
- package/framework/qualia-framework/references/git-integration.md +0 -254
- package/framework/qualia-framework/references/git-planning-commit.md +0 -50
- package/framework/qualia-framework/references/model-profile-resolution.md +0 -32
- package/framework/qualia-framework/references/model-profiles.md +0 -73
- package/framework/qualia-framework/references/phase-argument-parsing.md +0 -61
- package/framework/qualia-framework/references/planning-config.md +0 -195
- package/framework/qualia-framework/references/questioning.md +0 -141
- package/framework/qualia-framework/references/tdd.md +0 -263
- package/framework/qualia-framework/references/ui-brand.md +0 -160
- package/framework/qualia-framework/references/verification-patterns.md +0 -612
- package/framework/qualia-framework/templates/DEBUG.md +0 -159
- package/framework/qualia-framework/templates/DESIGN.md +0 -81
- package/framework/qualia-framework/templates/UAT.md +0 -247
- package/framework/qualia-framework/templates/codebase/architecture.md +0 -255
- package/framework/qualia-framework/templates/codebase/concerns.md +0 -310
- package/framework/qualia-framework/templates/codebase/conventions.md +0 -307
- package/framework/qualia-framework/templates/codebase/integrations.md +0 -280
- package/framework/qualia-framework/templates/codebase/stack.md +0 -186
- package/framework/qualia-framework/templates/codebase/structure.md +0 -285
- package/framework/qualia-framework/templates/codebase/testing.md +0 -480
- package/framework/qualia-framework/templates/config.json +0 -35
- package/framework/qualia-framework/templates/context.md +0 -283
- package/framework/qualia-framework/templates/continue-here.md +0 -78
- package/framework/qualia-framework/templates/debug-subagent-prompt.md +0 -91
- package/framework/qualia-framework/templates/discovery.md +0 -146
- package/framework/qualia-framework/templates/lab-notes.md +0 -16
- package/framework/qualia-framework/templates/milestone-archive.md +0 -123
- package/framework/qualia-framework/templates/milestone.md +0 -115
- package/framework/qualia-framework/templates/phase-prompt.md +0 -567
- package/framework/qualia-framework/templates/planner-subagent-prompt.md +0 -117
- package/framework/qualia-framework/templates/project.md +0 -184
- package/framework/qualia-framework/templates/projects/ai-agent.md +0 -156
- package/framework/qualia-framework/templates/projects/mobile-app.md +0 -181
- package/framework/qualia-framework/templates/projects/voice-agent.md +0 -134
- package/framework/qualia-framework/templates/projects/website.md +0 -137
- package/framework/qualia-framework/templates/requirements.md +0 -231
- package/framework/qualia-framework/templates/research-project/ARCHITECTURE.md +0 -204
- package/framework/qualia-framework/templates/research-project/FEATURES.md +0 -147
- package/framework/qualia-framework/templates/research-project/PITFALLS.md +0 -200
- package/framework/qualia-framework/templates/research-project/STACK.md +0 -120
- package/framework/qualia-framework/templates/research-project/SUMMARY.md +0 -170
- package/framework/qualia-framework/templates/research.md +0 -552
- package/framework/qualia-framework/templates/roadmap.md +0 -206
- package/framework/qualia-framework/templates/state.md +0 -179
- package/framework/qualia-framework/templates/summary-complex.md +0 -59
- package/framework/qualia-framework/templates/summary-minimal.md +0 -41
- package/framework/qualia-framework/templates/summary-standard.md +0 -48
- package/framework/qualia-framework/templates/summary.md +0 -246
- package/framework/qualia-framework/templates/user-setup.md +0 -311
- package/framework/qualia-framework/templates/verification-report.md +0 -322
- package/framework/qualia-framework/workflows/add-phase.md +0 -179
- package/framework/qualia-framework/workflows/add-todo.md +0 -157
- package/framework/qualia-framework/workflows/audit-milestone.md +0 -241
- package/framework/qualia-framework/workflows/check-todos.md +0 -176
- package/framework/qualia-framework/workflows/complete-milestone.md +0 -858
- package/framework/qualia-framework/workflows/diagnose-issues.md +0 -219
- package/framework/qualia-framework/workflows/discovery-phase.md +0 -289
- package/framework/qualia-framework/workflows/discuss-phase.md +0 -534
- package/framework/qualia-framework/workflows/execute-phase.md +0 -559
- package/framework/qualia-framework/workflows/execute-plan.md +0 -438
- package/framework/qualia-framework/workflows/help.md +0 -470
- package/framework/qualia-framework/workflows/insert-phase.md +0 -220
- package/framework/qualia-framework/workflows/list-phase-assumptions.md +0 -178
- package/framework/qualia-framework/workflows/map-codebase.md +0 -327
- package/framework/qualia-framework/workflows/new-milestone.md +0 -363
- package/framework/qualia-framework/workflows/new-project.md +0 -982
- package/framework/qualia-framework/workflows/pause-work.md +0 -122
- package/framework/qualia-framework/workflows/plan-milestone-gaps.md +0 -256
- package/framework/qualia-framework/workflows/plan-phase.md +0 -422
- package/framework/qualia-framework/workflows/progress.md +0 -389
- package/framework/qualia-framework/workflows/quick.md +0 -252
- package/framework/qualia-framework/workflows/remove-phase.md +0 -326
- package/framework/qualia-framework/workflows/research-phase.md +0 -74
- package/framework/qualia-framework/workflows/resume-project.md +0 -306
- package/framework/qualia-framework/workflows/set-profile.md +0 -80
- package/framework/qualia-framework/workflows/settings.md +0 -145
- package/framework/qualia-framework/workflows/transition.md +0 -556
- package/framework/qualia-framework/workflows/update.md +0 -197
- package/framework/qualia-framework/workflows/verify-phase.md +0 -195
- package/framework/qualia-framework/workflows/verify-work.md +0 -625
- package/framework/rules/context7.md +0 -14
- package/framework/rules/frontend.md +0 -33
- package/framework/rules/speed.md +0 -23
- package/framework/scripts/__pycache__/say.cpython-314.pyc +0 -0
- package/framework/scripts/apply-retention.sh +0 -120
- package/framework/scripts/bootstrap-pop-os.sh +0 -354
- package/framework/scripts/claude-voice +0 -13
- package/framework/scripts/cleanup.sh +0 -131
- package/framework/scripts/cowork-mode.sh +0 -141
- package/framework/scripts/generate-project-claude-md.sh +0 -153
- package/framework/scripts/load-test-webhook.js +0 -172
- package/framework/scripts/say.py +0 -236
- package/framework/scripts/showcase-video-recorder/ffmpeg-builder.js +0 -167
- package/framework/scripts/showcase-video-recorder/playwright-helpers.js +0 -216
- package/framework/scripts/speak.py +0 -55
- package/framework/scripts/speak.sh +0 -18
- package/framework/scripts/status.sh +0 -138
- package/framework/scripts/sync-to-framework.sh +0 -65
- package/framework/scripts/voice-hotkey.py +0 -227
- package/framework/scripts/voice-input.sh +0 -51
- package/framework/skills/animate/SKILL.md +0 -202
- package/framework/skills/bolder/SKILL.md +0 -144
- package/framework/skills/browser-qa/SKILL.md +0 -536
- package/framework/skills/clarify/SKILL.md +0 -179
- package/framework/skills/client-handoff/SKILL.md +0 -135
- package/framework/skills/collab-onboard/SKILL.md +0 -111
- package/framework/skills/colorize/SKILL.md +0 -170
- package/framework/skills/critique/SKILL.md +0 -126
- package/framework/skills/deep-research/SKILL.md +0 -240
- package/framework/skills/delight/SKILL.md +0 -329
- package/framework/skills/deploy/SKILL.md +0 -261
- package/framework/skills/deploy-verify/SKILL.md +0 -377
- package/framework/skills/deploy-verify/scripts/canary-check.sh +0 -206
- package/framework/skills/deploy-verify/scripts/check-console-errors.js +0 -147
- package/framework/skills/deploy-verify/scripts/check-cwv.js +0 -139
- package/framework/skills/deploy-verify/scripts/project-detect.sh +0 -84
- package/framework/skills/deploy-verify/scripts/verify.sh +0 -548
- package/framework/skills/design-quieter/SKILL.md +0 -130
- package/framework/skills/distill/SKILL.md +0 -149
- package/framework/skills/docs-lookup/SKILL.md +0 -79
- package/framework/skills/fcm-notifications/SKILL.md +0 -125
- package/framework/skills/financial-ledger/SKILL.md +0 -1039
- package/framework/skills/frontend-master/NOTICE.md +0 -4
- package/framework/skills/frontend-master/SKILL.md +0 -127
- package/framework/skills/frontend-master/reference/color-and-contrast.md +0 -132
- package/framework/skills/frontend-master/reference/interaction-design.md +0 -123
- package/framework/skills/frontend-master/reference/motion-design.md +0 -99
- package/framework/skills/frontend-master/reference/responsive-design.md +0 -114
- package/framework/skills/frontend-master/reference/spatial-design.md +0 -100
- package/framework/skills/frontend-master/reference/typography.md +0 -131
- package/framework/skills/frontend-master/reference/ux-writing.md +0 -107
- package/framework/skills/harden/SKILL.md +0 -357
- package/framework/skills/i18n-rtl/SKILL.md +0 -752
- package/framework/skills/learn/SKILL.md +0 -95
- package/framework/skills/memory/SKILL.md +0 -50
- package/framework/skills/mobile-expo/SKILL.md +0 -977
- package/framework/skills/mobile-expo/references/store-checklist.md +0 -550
- package/framework/skills/nestjs-backend/README.md +0 -73
- package/framework/skills/nestjs-backend/SKILL.md +0 -446
- package/framework/skills/nestjs-backend/references/templates.md +0 -1173
- package/framework/skills/normalize/SKILL.md +0 -79
- package/framework/skills/onboard/SKILL.md +0 -242
- package/framework/skills/openrouter-agent/SKILL.md +0 -922
- package/framework/skills/polish/SKILL.md +0 -209
- package/framework/skills/pr/SKILL.md +0 -66
- package/framework/skills/qualia/SKILL.md +0 -199
- package/framework/skills/qualia-add-todo/SKILL.md +0 -68
- package/framework/skills/qualia-audit-milestone/SKILL.md +0 -95
- package/framework/skills/qualia-check-todos/SKILL.md +0 -55
- package/framework/skills/qualia-complete-milestone/SKILL.md +0 -134
- package/framework/skills/qualia-debug/SKILL.md +0 -149
- package/framework/skills/qualia-design/SKILL.md +0 -203
- package/framework/skills/qualia-discuss-phase/SKILL.md +0 -72
- package/framework/skills/qualia-evolve/SKILL.md +0 -200
- package/framework/skills/qualia-execute-phase/SKILL.md +0 -89
- package/framework/skills/qualia-framework-audit/SKILL.md +0 -604
- package/framework/skills/qualia-guide/SKILL.md +0 -32
- package/framework/skills/qualia-help/SKILL.md +0 -114
- package/framework/skills/qualia-idk/SKILL.md +0 -352
- package/framework/skills/qualia-list-phase-assumptions/SKILL.md +0 -67
- package/framework/skills/qualia-new-milestone/SKILL.md +0 -72
- package/framework/skills/qualia-new-project/SKILL.md +0 -232
- package/framework/skills/qualia-optimize/SKILL.md +0 -417
- package/framework/skills/qualia-pause-work/SKILL.md +0 -96
- package/framework/skills/qualia-plan-milestone-gaps/SKILL.md +0 -57
- package/framework/skills/qualia-plan-phase/SKILL.md +0 -104
- package/framework/skills/qualia-production-check/SKILL.md +0 -0
- package/framework/skills/qualia-progress/SKILL.md +0 -53
- package/framework/skills/qualia-quick/SKILL.md +0 -89
- package/framework/skills/qualia-report/SKILL.md +0 -166
- package/framework/skills/qualia-research-phase/SKILL.md +0 -88
- package/framework/skills/qualia-resume-work/SKILL.md +0 -62
- package/framework/skills/qualia-review/SKILL.md +0 -263
- package/framework/skills/qualia-start/SKILL.md +0 -161
- package/framework/skills/qualia-verify-work/SKILL.md +0 -132
- package/framework/skills/rag/SKILL.md +0 -750
- package/framework/skills/responsive/SKILL.md +0 -231
- package/framework/skills/retro/SKILL.md +0 -284
- package/framework/skills/sakani-conventions/SKILL.md +0 -136
- package/framework/skills/sakani-conventions/evals/evals.json +0 -23
- package/framework/skills/sakani-conventions/references/entities.md +0 -365
- package/framework/skills/sakani-conventions/references/error-codes.md +0 -95
- package/framework/skills/seo-master/SKILL.md +0 -490
- package/framework/skills/seo-master/references/checklist.md +0 -199
- package/framework/skills/seo-master/references/structured-data.md +0 -609
- package/framework/skills/ship/SKILL.md +0 -239
- package/framework/skills/stack-researcher/SKILL.md +0 -215
- package/framework/skills/status/SKILL.md +0 -154
- package/framework/skills/status/scripts/health-check.sh +0 -562
- package/framework/skills/subscription-payments/SKILL.md +0 -250
- package/framework/skills/supabase/SKILL.md +0 -973
- package/framework/skills/supabase/references/templates.md +0 -159
- package/framework/skills/team/SKILL.md +0 -67
- package/framework/skills/test-runner/SKILL.md +0 -202
- package/framework/skills/voice-agent/SKILL.md +0 -1312
- package/framework/skills/zoho-workflow/SKILL.md +0 -51
- package/framework/statusline-command.sh +0 -117
- package/framework/teams/default/inboxes/plan-04.json +0 -9
- package/framework/teams/review-team.md +0 -75
- package/framework/teams/ship-team.md +0 -86
- package/profiles/fawzi.json +0 -16
- package/profiles/hasan.json +0 -16
- package/profiles/moayad.json +0 -16
- package/templates/CLAUDE-owner.md +0 -52
- package/templates/CLAUDE.md.hbs +0 -58
- package/templates/env.claude.template +0 -12
- package/templates/settings.json +0 -172
- /package/{framework/rules → rules}/deployment.md +0 -0
- /package/{framework/rules → rules}/security.md +0 -0
package/bin/state.js
ADDED
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Qualia State Machine — atomic state transitions with precondition validation
|
|
3
|
+
// No external dependencies. Node >= 18 only.
|
|
4
|
+
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
|
|
8
|
+
const PLANNING = ".planning";
|
|
9
|
+
const STATE_FILE = path.join(PLANNING, "STATE.md");
|
|
10
|
+
const TRACKING_FILE = path.join(PLANNING, "tracking.json");
|
|
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
|
+
// ─── Arg Parsing ─────────────────────────────────────────
|
|
24
|
+
function parseArgs(argv) {
|
|
25
|
+
const args = {};
|
|
26
|
+
for (let i = 0; i < argv.length; i++) {
|
|
27
|
+
if (argv[i].startsWith("--")) {
|
|
28
|
+
const key = argv[i].slice(2).replace(/-/g, "_");
|
|
29
|
+
const next = argv[i + 1];
|
|
30
|
+
if (!next || next.startsWith("--")) {
|
|
31
|
+
args[key] = true;
|
|
32
|
+
} else {
|
|
33
|
+
args[key] = next;
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return args;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── File I/O ────────────────────────────────────────────
|
|
42
|
+
function readTracking() {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(fs.readFileSync(TRACKING_FILE, "utf8"));
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function writeTracking(t) {
|
|
51
|
+
fs.writeFileSync(TRACKING_FILE, JSON.stringify(t, null, 2) + "\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readState() {
|
|
55
|
+
try {
|
|
56
|
+
return fs.readFileSync(STATE_FILE, "utf8");
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── STATE.md Parser ─────────────────────────────────────
|
|
63
|
+
function parseStateMd(content) {
|
|
64
|
+
if (!content) return null;
|
|
65
|
+
const schema_errors = [];
|
|
66
|
+
const get = (prefix) => {
|
|
67
|
+
const m = content.match(new RegExp(`^${prefix}:\\s*(.+)$`, "m"));
|
|
68
|
+
return m ? m[1].trim() : "";
|
|
69
|
+
};
|
|
70
|
+
const hasField = (prefix) =>
|
|
71
|
+
new RegExp(`^${prefix}:\\s*`, "m").test(content);
|
|
72
|
+
|
|
73
|
+
const phaseMatch = content.match(
|
|
74
|
+
/^Phase:\s*(\d+)\s+of\s+(\d+)\s*[—-]\s*(.+)$/m
|
|
75
|
+
);
|
|
76
|
+
if (!phaseMatch) {
|
|
77
|
+
schema_errors.push({
|
|
78
|
+
field: "phase_header",
|
|
79
|
+
message: 'Missing or malformed "Phase: N of M — Name" header',
|
|
80
|
+
severity: "error",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Status field presence (independent of value)
|
|
85
|
+
if (!hasField("Status")) {
|
|
86
|
+
schema_errors.push({
|
|
87
|
+
field: "status_field",
|
|
88
|
+
message: "Missing Status: field",
|
|
89
|
+
severity: "warning",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Parse roadmap table
|
|
94
|
+
const phases = [];
|
|
95
|
+
const tableHeaderRe = /\| # \| Phase \| Goal \| Status \|/;
|
|
96
|
+
const tableMatch = content.match(
|
|
97
|
+
/\| # \| Phase \| Goal \| Status \|\n\|[-|]+\|\n([\s\S]*?)(?=\n##|\n$|$)/
|
|
98
|
+
);
|
|
99
|
+
if (!tableHeaderRe.test(content)) {
|
|
100
|
+
schema_errors.push({
|
|
101
|
+
field: "roadmap_table",
|
|
102
|
+
message: "Roadmap table header not found",
|
|
103
|
+
severity: "error",
|
|
104
|
+
});
|
|
105
|
+
} else if (!tableMatch) {
|
|
106
|
+
// Header is there but the separator row or body is malformed
|
|
107
|
+
schema_errors.push({
|
|
108
|
+
field: "roadmap_table",
|
|
109
|
+
message: "Roadmap table is malformed (missing separator row or body)",
|
|
110
|
+
severity: "error",
|
|
111
|
+
});
|
|
112
|
+
} else {
|
|
113
|
+
for (const row of tableMatch[1].trim().split("\n")) {
|
|
114
|
+
const cols = row.split("|").map((c) => c.trim()).filter(Boolean);
|
|
115
|
+
if (cols.length >= 4) {
|
|
116
|
+
phases.push({
|
|
117
|
+
num: parseInt(cols[0]),
|
|
118
|
+
name: cols[1],
|
|
119
|
+
goal: cols[2],
|
|
120
|
+
status: cols[3],
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Row count vs header "of M"
|
|
127
|
+
if (phaseMatch) {
|
|
128
|
+
const declaredTotal = parseInt(phaseMatch[2]);
|
|
129
|
+
if (phases.length && phases.length !== declaredTotal) {
|
|
130
|
+
schema_errors.push({
|
|
131
|
+
field: "roadmap_rows",
|
|
132
|
+
message: `Expected ${declaredTotal} phases in roadmap, found ${phases.length}`,
|
|
133
|
+
severity: "warning",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
phase: phaseMatch ? parseInt(phaseMatch[1]) : 1,
|
|
140
|
+
total_phases: phaseMatch ? parseInt(phaseMatch[2]) : phases.length || 1,
|
|
141
|
+
phase_name: phaseMatch ? phaseMatch[3].trim() : "",
|
|
142
|
+
status: get("Status").toLowerCase().replace(/\s+/g, "_") || "setup",
|
|
143
|
+
assigned_to: get("Assigned to") || "",
|
|
144
|
+
phases,
|
|
145
|
+
schema_errors,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ─── STATE.md Writer ─────────────────────────────────────
|
|
150
|
+
function writeStateMd(s) {
|
|
151
|
+
const phaseFrac = Math.round(((s.phase - 1) / s.total_phases) * 100);
|
|
152
|
+
const filled = Math.round(phaseFrac / 10);
|
|
153
|
+
const bar = "█".repeat(filled) + "░".repeat(10 - filled);
|
|
154
|
+
const now = new Date().toISOString().split("T")[0];
|
|
155
|
+
|
|
156
|
+
const roadmap = s.phases
|
|
157
|
+
.map((p) => `| ${p.num} | ${p.name} | ${p.goal} | ${p.status} |`)
|
|
158
|
+
.join("\n");
|
|
159
|
+
|
|
160
|
+
const md = `# Project State
|
|
161
|
+
|
|
162
|
+
## Project
|
|
163
|
+
See: .planning/PROJECT.md
|
|
164
|
+
|
|
165
|
+
## Current Position
|
|
166
|
+
Phase: ${s.phase} of ${s.total_phases} — ${s.phase_name}
|
|
167
|
+
Status: ${s.status}
|
|
168
|
+
Assigned to: ${s.assigned_to}
|
|
169
|
+
Last activity: ${now} — ${s.last_activity || "State updated"}
|
|
170
|
+
|
|
171
|
+
Progress: [${bar}] ${phaseFrac}%
|
|
172
|
+
|
|
173
|
+
## Roadmap
|
|
174
|
+
| # | Phase | Goal | Status |
|
|
175
|
+
|---|-------|------|--------|
|
|
176
|
+
${roadmap}
|
|
177
|
+
|
|
178
|
+
## Blockers
|
|
179
|
+
${s.blockers || "None."}
|
|
180
|
+
|
|
181
|
+
## Session
|
|
182
|
+
Last session: ${now}
|
|
183
|
+
Last worked by: ${s.assigned_to}
|
|
184
|
+
Resume: ${s.resume || "—"}
|
|
185
|
+
`;
|
|
186
|
+
fs.writeFileSync(STATE_FILE, md);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── Precondition Checks ─────────────────────────────────
|
|
190
|
+
const VALID_FROM = {
|
|
191
|
+
planned: ["setup", "verified"], // verified(fail) → planned = gap closure
|
|
192
|
+
built: ["planned"],
|
|
193
|
+
verified: ["built"],
|
|
194
|
+
polished: ["verified"],
|
|
195
|
+
shipped: ["polished"],
|
|
196
|
+
handed_off: ["shipped"],
|
|
197
|
+
done: ["handed_off"],
|
|
198
|
+
};
|
|
199
|
+
|
|
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
|
+
function checkPreconditions(current, target, opts) {
|
|
220
|
+
const phase = parseInt(opts.phase) || current.phase;
|
|
221
|
+
|
|
222
|
+
// Special transitions (no status gate)
|
|
223
|
+
if (target === "note" || target === "activity") return { ok: true };
|
|
224
|
+
|
|
225
|
+
// Check valid transition
|
|
226
|
+
const allowed = VALID_FROM[target];
|
|
227
|
+
if (!allowed) return fail("INVALID_STATUS", `Unknown status: ${target}`);
|
|
228
|
+
if (!allowed.includes(current.status)) {
|
|
229
|
+
return fail(
|
|
230
|
+
"PRECONDITION_FAILED",
|
|
231
|
+
`Cannot go from '${current.status}' to '${target}'. Allowed from: ${allowed.join(", ")}`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// File checks
|
|
236
|
+
if (target === "planned") {
|
|
237
|
+
const planFile = path.join(PLANNING, `phase-${phase}-plan.md`);
|
|
238
|
+
if (!fs.existsSync(planFile))
|
|
239
|
+
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
|
+
}
|
|
249
|
+
|
|
250
|
+
if (target === "verified") {
|
|
251
|
+
const vFile = path.join(PLANNING, `phase-${phase}-verification.md`);
|
|
252
|
+
if (!fs.existsSync(vFile))
|
|
253
|
+
return fail("MISSING_FILE", `Verification file not found: ${vFile}`);
|
|
254
|
+
if (!opts.verification || !["pass", "fail"].includes(opts.verification))
|
|
255
|
+
return fail("MISSING_ARG", "--verification must be 'pass' or 'fail'");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (target === "shipped") {
|
|
259
|
+
if (!opts.deployed_url)
|
|
260
|
+
return fail("MISSING_ARG", "--deployed-url is required for 'shipped'");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (target === "handed_off") {
|
|
264
|
+
const hFile = path.join(PLANNING, "HANDOFF.md");
|
|
265
|
+
if (!fs.existsSync(hFile))
|
|
266
|
+
return fail("MISSING_FILE", `Handoff file not found: ${hFile}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Gap-closure circuit breaker (configurable limit)
|
|
270
|
+
if (target === "planned" && current.status === "verified") {
|
|
271
|
+
const t = readTracking() || {};
|
|
272
|
+
const cycles = (t.gap_cycles || {})[String(phase)] || 0;
|
|
273
|
+
const limit = getGapCycleLimit();
|
|
274
|
+
if (cycles >= limit) {
|
|
275
|
+
return fail(
|
|
276
|
+
"GAP_CYCLE_LIMIT",
|
|
277
|
+
`Phase ${phase} has failed verification ${cycles} times (limit: ${limit}). Escalate to Fawzi or re-plan from scratch.`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return { ok: true };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function fail(error, message) {
|
|
286
|
+
return { ok: false, error, message };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ─── Next Command Logic ──────────────────────────────────
|
|
290
|
+
function nextCommand(status, phase, totalPhases, verification) {
|
|
291
|
+
switch (status) {
|
|
292
|
+
case "setup":
|
|
293
|
+
return `/qualia-plan ${phase}`;
|
|
294
|
+
case "planned":
|
|
295
|
+
return `/qualia-build ${phase}`;
|
|
296
|
+
case "built":
|
|
297
|
+
return `/qualia-verify ${phase}`;
|
|
298
|
+
case "verified":
|
|
299
|
+
if (verification === "fail") return `/qualia-plan ${phase} --gaps`;
|
|
300
|
+
if (phase < totalPhases) return `/qualia-plan ${phase + 1}`;
|
|
301
|
+
return "/qualia-polish";
|
|
302
|
+
case "polished":
|
|
303
|
+
return "/qualia-ship";
|
|
304
|
+
case "shipped":
|
|
305
|
+
return "/qualia-handoff";
|
|
306
|
+
case "handed_off":
|
|
307
|
+
return "/qualia-report";
|
|
308
|
+
case "done":
|
|
309
|
+
return "Done.";
|
|
310
|
+
default:
|
|
311
|
+
return `/qualia`;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ─── Commands ────────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
function cmdCheck(opts) {
|
|
318
|
+
const t = readTracking();
|
|
319
|
+
const s = parseStateMd(readState());
|
|
320
|
+
if (!t || !s) {
|
|
321
|
+
return output({
|
|
322
|
+
ok: false,
|
|
323
|
+
error: "NO_PROJECT",
|
|
324
|
+
message: "No .planning/ found. Run /qualia-new to start.",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
output({
|
|
328
|
+
ok: true,
|
|
329
|
+
phase: s.phase,
|
|
330
|
+
phase_name: s.phase_name,
|
|
331
|
+
total_phases: s.total_phases,
|
|
332
|
+
status: s.status,
|
|
333
|
+
assigned_to: s.assigned_to,
|
|
334
|
+
verification: t.verification || "pending",
|
|
335
|
+
gap_cycles: (t.gap_cycles || {})[String(s.phase)] || 0,
|
|
336
|
+
gap_cycle_limit: getGapCycleLimit(),
|
|
337
|
+
tasks_done: t.tasks_done || 0,
|
|
338
|
+
tasks_total: t.tasks_total || 0,
|
|
339
|
+
deployed_url: t.deployed_url || "",
|
|
340
|
+
next_command: nextCommand(
|
|
341
|
+
s.status,
|
|
342
|
+
s.phase,
|
|
343
|
+
s.total_phases,
|
|
344
|
+
t.verification
|
|
345
|
+
),
|
|
346
|
+
schema_errors: s.schema_errors && s.schema_errors.length ? s.schema_errors : undefined,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function cmdTransition(opts) {
|
|
351
|
+
const target = opts.to;
|
|
352
|
+
if (!target) return output(fail("MISSING_ARG", "--to is required"));
|
|
353
|
+
|
|
354
|
+
const t = readTracking();
|
|
355
|
+
const s = parseStateMd(readState());
|
|
356
|
+
if (!t || !s) {
|
|
357
|
+
return output(
|
|
358
|
+
fail("NO_PROJECT", "No .planning/ found. Run /qualia-new.")
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Refuse transitions if STATE.md has schema errors (severity=error)
|
|
363
|
+
if (s.schema_errors && s.schema_errors.some((e) => e.severity === "error")) {
|
|
364
|
+
return output(
|
|
365
|
+
fail(
|
|
366
|
+
"STATE_SCHEMA_ERROR",
|
|
367
|
+
"STATE.md is malformed. Run `node state.js check` to see errors. Consider `state.js fix` to rewrite canonically."
|
|
368
|
+
)
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Special: note/activity (no status change)
|
|
373
|
+
if (target === "note" || target === "activity") {
|
|
374
|
+
if (opts.notes) t.notes = opts.notes;
|
|
375
|
+
t.last_updated = new Date().toISOString();
|
|
376
|
+
writeTracking(t);
|
|
377
|
+
s.last_activity = opts.notes || "Activity logged";
|
|
378
|
+
writeStateMd(s);
|
|
379
|
+
return output({
|
|
380
|
+
ok: true,
|
|
381
|
+
phase: s.phase,
|
|
382
|
+
status: s.status,
|
|
383
|
+
action: target,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const phase = parseInt(opts.phase) || s.phase;
|
|
388
|
+
|
|
389
|
+
// Precondition check
|
|
390
|
+
const check = checkPreconditions(
|
|
391
|
+
{ ...s, phase },
|
|
392
|
+
target,
|
|
393
|
+
{ ...opts, phase }
|
|
394
|
+
);
|
|
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
|
+
}
|
|
405
|
+
|
|
406
|
+
const prevStatus = s.status;
|
|
407
|
+
|
|
408
|
+
// Apply transition
|
|
409
|
+
s.status = target;
|
|
410
|
+
s.last_activity = `${target} (phase ${phase})`;
|
|
411
|
+
|
|
412
|
+
// Update tracking fields
|
|
413
|
+
t.status = target;
|
|
414
|
+
t.phase = phase;
|
|
415
|
+
t.phase_name = s.phases[phase - 1]?.name || s.phase_name;
|
|
416
|
+
t.last_updated = new Date().toISOString();
|
|
417
|
+
|
|
418
|
+
if (target === "planned") {
|
|
419
|
+
// Gap closure: increment counter if coming from verified(fail)
|
|
420
|
+
if (prevStatus === "verified") {
|
|
421
|
+
if (!t.gap_cycles) t.gap_cycles = {};
|
|
422
|
+
t.gap_cycles[String(phase)] = (t.gap_cycles[String(phase)] || 0) + 1;
|
|
423
|
+
s.last_activity = `Gap closure #${t.gap_cycles[String(phase)]} planned (phase ${phase})`;
|
|
424
|
+
}
|
|
425
|
+
// Update roadmap
|
|
426
|
+
if (s.phases[phase - 1]) s.phases[phase - 1].status = "planned";
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (target === "built") {
|
|
430
|
+
t.tasks_done = parseInt(opts.tasks_done) || 0;
|
|
431
|
+
t.tasks_total = parseInt(opts.tasks_total) || 0;
|
|
432
|
+
t.wave = parseInt(opts.wave) || 0;
|
|
433
|
+
s.last_activity = `Phase ${phase} built (${t.tasks_done}/${t.tasks_total} tasks)`;
|
|
434
|
+
if (s.phases[phase - 1]) s.phases[phase - 1].status = "built";
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (target === "verified") {
|
|
438
|
+
t.verification = opts.verification;
|
|
439
|
+
s.last_activity = `Phase ${phase} verified — ${opts.verification}`;
|
|
440
|
+
if (s.phases[phase - 1])
|
|
441
|
+
s.phases[phase - 1].status =
|
|
442
|
+
opts.verification === "pass" ? "verified" : "failed";
|
|
443
|
+
|
|
444
|
+
// Auto-advance on pass
|
|
445
|
+
if (opts.verification === "pass") {
|
|
446
|
+
if (phase < s.total_phases) {
|
|
447
|
+
s.phase = phase + 1;
|
|
448
|
+
s.phase_name = s.phases[phase]?.name || `Phase ${phase + 1}`;
|
|
449
|
+
s.status = "setup";
|
|
450
|
+
t.phase = s.phase;
|
|
451
|
+
t.phase_name = s.phase_name;
|
|
452
|
+
t.status = "setup";
|
|
453
|
+
t.verification = "pending";
|
|
454
|
+
t.tasks_done = 0;
|
|
455
|
+
t.tasks_total = 0;
|
|
456
|
+
s.last_activity = `Phase ${phase} passed — advancing to phase ${s.phase}`;
|
|
457
|
+
}
|
|
458
|
+
// Reset gap counter for the passed phase
|
|
459
|
+
if (t.gap_cycles) t.gap_cycles[String(phase)] = 0;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (target === "polished") {
|
|
464
|
+
if (s.phases[s.phases.length - 1])
|
|
465
|
+
s.phases[s.phases.length - 1].status = "verified";
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (target === "shipped") {
|
|
469
|
+
t.deployed_url = opts.deployed_url || "";
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Write both files
|
|
473
|
+
const backupState = readState();
|
|
474
|
+
try {
|
|
475
|
+
writeStateMd(s);
|
|
476
|
+
writeTracking(t);
|
|
477
|
+
} catch (e) {
|
|
478
|
+
// Revert STATE.md on failure
|
|
479
|
+
if (backupState) fs.writeFileSync(STATE_FILE, backupState);
|
|
480
|
+
return output(fail("WRITE_ERROR", e.message));
|
|
481
|
+
}
|
|
482
|
+
|
|
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
|
+
output({
|
|
496
|
+
ok: true,
|
|
497
|
+
phase: s.phase,
|
|
498
|
+
phase_name: s.phase_name,
|
|
499
|
+
status: s.status,
|
|
500
|
+
previous_status: prevStatus,
|
|
501
|
+
verification: t.verification,
|
|
502
|
+
gap_cycles: (t.gap_cycles || {})[String(s.phase)] || 0,
|
|
503
|
+
next_command: nextCommand(s.status, s.phase, s.total_phases, t.verification),
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function cmdInit(opts) {
|
|
508
|
+
if (!opts.project) return output(fail("MISSING_ARG", "--project required"));
|
|
509
|
+
|
|
510
|
+
// Parse phases
|
|
511
|
+
let phases = [];
|
|
512
|
+
if (opts.phases) {
|
|
513
|
+
try {
|
|
514
|
+
phases = JSON.parse(opts.phases);
|
|
515
|
+
} catch {
|
|
516
|
+
return output(fail("INVALID_ARG", "--phases must be valid JSON array"));
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const totalPhases = parseInt(opts.total_phases) || phases.length || 1;
|
|
520
|
+
|
|
521
|
+
// Ensure phases array has entries
|
|
522
|
+
while (phases.length < totalPhases) {
|
|
523
|
+
phases.push({
|
|
524
|
+
name: `Phase ${phases.length + 1}`,
|
|
525
|
+
goal: "TBD",
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Create .planning/ if needed
|
|
530
|
+
if (!fs.existsSync(PLANNING)) fs.mkdirSync(PLANNING, { recursive: true });
|
|
531
|
+
|
|
532
|
+
const now = new Date().toISOString();
|
|
533
|
+
const date = now.split("T")[0];
|
|
534
|
+
|
|
535
|
+
// Build state
|
|
536
|
+
const s = {
|
|
537
|
+
phase: 1,
|
|
538
|
+
total_phases: totalPhases,
|
|
539
|
+
phase_name: phases[0].name,
|
|
540
|
+
status: "setup",
|
|
541
|
+
assigned_to: opts.assigned_to || "",
|
|
542
|
+
last_activity: `Project initialized`,
|
|
543
|
+
phases: phases.map((p, i) => ({
|
|
544
|
+
num: i + 1,
|
|
545
|
+
name: p.name,
|
|
546
|
+
goal: p.goal,
|
|
547
|
+
status: i === 0 ? "ready" : "—",
|
|
548
|
+
})),
|
|
549
|
+
blockers: "None.",
|
|
550
|
+
resume: "—",
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
// Build tracking
|
|
554
|
+
const t = {
|
|
555
|
+
project: opts.project,
|
|
556
|
+
client: opts.client || "",
|
|
557
|
+
type: opts.type || "",
|
|
558
|
+
assigned_to: opts.assigned_to || "",
|
|
559
|
+
phase: 1,
|
|
560
|
+
phase_name: phases[0].name,
|
|
561
|
+
total_phases: totalPhases,
|
|
562
|
+
status: "setup",
|
|
563
|
+
wave: 0,
|
|
564
|
+
tasks_done: 0,
|
|
565
|
+
tasks_total: 0,
|
|
566
|
+
verification: "pending",
|
|
567
|
+
gap_cycles: {},
|
|
568
|
+
blockers: [],
|
|
569
|
+
last_updated: now,
|
|
570
|
+
last_commit: "",
|
|
571
|
+
deployed_url: "",
|
|
572
|
+
notes: "",
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
writeStateMd(s);
|
|
576
|
+
writeTracking(t);
|
|
577
|
+
|
|
578
|
+
output({
|
|
579
|
+
ok: true,
|
|
580
|
+
action: "init",
|
|
581
|
+
project: opts.project,
|
|
582
|
+
phase: 1,
|
|
583
|
+
total_phases: totalPhases,
|
|
584
|
+
status: "setup",
|
|
585
|
+
next_command: "/qualia-plan 1",
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function cmdFix(opts) {
|
|
590
|
+
const raw = readState();
|
|
591
|
+
const t = readTracking();
|
|
592
|
+
if (!raw && !t) {
|
|
593
|
+
return output(
|
|
594
|
+
fail("NO_PROJECT", "No .planning/ found. Run /qualia-new.")
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
const parsed = parseStateMd(raw) || {
|
|
598
|
+
phase: 1,
|
|
599
|
+
total_phases: 1,
|
|
600
|
+
phase_name: "",
|
|
601
|
+
status: "setup",
|
|
602
|
+
assigned_to: "",
|
|
603
|
+
phases: [],
|
|
604
|
+
schema_errors: [
|
|
605
|
+
{ field: "content", message: "STATE.md missing or empty", severity: "error" },
|
|
606
|
+
],
|
|
607
|
+
};
|
|
608
|
+
const previousErrors = (parsed.schema_errors || []).length;
|
|
609
|
+
|
|
610
|
+
// Prefer tracking.json values when parsed fields are defaulted/missing
|
|
611
|
+
const tr = t || {};
|
|
612
|
+
const totalPhases =
|
|
613
|
+
parseInt(tr.total_phases) || parsed.total_phases || parsed.phases.length || 1;
|
|
614
|
+
const phaseNum = parseInt(tr.phase) || parsed.phase || 1;
|
|
615
|
+
const phaseName =
|
|
616
|
+
(parsed.phase_name && parsed.phase_name.trim()) ||
|
|
617
|
+
tr.phase_name ||
|
|
618
|
+
`Phase ${phaseNum}`;
|
|
619
|
+
const status = parsed.status || tr.status || "setup";
|
|
620
|
+
const assignedTo = parsed.assigned_to || tr.assigned_to || "";
|
|
621
|
+
|
|
622
|
+
// Build a phases array of the right length
|
|
623
|
+
const phases = [];
|
|
624
|
+
for (let i = 0; i < totalPhases; i++) {
|
|
625
|
+
const existing = parsed.phases[i];
|
|
626
|
+
phases.push({
|
|
627
|
+
num: i + 1,
|
|
628
|
+
name: existing?.name || `Phase ${i + 1}`,
|
|
629
|
+
goal: existing?.goal || "TBD",
|
|
630
|
+
status: existing?.status || (i === 0 ? "ready" : "—"),
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const s = {
|
|
635
|
+
phase: phaseNum,
|
|
636
|
+
total_phases: totalPhases,
|
|
637
|
+
phase_name: phaseName,
|
|
638
|
+
status,
|
|
639
|
+
assigned_to: assignedTo,
|
|
640
|
+
last_activity: "STATE.md repaired by state.js fix",
|
|
641
|
+
phases,
|
|
642
|
+
blockers: "None.",
|
|
643
|
+
resume: "—",
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
try {
|
|
647
|
+
writeStateMd(s);
|
|
648
|
+
} catch (e) {
|
|
649
|
+
return output(fail("WRITE_ERROR", e.message));
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
output({
|
|
653
|
+
ok: true,
|
|
654
|
+
action: "fix",
|
|
655
|
+
previous_errors: previousErrors,
|
|
656
|
+
fixed: true,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
|
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
|
+
// ─── Output ──────────────────────────────────────────────
|
|
792
|
+
function output(obj) {
|
|
793
|
+
console.log(JSON.stringify(obj, null, 2));
|
|
794
|
+
if (!obj.ok) process.exit(1);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// ─── Main ────────────────────────────────────────────────
|
|
798
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
799
|
+
const opts = parseArgs(rest);
|
|
800
|
+
|
|
801
|
+
switch (cmd) {
|
|
802
|
+
case "check":
|
|
803
|
+
cmdCheck(opts);
|
|
804
|
+
break;
|
|
805
|
+
case "transition":
|
|
806
|
+
cmdTransition(opts);
|
|
807
|
+
break;
|
|
808
|
+
case "init":
|
|
809
|
+
cmdInit(opts);
|
|
810
|
+
break;
|
|
811
|
+
case "fix":
|
|
812
|
+
cmdFix(opts);
|
|
813
|
+
break;
|
|
814
|
+
case "validate-plan":
|
|
815
|
+
cmdValidatePlan(opts);
|
|
816
|
+
break;
|
|
817
|
+
default:
|
|
818
|
+
output(
|
|
819
|
+
fail(
|
|
820
|
+
"UNKNOWN_COMMAND",
|
|
821
|
+
`Usage: state.js <check|transition|init|fix|validate-plan> [--options]`
|
|
822
|
+
)
|
|
823
|
+
);
|
|
824
|
+
}
|