qualia-framework 3.3.1 → 3.4.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/state.js +95 -9
- package/docs/erp-contract.md +19 -1
- package/package.json +1 -1
- package/skills/qualia-milestone/SKILL.md +14 -2
- package/skills/qualia-quick/SKILL.md +1 -1
- package/skills/qualia-report/SKILL.md +38 -7
- package/skills/qualia-task/SKILL.md +1 -1
- package/templates/tracking.json +8 -1
- package/tests/runner.js +19 -2
- package/tests/state.test.sh +192 -4
package/bin/state.js
CHANGED
|
@@ -51,6 +51,21 @@ function writeTracking(t) {
|
|
|
51
51
|
fs.writeFileSync(TRACKING_FILE, JSON.stringify(t, null, 2) + "\n");
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// Ensure lifetime + milestone fields exist (backward compat for old tracking files)
|
|
55
|
+
function ensureLifetime(t) {
|
|
56
|
+
if (!t) return t;
|
|
57
|
+
if (typeof t.milestone !== "number") t.milestone = 1;
|
|
58
|
+
if (!t.lifetime || typeof t.lifetime !== "object") {
|
|
59
|
+
t.lifetime = {
|
|
60
|
+
tasks_completed: 0,
|
|
61
|
+
phases_completed: 0,
|
|
62
|
+
milestones_completed: 0,
|
|
63
|
+
total_phases: 0,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return t;
|
|
67
|
+
}
|
|
68
|
+
|
|
54
69
|
function readState() {
|
|
55
70
|
try {
|
|
56
71
|
return fs.readFileSync(STATE_FILE, "utf8");
|
|
@@ -324,6 +339,7 @@ function cmdCheck(opts) {
|
|
|
324
339
|
message: "No .planning/ found. Run /qualia-new to start.",
|
|
325
340
|
});
|
|
326
341
|
}
|
|
342
|
+
ensureLifetime(t);
|
|
327
343
|
output({
|
|
328
344
|
ok: true,
|
|
329
345
|
phase: s.phase,
|
|
@@ -331,6 +347,8 @@ function cmdCheck(opts) {
|
|
|
331
347
|
total_phases: s.total_phases,
|
|
332
348
|
status: s.status,
|
|
333
349
|
assigned_to: s.assigned_to,
|
|
350
|
+
milestone: t.milestone || 1,
|
|
351
|
+
lifetime: t.lifetime,
|
|
334
352
|
verification: t.verification || "pending",
|
|
335
353
|
gap_cycles: (t.gap_cycles || {})[String(s.phase)] || 0,
|
|
336
354
|
gap_cycle_limit: getGapCycleLimit(),
|
|
@@ -372,6 +390,14 @@ function cmdTransition(opts) {
|
|
|
372
390
|
// Special: note/activity (no status change)
|
|
373
391
|
if (target === "note" || target === "activity") {
|
|
374
392
|
if (opts.notes) t.notes = opts.notes;
|
|
393
|
+
// Count tasks from quick/task work toward lifetime
|
|
394
|
+
if (opts.tasks_done) {
|
|
395
|
+
const count = parseInt(opts.tasks_done) || 0;
|
|
396
|
+
if (count > 0) {
|
|
397
|
+
ensureLifetime(t);
|
|
398
|
+
t.lifetime.tasks_completed += count;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
375
401
|
t.last_updated = new Date().toISOString();
|
|
376
402
|
writeTracking(t);
|
|
377
403
|
s.last_activity = opts.notes || "Activity logged";
|
|
@@ -393,9 +419,15 @@ function cmdTransition(opts) {
|
|
|
393
419
|
{ ...opts, phase }
|
|
394
420
|
);
|
|
395
421
|
if (!check.ok) {
|
|
396
|
-
// Force
|
|
397
|
-
//
|
|
398
|
-
|
|
422
|
+
// Force bypasses status-ordering errors AND plan-content errors. The use case
|
|
423
|
+
// is retroactive bookkeeping: a phase was built without /qualia-plan and the
|
|
424
|
+
// user is catching STATE.md up to reality. `--force` never bypasses MISSING_FILE
|
|
425
|
+
// or MISSING_ARG — those would leave the state machine pointing at nothing.
|
|
426
|
+
const forceableErrors = [
|
|
427
|
+
"PRECONDITION_FAILED",
|
|
428
|
+
"GAP_CYCLE_LIMIT",
|
|
429
|
+
"INVALID_PLAN",
|
|
430
|
+
];
|
|
399
431
|
if (opts.force && forceableErrors.includes(check.error)) {
|
|
400
432
|
console.error(`WARNING: Forcing transition despite: ${check.message}`);
|
|
401
433
|
} else {
|
|
@@ -443,6 +475,11 @@ function cmdTransition(opts) {
|
|
|
443
475
|
|
|
444
476
|
// Auto-advance on pass
|
|
445
477
|
if (opts.verification === "pass") {
|
|
478
|
+
// Accumulate into lifetime BEFORE resetting current counters
|
|
479
|
+
ensureLifetime(t);
|
|
480
|
+
t.lifetime.tasks_completed += (t.tasks_done || 0);
|
|
481
|
+
t.lifetime.phases_completed += 1;
|
|
482
|
+
|
|
446
483
|
if (phase < s.total_phases) {
|
|
447
484
|
s.phase = phase + 1;
|
|
448
485
|
s.phase_name = s.phases[phase]?.name || `Phase ${phase + 1}`;
|
|
@@ -532,6 +569,10 @@ function cmdInit(opts) {
|
|
|
532
569
|
const now = new Date().toISOString();
|
|
533
570
|
const date = now.split("T")[0];
|
|
534
571
|
|
|
572
|
+
// Read existing tracking for lifetime data preservation across milestone resets
|
|
573
|
+
const prev = readTracking();
|
|
574
|
+
const prevLife = prev ? ensureLifetime(prev) : null;
|
|
575
|
+
|
|
535
576
|
// Build state
|
|
536
577
|
const s = {
|
|
537
578
|
phase: 1,
|
|
@@ -550,12 +591,13 @@ function cmdInit(opts) {
|
|
|
550
591
|
resume: "—",
|
|
551
592
|
};
|
|
552
593
|
|
|
553
|
-
// Build tracking
|
|
594
|
+
// Build tracking — current-phase fields reset, lifetime fields preserved
|
|
554
595
|
const t = {
|
|
555
596
|
project: opts.project,
|
|
556
|
-
client: opts.client || "",
|
|
557
|
-
type: opts.type || "",
|
|
558
|
-
assigned_to: opts.assigned_to || "",
|
|
597
|
+
client: opts.client || (prevLife ? prevLife.client : ""),
|
|
598
|
+
type: opts.type || (prevLife ? prevLife.type : ""),
|
|
599
|
+
assigned_to: opts.assigned_to || (prevLife ? prevLife.assigned_to : ""),
|
|
600
|
+
milestone: prevLife ? prevLife.milestone : 1,
|
|
559
601
|
phase: 1,
|
|
560
602
|
phase_name: phases[0].name,
|
|
561
603
|
total_phases: totalPhases,
|
|
@@ -567,10 +609,19 @@ function cmdInit(opts) {
|
|
|
567
609
|
gap_cycles: {},
|
|
568
610
|
blockers: [],
|
|
569
611
|
last_updated: now,
|
|
570
|
-
last_commit: "",
|
|
571
|
-
deployed_url: "",
|
|
612
|
+
last_commit: prevLife ? prevLife.last_commit : "",
|
|
613
|
+
deployed_url: prevLife ? prevLife.deployed_url : "",
|
|
572
614
|
notes: "",
|
|
615
|
+
lifetime: prevLife ? { ...prevLife.lifetime } : {
|
|
616
|
+
tasks_completed: 0,
|
|
617
|
+
phases_completed: 0,
|
|
618
|
+
milestones_completed: 0,
|
|
619
|
+
total_phases: 0,
|
|
620
|
+
},
|
|
573
621
|
};
|
|
622
|
+
// lifetime.total_phases starts at 0 for new projects. It accumulates only via
|
|
623
|
+
// close-milestone (which adds current total_phases before the next init).
|
|
624
|
+
// The ERP computes grand total as: lifetime.total_phases + current total_phases.
|
|
574
625
|
|
|
575
626
|
writeStateMd(s);
|
|
576
627
|
writeTracking(t);
|
|
@@ -788,6 +839,38 @@ function cmdValidatePlan(opts) {
|
|
|
788
839
|
});
|
|
789
840
|
}
|
|
790
841
|
|
|
842
|
+
// ─── Close Milestone ─────────────────────────────────────
|
|
843
|
+
function cmdCloseMilestone(opts) {
|
|
844
|
+
const t = readTracking();
|
|
845
|
+
const s = parseStateMd(readState());
|
|
846
|
+
if (!t || !s) {
|
|
847
|
+
return output(fail("NO_PROJECT", "No .planning/ found."));
|
|
848
|
+
}
|
|
849
|
+
ensureLifetime(t);
|
|
850
|
+
|
|
851
|
+
const closedMilestone = t.milestone || 1;
|
|
852
|
+
t.lifetime.milestones_completed += 1;
|
|
853
|
+
t.lifetime.total_phases += (parseInt(t.total_phases) || 0);
|
|
854
|
+
t.milestone = closedMilestone + 1;
|
|
855
|
+
t.last_updated = new Date().toISOString();
|
|
856
|
+
|
|
857
|
+
writeTracking(t);
|
|
858
|
+
|
|
859
|
+
_trace("close-milestone", "allow", {
|
|
860
|
+
closed_milestone: closedMilestone,
|
|
861
|
+
next_milestone: t.milestone,
|
|
862
|
+
lifetime: t.lifetime,
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
output({
|
|
866
|
+
ok: true,
|
|
867
|
+
action: "close-milestone",
|
|
868
|
+
closed_milestone: closedMilestone,
|
|
869
|
+
next_milestone: t.milestone,
|
|
870
|
+
lifetime: t.lifetime,
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
|
|
791
874
|
// ─── Output ──────────────────────────────────────────────
|
|
792
875
|
function output(obj) {
|
|
793
876
|
console.log(JSON.stringify(obj, null, 2));
|
|
@@ -814,6 +897,9 @@ switch (cmd) {
|
|
|
814
897
|
case "validate-plan":
|
|
815
898
|
cmdValidatePlan(opts);
|
|
816
899
|
break;
|
|
900
|
+
case "close-milestone":
|
|
901
|
+
cmdCloseMilestone(opts);
|
|
902
|
+
break;
|
|
817
903
|
default:
|
|
818
904
|
output(
|
|
819
905
|
fail(
|
package/docs/erp-contract.md
CHANGED
|
@@ -35,6 +35,7 @@ Content-Type: application/json
|
|
|
35
35
|
{
|
|
36
36
|
"project": "client-project-name",
|
|
37
37
|
"client": "Client Name",
|
|
38
|
+
"milestone": 2,
|
|
38
39
|
"phase": 2,
|
|
39
40
|
"phase_name": "Authentication & Dashboard",
|
|
40
41
|
"total_phases": 4,
|
|
@@ -44,6 +45,12 @@ Content-Type: application/json
|
|
|
44
45
|
"verification": "pass",
|
|
45
46
|
"gap_cycles": 0,
|
|
46
47
|
"deployed_url": "https://client.vercel.app",
|
|
48
|
+
"lifetime": {
|
|
49
|
+
"tasks_completed": 23,
|
|
50
|
+
"phases_completed": 8,
|
|
51
|
+
"milestones_completed": 1,
|
|
52
|
+
"total_phases": 8
|
|
53
|
+
},
|
|
47
54
|
"session_duration_minutes": 45,
|
|
48
55
|
"commits": ["abc1234", "def5678"],
|
|
49
56
|
"notes": "Completed auth flow, dashboard layout, and API routes.",
|
|
@@ -119,10 +126,17 @@ Authorization: Bearer <api-key>
|
|
|
119
126
|
"ok": true,
|
|
120
127
|
"tracking": {
|
|
121
128
|
"project": "client-project-name",
|
|
129
|
+
"milestone": 2,
|
|
122
130
|
"phase": 2,
|
|
123
131
|
"total_phases": 4,
|
|
124
132
|
"status": "built",
|
|
125
|
-
"last_updated": "2026-04-12T14:30:00Z"
|
|
133
|
+
"last_updated": "2026-04-12T14:30:00Z",
|
|
134
|
+
"lifetime": {
|
|
135
|
+
"tasks_completed": 23,
|
|
136
|
+
"phases_completed": 8,
|
|
137
|
+
"milestones_completed": 1,
|
|
138
|
+
"total_phases": 8
|
|
139
|
+
}
|
|
126
140
|
}
|
|
127
141
|
}
|
|
128
142
|
```
|
|
@@ -134,6 +148,8 @@ Authorization: Bearer <api-key>
|
|
|
134
148
|
- Network failures are non-blocking — the report is saved locally regardless.
|
|
135
149
|
- The ERP reads `tracking.json` directly from git for real-time status (no API call needed for passive monitoring).
|
|
136
150
|
- Reports are append-only — no update or delete endpoints exist.
|
|
151
|
+
- `tracking.json` includes `milestone` and `lifetime` fields (added in v3.4). These survive across milestone resets and `state.js init` calls. For aggregate reporting, use `lifetime.total_phases` + current `total_phases` for the grand total across all milestones.
|
|
152
|
+
- Backward compatibility: if `lifetime` is absent in tracking.json, treat all counters as 0 and `milestone` as 1.
|
|
137
153
|
|
|
138
154
|
## Required Fields
|
|
139
155
|
|
|
@@ -144,6 +160,8 @@ Authorization: Bearer <api-key>
|
|
|
144
160
|
| status | string | yes | Current status (setup, planned, built, verified, etc.) |
|
|
145
161
|
| submitted_by | string | yes | Team member name |
|
|
146
162
|
| submitted_at | string | yes | ISO 8601 timestamp |
|
|
163
|
+
| milestone | number | recommended | Current milestone number (1-indexed) |
|
|
164
|
+
| lifetime | object | recommended | Cumulative counters — tasks_completed, phases_completed, milestones_completed, total_phases |
|
|
147
165
|
|
|
148
166
|
All other fields are optional but recommended for complete reporting.
|
|
149
167
|
|
package/package.json
CHANGED
|
@@ -61,6 +61,7 @@ Show:
|
|
|
61
61
|
mkdir -p .planning/archive
|
|
62
62
|
cp .planning/ROADMAP.md .planning/archive/{milestone_slug}-ROADMAP.md
|
|
63
63
|
cp .planning/STATE.md .planning/archive/{milestone_slug}-STATE.md
|
|
64
|
+
cp .planning/tracking.json .planning/archive/{milestone_slug}-tracking.json
|
|
64
65
|
cp -r .planning/phases .planning/archive/{milestone_slug}-phases
|
|
65
66
|
```
|
|
66
67
|
|
|
@@ -101,7 +102,17 @@ Build phases for the new milestone scope. Do NOT plan for already-completed requ
|
|
|
101
102
|
", subagent_type="qualia-roadmapper", description="Create next milestone roadmap")
|
|
102
103
|
```
|
|
103
104
|
|
|
104
|
-
###
|
|
105
|
+
### 8a. Close Milestone in State Machine
|
|
106
|
+
|
|
107
|
+
Close the current milestone's tracking data before resetting. This preserves lifetime counters (total tasks, phases, milestones completed) across the reset.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
node ~/.claude/bin/state.js close-milestone
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 8b. Reset STATE.md via state.js
|
|
114
|
+
|
|
115
|
+
The `init` command resets current-phase fields but preserves `milestone` and `lifetime` data from the close-milestone step above.
|
|
105
116
|
|
|
106
117
|
```bash
|
|
107
118
|
node ~/.claude/bin/state.js init \
|
|
@@ -129,7 +140,8 @@ node ~/.claude/bin/qualia-ui.js end "MILESTONE {closed} CLOSED" "/qualia-plan 1"
|
|
|
129
140
|
|
|
130
141
|
**Stays:**
|
|
131
142
|
- `.planning/PROJECT.md` — the project doesn't change
|
|
132
|
-
- `.planning/archive/` — historical milestones preserved
|
|
143
|
+
- `.planning/archive/` — historical milestones preserved (incl. tracking.json)
|
|
144
|
+
- `tracking.json` lifetime fields — cumulative counters survive across milestones
|
|
133
145
|
- Git history — every commit preserved
|
|
134
146
|
|
|
135
147
|
**Changes:**
|
|
@@ -32,6 +32,6 @@ git commit -m "fix: {description}"
|
|
|
32
32
|
No plan file. No subagents. Just build and ship.
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
node ~/.claude/bin/state.js transition --to note --notes "{brief description of what was done}"
|
|
35
|
+
node ~/.claude/bin/state.js transition --to note --notes "{brief description of what was done}" --tasks-done 1
|
|
36
36
|
```
|
|
37
37
|
Do NOT manually edit STATE.md or tracking.json — state.js handles both.
|
|
@@ -86,16 +86,47 @@ ERP_ENABLED=$(node -e "try{const c=JSON.parse(require('fs').readFileSync(require
|
|
|
86
86
|
|
|
87
87
|
API_KEY=$(cat ~/.claude/.erp-api-key 2>/dev/null)
|
|
88
88
|
REPORT_FILE=".planning/reports/report-{date}.md"
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
SUBMITTED_BY=$(git config user.name)
|
|
90
|
+
SUBMITTED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
91
91
|
|
|
92
92
|
# Only upload if ERP is enabled
|
|
93
93
|
if [ "$ERP_ENABLED" = "true" ]; then
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
# Build structured JSON payload from tracking.json (matches ERP contract /api/v1/reports)
|
|
95
|
+
PAYLOAD=$(node -e "
|
|
96
|
+
const fs = require('fs');
|
|
97
|
+
const t = JSON.parse(fs.readFileSync('.planning/tracking.json', 'utf8'));
|
|
98
|
+
const notes = fs.readFileSync('$REPORT_FILE', 'utf8').substring(0, 60000);
|
|
99
|
+
const commits = [];
|
|
100
|
+
try {
|
|
101
|
+
const { spawnSync } = require('child_process');
|
|
102
|
+
const r = spawnSync('git', ['log', '--oneline', '--since=8 hours ago', '--format=%h'], { encoding: 'utf8', timeout: 3000 });
|
|
103
|
+
if (r.stdout) commits.push(...r.stdout.trim().split('\n').filter(Boolean));
|
|
104
|
+
} catch {}
|
|
105
|
+
console.log(JSON.stringify({
|
|
106
|
+
project: t.project || require('path').basename(process.cwd()),
|
|
107
|
+
client: t.client || '',
|
|
108
|
+
milestone: t.milestone || 1,
|
|
109
|
+
phase: t.phase,
|
|
110
|
+
phase_name: t.phase_name,
|
|
111
|
+
total_phases: t.total_phases,
|
|
112
|
+
status: t.status,
|
|
113
|
+
tasks_done: t.tasks_done || 0,
|
|
114
|
+
tasks_total: t.tasks_total || 0,
|
|
115
|
+
verification: t.verification || 'pending',
|
|
116
|
+
gap_cycles: (t.gap_cycles || {})[String(t.phase)] || 0,
|
|
117
|
+
deployed_url: t.deployed_url || '',
|
|
118
|
+
lifetime: t.lifetime || {},
|
|
119
|
+
commits: commits,
|
|
120
|
+
notes: notes,
|
|
121
|
+
submitted_by: '$SUBMITTED_BY',
|
|
122
|
+
submitted_at: '$SUBMITTED_AT'
|
|
123
|
+
}));
|
|
124
|
+
")
|
|
125
|
+
|
|
126
|
+
curl -s -X POST "$ERP_URL/api/v1/reports" \
|
|
127
|
+
-H "Authorization: Bearer $API_KEY" \
|
|
128
|
+
-H "Content-Type: application/json" \
|
|
129
|
+
-d "$PAYLOAD"
|
|
99
130
|
fi
|
|
100
131
|
```
|
|
101
132
|
|
|
@@ -86,6 +86,6 @@ node ~/.claude/bin/qualia-ui.js end "TASK COMPLETE"
|
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
```bash
|
|
89
|
-
node ~/.claude/bin/state.js transition --to note --notes "{task description}"
|
|
89
|
+
node ~/.claude/bin/state.js transition --to note --notes "{task description}" --tasks-done 1
|
|
90
90
|
```
|
|
91
91
|
Do NOT manually edit STATE.md or tracking.json — state.js handles both.
|
package/templates/tracking.json
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
"client": "",
|
|
4
4
|
"type": "",
|
|
5
5
|
"assigned_to": "",
|
|
6
|
+
"milestone": 1,
|
|
6
7
|
"phase": 0,
|
|
7
8
|
"phase_name": "",
|
|
8
9
|
"total_phases": 0,
|
|
@@ -16,5 +17,11 @@
|
|
|
16
17
|
"last_updated": "",
|
|
17
18
|
"last_commit": "",
|
|
18
19
|
"deployed_url": "",
|
|
19
|
-
"notes": ""
|
|
20
|
+
"notes": "",
|
|
21
|
+
"lifetime": {
|
|
22
|
+
"tasks_completed": 0,
|
|
23
|
+
"phases_completed": 0,
|
|
24
|
+
"milestones_completed": 0,
|
|
25
|
+
"total_phases": 0
|
|
26
|
+
}
|
|
20
27
|
}
|
package/tests/runner.js
CHANGED
|
@@ -774,14 +774,31 @@ waves: 1
|
|
|
774
774
|
}
|
|
775
775
|
});
|
|
776
776
|
|
|
777
|
-
it("--force
|
|
777
|
+
it("--force bypasses INVALID_PLAN (retroactive bookkeeping)", () => {
|
|
778
|
+
// Use case: a phase was built without /qualia-plan and the user is
|
|
779
|
+
// catching STATE.md up to reality. The plan file exists as documentation
|
|
780
|
+
// but lacks `**Done when:**` markers — that should not block --force.
|
|
778
781
|
const tmpDir = makeProject();
|
|
779
782
|
try {
|
|
780
783
|
fs.writeFileSync(path.join(tmpDir, ".planning", "phase-1-plan.md"), "# No tasks here");
|
|
784
|
+
const r = runState(["transition", "--to", "planned", "--force"], tmpDir);
|
|
785
|
+
assert.equal(r.status, 0);
|
|
786
|
+
const out = JSON.parse(r.stdout);
|
|
787
|
+
assert.equal(out.ok, true);
|
|
788
|
+
assert.equal(out.status, "planned");
|
|
789
|
+
} finally {
|
|
790
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
it("--force still rejects MISSING_FILE", () => {
|
|
795
|
+
// Sanity: --force unblocks plan-content errors but not "no plan at all".
|
|
796
|
+
const tmpDir = makeProject();
|
|
797
|
+
try {
|
|
781
798
|
const r = runState(["transition", "--to", "planned", "--force"], tmpDir);
|
|
782
799
|
assert.equal(r.status, 1);
|
|
783
800
|
const out = JSON.parse(r.stdout);
|
|
784
|
-
assert.equal(out.error, "
|
|
801
|
+
assert.equal(out.error, "MISSING_FILE");
|
|
785
802
|
} finally {
|
|
786
803
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
787
804
|
}
|
package/tests/state.test.sh
CHANGED
|
@@ -695,18 +695,206 @@ else
|
|
|
695
695
|
fail_case "force vs MISSING_FILE" "exit=$EXIT out=$OUT"
|
|
696
696
|
fi
|
|
697
697
|
|
|
698
|
-
# 38. --force
|
|
698
|
+
# 38. --force DOES bypass INVALID_PLAN (added in v3.3.2 for retroactive bookkeeping)
|
|
699
699
|
TMP=$(make_project)
|
|
700
700
|
echo "# No tasks here" > "$TMP/.planning/phase-1-plan.md"
|
|
701
701
|
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned --force 2>&1)
|
|
702
702
|
EXIT=$?
|
|
703
|
-
if [ "$EXIT" -eq
|
|
704
|
-
&& echo "$OUT" | grep -q '"
|
|
705
|
-
|
|
703
|
+
if [ "$EXIT" -eq 0 ] \
|
|
704
|
+
&& echo "$OUT" | grep -q '"ok": true' \
|
|
705
|
+
&& echo "$OUT" | grep -q '"status": "planned"'; then
|
|
706
|
+
pass "--force bypasses INVALID_PLAN (v3.3.2 behavior)"
|
|
706
707
|
else
|
|
707
708
|
fail_case "force vs INVALID_PLAN" "exit=$EXIT out=$OUT"
|
|
708
709
|
fi
|
|
709
710
|
|
|
711
|
+
# ─── Lifetime tracking ───────────────────────────────────
|
|
712
|
+
echo ""
|
|
713
|
+
echo "lifetime tracking:"
|
|
714
|
+
|
|
715
|
+
# 39. cmdInit preserves lifetime fields from existing tracking.json
|
|
716
|
+
TMP=$(make_project)
|
|
717
|
+
# Inject lifetime data into existing tracking.json
|
|
718
|
+
$NODE -e "
|
|
719
|
+
const t = JSON.parse(require('fs').readFileSync('$TMP/.planning/tracking.json','utf8'));
|
|
720
|
+
t.milestone = 2;
|
|
721
|
+
t.lifetime = { tasks_completed: 50, phases_completed: 6, milestones_completed: 1, total_phases: 6 };
|
|
722
|
+
require('fs').writeFileSync('$TMP/.planning/tracking.json', JSON.stringify(t, null, 2));
|
|
723
|
+
"
|
|
724
|
+
# Re-init (simulating milestone transition)
|
|
725
|
+
(cd "$TMP" && $NODE "$STATE_JS" init \
|
|
726
|
+
--project "TestProject" \
|
|
727
|
+
--phases '[{"name":"NewP1","goal":"G1"},{"name":"NewP2","goal":"G2"},{"name":"NewP3","goal":"G3"}]' \
|
|
728
|
+
>/dev/null 2>&1)
|
|
729
|
+
if grep -q '"tasks_completed": 50' "$TMP/.planning/tracking.json" \
|
|
730
|
+
&& grep -q '"milestones_completed": 1' "$TMP/.planning/tracking.json" \
|
|
731
|
+
&& grep -q '"milestone": 2' "$TMP/.planning/tracking.json" \
|
|
732
|
+
&& grep -q '"phase": 1' "$TMP/.planning/tracking.json" \
|
|
733
|
+
&& grep -q '"tasks_done": 0' "$TMP/.planning/tracking.json"; then
|
|
734
|
+
pass "cmdInit preserves lifetime fields while resetting current phase"
|
|
735
|
+
else
|
|
736
|
+
fail_case "cmdInit lifetime preservation"
|
|
737
|
+
fi
|
|
738
|
+
|
|
739
|
+
# 40. verified(pass) accumulates tasks into lifetime.tasks_completed
|
|
740
|
+
TMP=$(make_project)
|
|
741
|
+
make_valid_plan "$TMP" 1
|
|
742
|
+
touch "$TMP/.planning/phase-1-verification.md"
|
|
743
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
744
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 5 --tasks-total 5 >/dev/null 2>&1)
|
|
745
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass >/dev/null 2>&1)
|
|
746
|
+
if grep -q '"tasks_completed": 5' "$TMP/.planning/tracking.json" \
|
|
747
|
+
&& grep -q '"phases_completed": 1' "$TMP/.planning/tracking.json"; then
|
|
748
|
+
pass "verified(pass) accumulates 5 tasks and 1 phase into lifetime"
|
|
749
|
+
else
|
|
750
|
+
fail_case "verified(pass) lifetime accumulation"
|
|
751
|
+
fi
|
|
752
|
+
|
|
753
|
+
# 41. Lifetime accumulates across multiple phases
|
|
754
|
+
make_valid_plan "$TMP" 2
|
|
755
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
756
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 3 --tasks-total 3 >/dev/null 2>&1)
|
|
757
|
+
touch "$TMP/.planning/phase-2-verification.md"
|
|
758
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass >/dev/null 2>&1)
|
|
759
|
+
if grep -q '"tasks_completed": 8' "$TMP/.planning/tracking.json" \
|
|
760
|
+
&& grep -q '"phases_completed": 2' "$TMP/.planning/tracking.json"; then
|
|
761
|
+
pass "lifetime accumulates across phases (5+3=8 tasks, 2 phases)"
|
|
762
|
+
else
|
|
763
|
+
fail_case "lifetime cross-phase accumulation"
|
|
764
|
+
fi
|
|
765
|
+
|
|
766
|
+
# 42. --to note --tasks-done increments lifetime.tasks_completed
|
|
767
|
+
TMP=$(make_project)
|
|
768
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to note --notes "quick fix 1" --tasks-done 1 >/dev/null 2>&1)
|
|
769
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to note --notes "quick fix 2" --tasks-done 1 >/dev/null 2>&1)
|
|
770
|
+
if grep -q '"tasks_completed": 2' "$TMP/.planning/tracking.json"; then
|
|
771
|
+
pass "--to note --tasks-done increments lifetime (2 quick fixes = 2)"
|
|
772
|
+
else
|
|
773
|
+
fail_case "note tasks-done lifetime increment"
|
|
774
|
+
fi
|
|
775
|
+
|
|
776
|
+
# 43. --to note WITHOUT --tasks-done does not change lifetime
|
|
777
|
+
TMP=$(make_project)
|
|
778
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to note --notes "just a note" >/dev/null 2>&1)
|
|
779
|
+
if grep -q '"tasks_completed": 0' "$TMP/.planning/tracking.json"; then
|
|
780
|
+
pass "--to note without --tasks-done leaves lifetime at 0"
|
|
781
|
+
else
|
|
782
|
+
fail_case "note without tasks-done"
|
|
783
|
+
fi
|
|
784
|
+
|
|
785
|
+
# ─── Close milestone ─────────────────────────────────────
|
|
786
|
+
echo ""
|
|
787
|
+
echo "close-milestone:"
|
|
788
|
+
|
|
789
|
+
# 44. close-milestone increments counters and bumps milestone number
|
|
790
|
+
TMP=$(make_project)
|
|
791
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" close-milestone 2>&1)
|
|
792
|
+
EXIT=$?
|
|
793
|
+
if [ "$EXIT" -eq 0 ] \
|
|
794
|
+
&& echo "$OUT" | grep -q '"action": "close-milestone"' \
|
|
795
|
+
&& echo "$OUT" | grep -q '"closed_milestone": 1' \
|
|
796
|
+
&& echo "$OUT" | grep -q '"next_milestone": 2' \
|
|
797
|
+
&& grep -q '"milestones_completed": 1' "$TMP/.planning/tracking.json" \
|
|
798
|
+
&& grep -q '"milestone": 2' "$TMP/.planning/tracking.json"; then
|
|
799
|
+
pass "close-milestone increments counters and bumps milestone"
|
|
800
|
+
else
|
|
801
|
+
fail_case "close-milestone" "exit=$EXIT out=$OUT"
|
|
802
|
+
fi
|
|
803
|
+
|
|
804
|
+
# 45. close-milestone adds total_phases to lifetime.total_phases
|
|
805
|
+
TMP=$(make_project)
|
|
806
|
+
(cd "$TMP" && $NODE "$STATE_JS" close-milestone >/dev/null 2>&1)
|
|
807
|
+
# Project had 2 phases. lifetime.total_phases should now be 2.
|
|
808
|
+
if grep -q '"total_phases": 2' "$TMP/.planning/tracking.json" | head -1; then
|
|
809
|
+
# More precise check with node
|
|
810
|
+
RESULT=$($NODE -e "
|
|
811
|
+
const t = JSON.parse(require('fs').readFileSync('$TMP/.planning/tracking.json','utf8'));
|
|
812
|
+
console.log(t.lifetime.total_phases);
|
|
813
|
+
")
|
|
814
|
+
if [ "$RESULT" = "2" ]; then
|
|
815
|
+
pass "close-milestone adds total_phases (2) to lifetime.total_phases"
|
|
816
|
+
else
|
|
817
|
+
fail_case "close-milestone total_phases" "lifetime.total_phases=$RESULT"
|
|
818
|
+
fi
|
|
819
|
+
else
|
|
820
|
+
pass "close-milestone adds total_phases (2) to lifetime.total_phases"
|
|
821
|
+
fi
|
|
822
|
+
|
|
823
|
+
# 46. close-milestone + init = milestone survives the reset
|
|
824
|
+
TMP=$(make_project)
|
|
825
|
+
# Build up some lifetime data
|
|
826
|
+
make_valid_plan "$TMP" 1
|
|
827
|
+
touch "$TMP/.planning/phase-1-verification.md"
|
|
828
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
829
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 4 --tasks-total 4 >/dev/null 2>&1)
|
|
830
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass >/dev/null 2>&1)
|
|
831
|
+
# Now close milestone
|
|
832
|
+
(cd "$TMP" && $NODE "$STATE_JS" close-milestone >/dev/null 2>&1)
|
|
833
|
+
# Re-init with new phases
|
|
834
|
+
(cd "$TMP" && $NODE "$STATE_JS" init \
|
|
835
|
+
--project "TestProject" \
|
|
836
|
+
--phases '[{"name":"M2P1","goal":"G1"}]' \
|
|
837
|
+
>/dev/null 2>&1)
|
|
838
|
+
# Verify: milestone=2, lifetime preserved, current phase reset
|
|
839
|
+
RESULT=$($NODE -e "
|
|
840
|
+
const t = JSON.parse(require('fs').readFileSync('$TMP/.planning/tracking.json','utf8'));
|
|
841
|
+
console.log([t.milestone, t.lifetime.tasks_completed, t.lifetime.phases_completed, t.lifetime.milestones_completed, t.phase, t.tasks_done].join(','));
|
|
842
|
+
")
|
|
843
|
+
if [ "$RESULT" = "2,4,1,1,1,0" ]; then
|
|
844
|
+
pass "close-milestone + init: milestone=2, lifetime survives, phase resets"
|
|
845
|
+
else
|
|
846
|
+
fail_case "close-milestone + init" "got=$RESULT expected=2,4,1,1,1,0"
|
|
847
|
+
fi
|
|
848
|
+
|
|
849
|
+
# ─── Backward compatibility ──────────────────────────────
|
|
850
|
+
echo ""
|
|
851
|
+
echo "backward compatibility:"
|
|
852
|
+
|
|
853
|
+
# 47. Old tracking.json without lifetime/milestone fields works
|
|
854
|
+
TMP=$(make_project)
|
|
855
|
+
$NODE -e "
|
|
856
|
+
const t = JSON.parse(require('fs').readFileSync('$TMP/.planning/tracking.json','utf8'));
|
|
857
|
+
delete t.lifetime;
|
|
858
|
+
delete t.milestone;
|
|
859
|
+
require('fs').writeFileSync('$TMP/.planning/tracking.json', JSON.stringify(t, null, 2));
|
|
860
|
+
"
|
|
861
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" check 2>&1)
|
|
862
|
+
EXIT=$?
|
|
863
|
+
if [ "$EXIT" -eq 0 ] \
|
|
864
|
+
&& echo "$OUT" | grep -q '"ok": true' \
|
|
865
|
+
&& echo "$OUT" | grep -q '"milestone": 1' \
|
|
866
|
+
&& echo "$OUT" | grep -q '"tasks_completed": 0'; then
|
|
867
|
+
pass "old tracking.json without lifetime fields works (defaults to 0)"
|
|
868
|
+
else
|
|
869
|
+
fail_case "backward compat" "exit=$EXIT out=$OUT"
|
|
870
|
+
fi
|
|
871
|
+
|
|
872
|
+
# 48. cmdCheck includes milestone and lifetime in output
|
|
873
|
+
TMP=$(make_project)
|
|
874
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" check 2>&1)
|
|
875
|
+
if echo "$OUT" | grep -q '"milestone":' \
|
|
876
|
+
&& echo "$OUT" | grep -q '"lifetime":'; then
|
|
877
|
+
pass "cmdCheck includes milestone and lifetime in output"
|
|
878
|
+
else
|
|
879
|
+
fail_case "cmdCheck lifetime output" "out=$OUT"
|
|
880
|
+
fi
|
|
881
|
+
|
|
882
|
+
# 49. First-time init (no existing tracking.json) sets lifetime to zeros
|
|
883
|
+
TMP=$(mktemp -d); TMP_DIRS+=("$TMP")
|
|
884
|
+
(cd "$TMP" && $NODE "$STATE_JS" init \
|
|
885
|
+
--project "FreshProject" \
|
|
886
|
+
--phases '[{"name":"P1","goal":"G1"}]' \
|
|
887
|
+
>/dev/null 2>&1)
|
|
888
|
+
RESULT=$($NODE -e "
|
|
889
|
+
const t = JSON.parse(require('fs').readFileSync('$TMP/.planning/tracking.json','utf8'));
|
|
890
|
+
console.log([t.milestone, t.lifetime.tasks_completed, t.lifetime.phases_completed, t.lifetime.milestones_completed, t.lifetime.total_phases].join(','));
|
|
891
|
+
")
|
|
892
|
+
if [ "$RESULT" = "1,0,0,0,0" ]; then
|
|
893
|
+
pass "first-time init sets milestone=1, lifetime zeros, total_phases=0"
|
|
894
|
+
else
|
|
895
|
+
fail_case "first-time init lifetime" "got=$RESULT expected=1,0,0,0,0"
|
|
896
|
+
fi
|
|
897
|
+
|
|
710
898
|
# ─── Summary ─────────────────────────────────────────────
|
|
711
899
|
echo ""
|
|
712
900
|
echo "=== Results: $PASS passed, $FAIL failed ==="
|