create-harness-vibe-coding 0.8.13 → 0.8.17

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.
Files changed (133) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/README-CN.md +110 -107
  3. package/README.md +93 -44
  4. package/package.json +2 -1
  5. package/src/generator.js +609 -489
  6. package/src/index.js +5 -5
  7. package/templates/common/.claude/agents/architect-manager.md +1 -0
  8. package/templates/common/.claude/agents/architect.md +2 -1
  9. package/templates/common/.claude/agents/codebase-explorer.md +1 -0
  10. package/templates/common/.claude/agents/context-master.md +2 -1
  11. package/templates/common/.claude/agents/debugger.md +1 -0
  12. package/templates/common/.claude/agents/docs-researcher.md +2 -1
  13. package/templates/common/.claude/agents/explore-manager.md +1 -0
  14. package/templates/common/.claude/agents/implement-manager.md +1 -0
  15. package/templates/common/.claude/agents/implementer.md +1 -0
  16. package/templates/common/.claude/agents/memory-master.md +2 -1
  17. package/templates/common/.claude/agents/planner.md +3 -2
  18. package/templates/common/.claude/agents/reflector.md +1 -0
  19. package/templates/common/.claude/agents/researcher.md +1 -0
  20. package/templates/common/.claude/agents/review-manager.md +1 -0
  21. package/templates/common/.claude/agents/reviewer.md +2 -1
  22. package/templates/common/.claude/agents/task-scribe.md +1 -0
  23. package/templates/common/.claude/agents/tdd-guide.md +5 -4
  24. package/templates/common/.claude/agents/test-writer.md +6 -5
  25. package/templates/common/.claude/agents/verifier.md +1 -0
  26. package/templates/common/.claude/commands/wf-auto-spark.md +16 -0
  27. package/templates/common/.claude/commands/wf-auto.md +16 -0
  28. package/templates/{optional/skills/browser-e2e/.opencode → common/.claude}/commands/wf-browser.md +3 -2
  29. package/templates/common/.claude/commands/wf-help.md +9 -5
  30. package/templates/common/.claude/commands/wf-learn.md +16 -0
  31. package/templates/common/.claude/commands/wf-max.md +20 -0
  32. package/templates/common/.claude/commands/wf-readme.md +16 -0
  33. package/templates/common/.claude/commands/wf-remove.md +16 -0
  34. package/templates/common/.claude/commands/wf-review.md +16 -0
  35. package/templates/common/.claude/commands/wf-update.md +68 -10
  36. package/templates/common/.claude/commands/wf.md +16 -0
  37. package/templates/common/.claude/rules/ecc/common.md +6 -5
  38. package/templates/common/.claude/skills/subagent-orchestrator/SKILL.md +12 -6
  39. package/templates/common/.claude/skills/tdd/SKILL.md +5 -5
  40. package/templates/common/.claude/skills/wf/SKILL.md +13 -5
  41. package/templates/common/.claude/skills/wf-agents-docs/SKILL.md +119 -0
  42. package/templates/common/.claude/skills/wf-auto/SKILL.md +27 -7
  43. package/templates/common/.claude/skills/wf-auto-spark/SKILL.md +12 -5
  44. package/templates/common/.claude/skills/wf-browser/SKILL.md +176 -0
  45. package/templates/common/.claude/skills/wf-help/SKILL.md +30 -0
  46. package/templates/common/.claude/skills/wf-learn/SKILL.md +6 -0
  47. package/templates/common/.claude/skills/wf-max/SKILL.md +43 -14
  48. package/templates/common/.claude/skills/wf-readme/SKILL.md +8 -2
  49. package/templates/common/.claude/skills/wf-remove/SKILL.md +6 -0
  50. package/templates/common/.claude/skills/wf-review/SKILL.md +10 -3
  51. package/templates/common/.claude/skills/wf-update/SKILL.md +41 -4
  52. package/templates/common/.codex/config.toml +3 -5
  53. package/templates/common/.harness-version +326 -131
  54. package/templates/common/.opencode/agents/architect-manager.md +8 -1
  55. package/templates/common/.opencode/agents/architect.md +2 -1
  56. package/templates/common/.opencode/agents/codebase-explorer.md +1 -0
  57. package/templates/common/.opencode/agents/context-master.md +2 -1
  58. package/templates/common/.opencode/agents/debugger.md +1 -0
  59. package/templates/common/.opencode/agents/docs-researcher.md +2 -1
  60. package/templates/common/.opencode/agents/explore-manager.md +11 -1
  61. package/templates/common/.opencode/agents/implement-manager.md +6 -3
  62. package/templates/common/.opencode/agents/implementer.md +1 -0
  63. package/templates/common/.opencode/agents/memory-master.md +2 -1
  64. package/templates/common/.opencode/agents/planner.md +3 -2
  65. package/templates/common/.opencode/agents/reflector.md +1 -0
  66. package/templates/common/.opencode/agents/researcher.md +1 -0
  67. package/templates/common/.opencode/agents/review-manager.md +6 -2
  68. package/templates/common/.opencode/agents/reviewer.md +2 -1
  69. package/templates/common/.opencode/agents/task-scribe.md +1 -0
  70. package/templates/common/.opencode/agents/tdd-guide.md +5 -4
  71. package/templates/common/.opencode/agents/test-writer.md +6 -5
  72. package/templates/common/.opencode/agents/verifier.md +1 -0
  73. package/templates/common/.opencode/commands/wf-auto-spark.md +3 -2
  74. package/templates/common/.opencode/commands/wf-auto.md +3 -2
  75. package/templates/common/.opencode/commands/wf-browser.md +16 -0
  76. package/templates/common/.opencode/commands/wf-help.md +9 -5
  77. package/templates/common/.opencode/commands/wf-learn.md +3 -2
  78. package/templates/common/.opencode/commands/wf-max.md +12 -6
  79. package/templates/common/.opencode/commands/wf-readme.md +3 -2
  80. package/templates/common/.opencode/commands/wf-remove.md +3 -2
  81. package/templates/common/.opencode/commands/wf-review.md +3 -2
  82. package/templates/common/.opencode/commands/wf-update.md +68 -10
  83. package/templates/common/.opencode/commands/wf.md +3 -2
  84. package/templates/common/CLAUDE.md +15 -13
  85. package/templates/common/Harness/MEMORY.md +24 -21
  86. package/templates/common/Harness/README.md +50 -47
  87. package/templates/common/Harness/ownership.manifest.json +878 -0
  88. package/templates/common/Harness/{architecture.md → project/architecture.md} +1 -1
  89. package/templates/common/Harness/research/README.md +3 -3
  90. package/templates/common/Harness/scripts/archive-tasks.mjs +12 -220
  91. package/templates/common/Harness/scripts/context-budget.mjs +95 -0
  92. package/templates/common/Harness/scripts/l2-cache-telemetry.mjs +703 -0
  93. package/templates/common/Harness/scripts/scan-clean.mjs +16 -12
  94. package/templates/common/Harness/scripts/task-state.mjs +889 -0
  95. package/templates/common/Harness/scripts/validate-harness.mjs +553 -191
  96. package/templates/common/Harness/scripts/wf-remove.mjs +61 -35
  97. package/templates/common/Harness/scripts/wf-update-check.mjs +358 -45
  98. package/templates/common/Harness/settings.json +43 -0
  99. package/templates/common/Harness/{ECC-GUIDE.md → specs/guides/ECC-GUIDE.md} +4 -4
  100. package/templates/common/Harness/{SETUP.md → specs/guides/SETUP.md} +34 -37
  101. package/templates/common/Harness/{extension.md → specs/guides/extension.md} +3 -3
  102. package/templates/common/Harness/{lifecycle.md → specs/guides/lifecycle.md} +2 -2
  103. package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +76 -0
  104. package/templates/common/Harness/{agent-workflow.md → specs/runtime/agent-workflow.md} +6 -6
  105. package/templates/common/Harness/{context-loading.md → specs/runtime/context-loading.md} +85 -21
  106. package/templates/common/Harness/{dispatch.md → specs/runtime/dispatch.md} +4 -3
  107. package/templates/common/Harness/{subagents.md → specs/runtime/subagents.md} +19 -10
  108. package/templates/common/Harness/{WF-AUTO-SPARK.md → specs/workflows/WF-AUTO-SPARK.md} +2 -2
  109. package/templates/common/Harness/{WF-AUTO.md → specs/workflows/WF-AUTO.md} +12 -7
  110. package/templates/common/Harness/{WF-KERNEL.md → specs/workflows/WF-KERNEL.md} +11 -1
  111. package/templates/common/Harness/{WF-MAX.md → specs/workflows/WF-MAX.md} +40 -7
  112. package/templates/common/Harness/specs/workflows/WF-STATE.md +141 -0
  113. package/templates/common/Harness/{WF.md → specs/workflows/WF.md} +12 -1
  114. package/templates/common/README.md +8 -6
  115. package/templates/common/memory/startup-hints.md +19 -17
  116. package/templates/common/opencode.json +1 -0
  117. package/templates/optional/catalog.json +2 -9
  118. package/templates/optional/skills/github-pr-review/.claude/skills/github-pr-review/SKILL.md +1 -1
  119. package/templates/optional/skills/python-backend/.claude/skills/python-backend/SKILL.md +1 -1
  120. package/templates/optional/skills/ts-react-frontend/.claude/skills/ts-react-frontend/SKILL.md +1 -1
  121. package/templates/optional/skills/ui-ux-review/.claude/skills/ui-ux-review/SKILL.md +1 -1
  122. package/templates/common/Harness/TASK_ARCHIVE.md +0 -56
  123. package/templates/common/Harness/WF-STATE.md +0 -83
  124. package/templates/optional/skills/browser-e2e/.claude/skills/browser-e2e/SKILL.md +0 -42
  125. package/templates/optional/skills/browser-e2e/.claude/skills/wf-browser/SKILL.md +0 -194
  126. package/templates/optional/skills/browser-e2e/Harness/workflows/browser-e2e.md +0 -119
  127. /package/templates/common/Harness/{ACCEPTANCE_PROTOCOL.md → specs/protocols/ACCEPTANCE_PROTOCOL.md} +0 -0
  128. /package/templates/common/Harness/{AGENT_ISOLATION.md → specs/protocols/AGENT_ISOLATION.md} +0 -0
  129. /package/templates/common/Harness/{DEBUG_PROTOCOL.md → specs/protocols/DEBUG_PROTOCOL.md} +0 -0
  130. /package/templates/common/Harness/{HARNESS_BRIDGE.md → specs/protocols/HARNESS_BRIDGE.md} +0 -0
  131. /package/templates/common/Harness/{MEMORY_PROTOCOL.md → specs/protocols/MEMORY_PROTOCOL.md} +0 -0
  132. /package/templates/common/Harness/{TDD-GUIDE.md → specs/protocols/TDD-GUIDE.md} +0 -0
  133. /package/templates/common/Harness/{WF-AUTO-ANGLES.md → specs/workflows/WF-AUTO-ANGLES.md} +0 -0
@@ -0,0 +1,889 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const root = process.cwd();
6
+ const harnessDir = path.join(root, 'Harness');
7
+ const tasksDir = path.join(harnessDir, 'tasks');
8
+ const archiveDir = path.join(tasksDir, '_archive');
9
+ const progressPath = path.join(harnessDir, 'PROGRESS.md');
10
+ const args = process.argv.slice(2);
11
+ const command = args[0] && !args[0].startsWith('--') ? args[0] : 'help';
12
+
13
+ const OUTER_TASK_CAP = 5;
14
+ const RESERVED = new Set(['_template', '_archive', 'auto']);
15
+ const TASK_ID_RE = /^task-[a-z]+(-[a-z0-9]+){1,4}$/;
16
+ const NEVER_ARCHIVE_STATUSES = new Set([
17
+ 'active',
18
+ 'blocked',
19
+ 'in_progress',
20
+ 'running',
21
+ 'pending',
22
+ 'needs-user-decision',
23
+ ]);
24
+ const SAFE_ARCHIVE_STATUSES = new Set([
25
+ 'complete',
26
+ 'verified',
27
+ 'archived',
28
+ 'abandoned',
29
+ 'obsolete',
30
+ 'done',
31
+ 'closed',
32
+ 'closeout',
33
+ ]);
34
+ const VALID_STATUSES = new Set([
35
+ ...NEVER_ARCHIVE_STATUSES,
36
+ ...SAFE_ARCHIVE_STATUSES,
37
+ 'skipped',
38
+ 'failed',
39
+ ]);
40
+ const VALID_PHASES = new Set([
41
+ 'intake',
42
+ 'clarify',
43
+ 'requirements',
44
+ 'prd',
45
+ 'acceptance',
46
+ 'plan',
47
+ 'explore',
48
+ 'implement',
49
+ 'verify',
50
+ 'review',
51
+ 'fix',
52
+ 'reflect',
53
+ 'closeout',
54
+ 'blocked',
55
+ 'archived',
56
+ 'verified',
57
+ ]);
58
+ const PHASE_ALIASES = new Map([
59
+ ['implementation', 'implement'],
60
+ ['build', 'implement'],
61
+ ['validation', 'verify'],
62
+ ['complete', 'verified'],
63
+ ['done', 'verified'],
64
+ ['closed', 'closeout'],
65
+ ]);
66
+ const STATUS_ALIASES = new Map([
67
+ ['in-progress', 'in_progress'],
68
+ ['inprogress', 'in_progress'],
69
+ ['needs_user_decision', 'needs-user-decision'],
70
+ ['needs-user', 'needs-user-decision'],
71
+ ['need-user-decision', 'needs-user-decision'],
72
+ ['close-out', 'closeout'],
73
+ ]);
74
+
75
+ function hasFlag(name) {
76
+ return args.includes(name);
77
+ }
78
+
79
+ function flagValue(name, fallback = null) {
80
+ const index = args.indexOf(name);
81
+ if (index === -1 || index + 1 >= args.length) return fallback;
82
+ return args[index + 1];
83
+ }
84
+
85
+ const outputJson = hasFlag('--json');
86
+
87
+ function print(payload) {
88
+ if (outputJson) {
89
+ console.log(JSON.stringify(payload, null, 2));
90
+ return;
91
+ }
92
+
93
+ if (payload.command === 'list' || payload.command === 'validate') {
94
+ console.log(`Active Task: ${payload.activeTask || 'None'}`);
95
+ console.log(`Tasks: ${payload.taskCount}`);
96
+ for (const task of payload.tasks || []) {
97
+ console.log(`- ${task.id}: status=${task.status || '-'} phase=${task.phase || '-'}`);
98
+ }
99
+ } else if (payload.command === 'reconcile' || payload.command === 'set-active' || payload.command === 'transition') {
100
+ if (payload.dryRun) console.log('[DRY RUN] No files changed. Use --apply to write.');
101
+ console.log(`Operations: ${payload.operations.length}`);
102
+ for (const op of payload.operations) console.log(`- ${op.action}: ${op.taskId || op.path} (${op.reason})`);
103
+ } else if (payload.command === 'archive') {
104
+ if (payload.dryRun) console.log('[DRY RUN] No files moved. Use --apply to execute.');
105
+ console.log(`Scanned: ${payload.scanned}, Archiveable: ${payload.archiveable}, To archive: ${payload.toArchive}, Kept: ${payload.kept}, Skipped: ${payload.skipped}`);
106
+ for (const r of payload.results) {
107
+ const suffix = r.year ? ` -> _archive/${r.year}` : '';
108
+ console.log(`- ${r.action}: ${r.dir} (${r.status})${suffix}`);
109
+ }
110
+ } else {
111
+ console.log(payload.message || '');
112
+ }
113
+
114
+ for (const warning of payload.warnings || []) console.warn(`Warning: ${warning}`);
115
+ for (const error of payload.errors || []) console.error(`Error: ${error}`);
116
+ }
117
+
118
+ function finish(payload, exitCode = payload.ok === false ? 1 : 0) {
119
+ print(payload);
120
+ process.exit(exitCode);
121
+ }
122
+
123
+ function usage() {
124
+ finish({
125
+ ok: true,
126
+ command: 'help',
127
+ message: `Usage: node Harness/scripts/task-state.mjs <command> [options]
128
+
129
+ Commands:
130
+ list [--json] List task state.
131
+ validate [--strict] [--json] Validate state consistency.
132
+ reconcile [--dry-run|--apply] [--json] Normalize STATE.json and root PROGRESS.md.
133
+ set-active <task-id> [--dry-run] Set the single active task.
134
+ transition <task-id> --status <s> --phase <p> [--dry-run]
135
+ archive [--dry-run|--apply] [--keep n] [--task id] [--json]
136
+
137
+ Archive defaults to dry-run and keeps ${OUTER_TASK_CAP} non-archived task capsules.`,
138
+ }, 0);
139
+ }
140
+
141
+ function readText(file) {
142
+ return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
143
+ }
144
+
145
+ function writeTextAtomic(file, text) {
146
+ fs.mkdirSync(path.dirname(file), { recursive: true });
147
+ const tmp = `${file}.tmp-${process.pid}`;
148
+ fs.writeFileSync(tmp, text, 'utf8');
149
+ fs.renameSync(tmp, file);
150
+ }
151
+
152
+ function writeJsonAtomic(file, value) {
153
+ writeTextAtomic(file, JSON.stringify(value, null, 2) + '\n');
154
+ }
155
+
156
+ function normalizeCandidate(value, validSet, aliasMap) {
157
+ if (value === null || value === undefined) return '';
158
+ const raw = String(value).trim().toLowerCase();
159
+ if (!raw) return '';
160
+ const compact = raw.replace(/\s+/g, '-');
161
+ const direct = aliasMap.get(compact) || compact;
162
+ if (validSet.has(direct)) return direct;
163
+
164
+ const tokens = raw
165
+ .replace(/[`*_()[\]{}:]/g, ' ')
166
+ .replace(/[^a-z0-9_-]+/g, ' ')
167
+ .split(/\s+/)
168
+ .map(token => token.trim())
169
+ .filter(Boolean);
170
+
171
+ for (const token of tokens) {
172
+ const canonical = aliasMap.get(token) || token;
173
+ if (validSet.has(canonical)) return canonical;
174
+ }
175
+
176
+ return '';
177
+ }
178
+
179
+ function normalizeStatus(value) {
180
+ return normalizeCandidate(value, VALID_STATUSES, STATUS_ALIASES);
181
+ }
182
+
183
+ function normalizePhase(value) {
184
+ return normalizeCandidate(value, VALID_PHASES, PHASE_ALIASES);
185
+ }
186
+
187
+ function displayPhase(phase) {
188
+ const value = normalizePhase(phase) || String(phase || '').trim().toLowerCase();
189
+ const labels = {
190
+ intake: 'Intake',
191
+ clarify: 'Clarify',
192
+ requirements: 'Requirements',
193
+ prd: 'PRD',
194
+ acceptance: 'Acceptance',
195
+ plan: 'Plan',
196
+ explore: 'Explore',
197
+ implement: 'Implementation',
198
+ verify: 'Validation',
199
+ review: 'Review',
200
+ fix: 'Fix',
201
+ reflect: 'Reflect',
202
+ closeout: 'Closeout',
203
+ blocked: 'Blocked',
204
+ archived: 'Archived',
205
+ verified: 'Verified',
206
+ };
207
+ return labels[value] || (value ? value[0].toUpperCase() + value.slice(1) : '-');
208
+ }
209
+
210
+ function statusFromPhase(phase, isActive) {
211
+ const normalized = normalizePhase(phase);
212
+ if (isActive) return 'active';
213
+ if (normalized === 'verified' || normalized === 'closeout') return 'verified';
214
+ if (normalized === 'archived') return 'archived';
215
+ if (normalized === 'blocked') return 'blocked';
216
+ if (normalized) return 'in_progress';
217
+ return 'pending';
218
+ }
219
+
220
+ function defaultQueues() {
221
+ return {
222
+ ready: [],
223
+ running: [],
224
+ blocked: [],
225
+ done: [],
226
+ };
227
+ }
228
+
229
+ function normalizeQueues(state) {
230
+ const source = state && typeof state.queues === 'object' && state.queues ? state.queues : state || {};
231
+ return {
232
+ ready: Array.isArray(source.ready) ? source.ready : [],
233
+ running: Array.isArray(source.running) ? source.running : [],
234
+ blocked: Array.isArray(source.blocked) ? source.blocked : [],
235
+ done: Array.isArray(source.done) ? source.done : [],
236
+ };
237
+ }
238
+
239
+ function readState(taskId) {
240
+ const file = path.join(tasksDir, taskId, 'STATE.json');
241
+ if (!fs.existsSync(file)) return { state: null, error: null, path: file };
242
+ try {
243
+ return { state: JSON.parse(fs.readFileSync(file, 'utf8')), error: null, path: file };
244
+ } catch (err) {
245
+ return { state: null, error: `invalid STATE.json: ${err.message}`, path: file };
246
+ }
247
+ }
248
+
249
+ function readTaskProgressPhase(taskId) {
250
+ const text = readText(path.join(tasksDir, taskId, 'PROGRESS.md'));
251
+ for (const pattern of [
252
+ /^(?:-\s*)?Phase:\s*(.+)$/mi,
253
+ /^Current phase:\s*(.+)$/mi,
254
+ /^Current:\s*(.+)$/mi,
255
+ ]) {
256
+ const match = text.match(pattern);
257
+ if (match) return match[1].trim();
258
+ }
259
+ return '';
260
+ }
261
+
262
+ function parseRootProgress() {
263
+ const text = readText(progressPath);
264
+ const activeMatch = text.match(/## Active Task\s*\r?\n+(?:\s*\r?\n)?\s*-\s*([^\r\n]+)/);
265
+ const rawActive = activeMatch ? activeMatch[1].trim() : '';
266
+ const activeTask = rawActive && !/^none$/i.test(rawActive) ? rawActive : null;
267
+ const rows = [];
268
+ const taskIndexMatch = text.match(/## Task Index\s*\r?\n([\s\S]*?)(?=\r?\n## |\s*$)/);
269
+ if (taskIndexMatch) {
270
+ for (const line of taskIndexMatch[1].split(/\r?\n/)) {
271
+ const trimmed = line.trim();
272
+ if (!trimmed.startsWith('|')) continue;
273
+ if (/^\|\s*-+/.test(trimmed) || /^\|\s*ID\s*\|/i.test(trimmed)) continue;
274
+ const cells = trimmed.split('|').slice(1, -1).map(cell => cell.trim());
275
+ if (cells.length >= 4 && cells[0]) {
276
+ rows.push({ id: cells[0], goal: cells[1], phase: cells[2], closed: cells[3] });
277
+ }
278
+ }
279
+ }
280
+ return { text, activeTask, rows };
281
+ }
282
+
283
+ function listOuterTaskNames() {
284
+ if (!fs.existsSync(tasksDir)) return [];
285
+ return fs.readdirSync(tasksDir, { withFileTypes: true })
286
+ .filter(entry => entry.isDirectory())
287
+ .map(entry => entry.name)
288
+ .filter(name => !RESERVED.has(name) && !name.startsWith('_'))
289
+ .sort();
290
+ }
291
+
292
+ function taskTitle(taskId) {
293
+ return taskId
294
+ .replace(/^task-/, '')
295
+ .split('-')
296
+ .map(word => word[0] ? word[0].toUpperCase() + word.slice(1) : word)
297
+ .join(' ');
298
+ }
299
+
300
+ function collectTasks() {
301
+ const rootProgress = parseRootProgress();
302
+ const rowsById = new Map(rootProgress.rows.map(row => [row.id, row]));
303
+ const tasks = listOuterTaskNames().map(id => {
304
+ const stateRead = readState(id);
305
+ const state = stateRead.state;
306
+ const rootRow = rowsById.get(id) || null;
307
+ const progressPhaseRaw = readTaskProgressPhase(id);
308
+ const stateStatus = normalizeStatus(state?.status);
309
+ const statePhase = normalizePhase(state?.phase);
310
+ const rootPhase = normalizePhase(rootRow?.phase);
311
+ const progressPhase = normalizePhase(progressPhaseRaw);
312
+ const phase = rootPhase || progressPhase || statePhase;
313
+ const status = stateStatus || statusFromPhase(phase, id === rootProgress.activeTask);
314
+ const stat = fs.statSync(path.join(tasksDir, id));
315
+ return {
316
+ id,
317
+ path: path.join(tasksDir, id),
318
+ statePath: stateRead.path,
319
+ state,
320
+ stateError: stateRead.error,
321
+ rootRow,
322
+ progressPhaseRaw,
323
+ stateStatus,
324
+ statePhase,
325
+ rootPhase,
326
+ progressPhase,
327
+ status,
328
+ phase,
329
+ mtimeMs: stat.mtimeMs,
330
+ };
331
+ });
332
+
333
+ return { rootProgress, tasks, rowsById };
334
+ }
335
+
336
+ function archiveEligibility(task, activeTask) {
337
+ if (task.id === activeTask) return { ok: false, reason: 'active task' };
338
+ if (task.stateError) return { ok: false, reason: task.stateError };
339
+ if (!task.state) return { ok: false, reason: 'missing STATE.json requires reconcile' };
340
+ const status = normalizeStatus(task.state.status);
341
+ const phase = normalizePhase(task.state.phase);
342
+ if (NEVER_ARCHIVE_STATUSES.has(status)) return { ok: false, reason: `status "${status}" is never auto-archived` };
343
+ if (NEVER_ARCHIVE_STATUSES.has(phase)) return { ok: false, reason: `phase "${phase}" is never auto-archived` };
344
+ if (task.state.activeQuestion) return { ok: false, reason: 'activeQuestion is set' };
345
+ const queues = normalizeQueues(task.state);
346
+ if (queues.running.length > 0) return { ok: false, reason: 'running queue is not empty' };
347
+ if (queues.blocked.length > 0) return { ok: false, reason: 'blocked queue is not empty' };
348
+ if (SAFE_ARCHIVE_STATUSES.has(status) || SAFE_ARCHIVE_STATUSES.has(phase)) {
349
+ return { ok: true, reason: 'status/phase allows archive' };
350
+ }
351
+ if (SAFE_ARCHIVE_STATUSES.has(task.rootPhase) || SAFE_ARCHIVE_STATUSES.has(task.progressPhase)) {
352
+ return { ok: true, reason: 'root/progress phase allows archive' };
353
+ }
354
+ return { ok: false, reason: 'status indeterminate; run reconcile or review manually' };
355
+ }
356
+
357
+ function validateState({ strict = false } = {}) {
358
+ const { rootProgress, tasks } = collectTasks();
359
+ const errors = [];
360
+ const warnings = [];
361
+ const rootIds = new Set(rootProgress.rows.map(row => row.id));
362
+ const taskIds = new Set(tasks.map(task => task.id));
363
+
364
+ function issue(message, hard = false) {
365
+ if (hard || strict) errors.push(message);
366
+ else warnings.push(message);
367
+ }
368
+
369
+ if (rootProgress.activeTask && !taskIds.has(rootProgress.activeTask)) {
370
+ issue(`Active Task "${rootProgress.activeTask}" does not exist under Harness/tasks/`, true);
371
+ }
372
+
373
+ for (const row of rootProgress.rows) {
374
+ if (!taskIds.has(row.id)) issue(`Task Index row has no outer task directory: ${row.id}`);
375
+ }
376
+ for (const task of tasks) {
377
+ if (!rootIds.has(task.id)) issue(`Outer task directory is missing from Task Index: ${task.id}`);
378
+ if (task.stateError) {
379
+ issue(`${task.id}: ${task.stateError}`, true);
380
+ continue;
381
+ }
382
+ if (!task.state) {
383
+ issue(`${task.id}: missing STATE.json`);
384
+ continue;
385
+ }
386
+ if (task.state.taskId && task.state.taskId !== task.id) {
387
+ issue(`${task.id}: STATE.json taskId "${task.state.taskId}" does not match directory`, true);
388
+ }
389
+ if (task.state.status && !normalizeStatus(task.state.status)) {
390
+ issue(`${task.id}: unknown status "${task.state.status}"`);
391
+ }
392
+ if (task.state.phase && !normalizePhase(task.state.phase)) {
393
+ issue(`${task.id}: unknown phase "${task.state.phase}"`);
394
+ }
395
+ if (normalizeStatus(task.state.status) === 'active' && task.id !== rootProgress.activeTask) {
396
+ issue(`${task.id}: STATE.json is active but root Active Task is ${rootProgress.activeTask || 'None'}`, true);
397
+ }
398
+ if (task.id === rootProgress.activeTask && normalizeStatus(task.state.status) && normalizeStatus(task.state.status) !== 'active') {
399
+ issue(`${task.id}: root Active Task points here but STATE.json status is "${normalizeStatus(task.state.status)}"`, true);
400
+ }
401
+ }
402
+
403
+ const activeStateTasks = tasks.filter(task => normalizeStatus(task.state?.status) === 'active');
404
+ if (activeStateTasks.length > 1) {
405
+ issue(`Multiple STATE.json files are active: ${activeStateTasks.map(task => task.id).join(', ')}`, true);
406
+ }
407
+ if (tasks.length > OUTER_TASK_CAP) {
408
+ issue(`Harness/tasks/ has ${tasks.length} outer task capsules (cap ${OUTER_TASK_CAP}); run node Harness/scripts/task-state.mjs archive --apply`);
409
+ }
410
+
411
+ return {
412
+ ok: errors.length === 0,
413
+ command: 'validate',
414
+ activeTask: rootProgress.activeTask,
415
+ outerTaskCap: OUTER_TASK_CAP,
416
+ taskCount: tasks.length,
417
+ errors,
418
+ warnings,
419
+ tasks: tasks.map(task => ({
420
+ id: task.id,
421
+ status: normalizeStatus(task.state?.status) || task.status,
422
+ phase: normalizePhase(task.state?.phase) || task.phase,
423
+ rootPhase: task.rootPhase,
424
+ progressPhase: task.progressPhase,
425
+ archive: archiveEligibility(task, rootProgress.activeTask),
426
+ })),
427
+ };
428
+ }
429
+
430
+ function ensureValidTaskId(taskId) {
431
+ if (!taskId || !TASK_ID_RE.test(taskId) || RESERVED.has(taskId) || taskId.startsWith('_')) {
432
+ throw new Error(`Invalid task id "${taskId || ''}". Expected task-<verb>-<noun>[-detail].`);
433
+ }
434
+ }
435
+
436
+ function safeTaskPath(...segments) {
437
+ const resolved = path.resolve(tasksDir, ...segments);
438
+ const normalized = resolved.replace(/\\/g, '/');
439
+ const prefix = tasksDir.replace(/\\/g, '/');
440
+ if (normalized !== prefix && !normalized.startsWith(`${prefix}/`)) {
441
+ throw new Error(`Resolved path is outside Harness/tasks/: ${resolved}`);
442
+ }
443
+ return resolved;
444
+ }
445
+
446
+ function desiredPhaseForTask(task, activeTask) {
447
+ return task.rootPhase || task.progressPhase || task.statePhase || (task.id === activeTask ? 'implement' : 'intake');
448
+ }
449
+
450
+ function desiredStatusForTask(task, activeTask, desiredPhase) {
451
+ if (task.id === activeTask) return 'active';
452
+ const current = normalizeStatus(task.state?.status);
453
+ if (current && current !== 'active') return current;
454
+ return statusFromPhase(desiredPhase, false);
455
+ }
456
+
457
+ function defaultState(taskId, status, phase, now) {
458
+ return {
459
+ schemaVersion: 1,
460
+ taskId,
461
+ status,
462
+ mode: 'direct',
463
+ tier: 'none',
464
+ phase,
465
+ gate: null,
466
+ updatedAt: now,
467
+ activeQuestion: null,
468
+ nextAction: 'Review task state.',
469
+ acceptance: [],
470
+ queues: defaultQueues(),
471
+ dispatchLedger: [],
472
+ decisions: [],
473
+ risks: [],
474
+ artifacts: [],
475
+ };
476
+ }
477
+
478
+ function normalizeState(task, activeTask, now) {
479
+ const phase = desiredPhaseForTask(task, activeTask);
480
+ const status = desiredStatusForTask(task, activeTask, phase);
481
+ const before = task.state ? JSON.stringify(task.state) : null;
482
+ const state = task.state
483
+ ? { ...task.state }
484
+ : defaultState(task.id, status, phase, now);
485
+
486
+ state.schemaVersion = 1;
487
+ state.taskId = task.id;
488
+ state.status = status;
489
+ state.phase = phase;
490
+ if (!state.mode) state.mode = 'direct';
491
+ if (!state.tier) state.tier = 'none';
492
+ if (!Object.prototype.hasOwnProperty.call(state, 'gate')) state.gate = null;
493
+ if (!Object.prototype.hasOwnProperty.call(state, 'activeQuestion')) state.activeQuestion = null;
494
+ if (!state.nextAction) state.nextAction = task.rootRow?.goal || 'Review task state.';
495
+ state.queues = normalizeQueues(state);
496
+ if (!Array.isArray(state.dispatchLedger)) state.dispatchLedger = [];
497
+ if (!Array.isArray(state.decisions)) state.decisions = [];
498
+ if (!Array.isArray(state.risks)) state.risks = [];
499
+ if (!Array.isArray(state.artifacts)) state.artifacts = [];
500
+ if (!state.updatedAt || JSON.stringify(state) !== before) state.updatedAt = now;
501
+
502
+ const after = JSON.stringify(state);
503
+ return {
504
+ state,
505
+ changed: before !== after,
506
+ reason: task.state ? 'normalize existing STATE.json' : 'create missing STATE.json',
507
+ };
508
+ }
509
+
510
+ function renderRootProgress(existingText, activeTask, rows) {
511
+ const activeSection = `## Active Task\n\n- ${activeTask || 'None'}`;
512
+ const taskIndex = `## Task Index
513
+
514
+ Non-archived tasks only (max ${OUTER_TASK_CAP}). Archived tasks are listed in \`Harness/tasks/_archive/INDEX.md\` (see \`Harness/specs/protocols/TASK_ARCHIVE.md\`).
515
+
516
+ | ID | Goal | Phase | Closed |
517
+ |----|------|-------|--------|
518
+ ${rows.map(row => `| ${row.id} | ${row.goal || taskTitle(row.id)} | ${row.phase || '-'} | ${row.closed || '-'} |`).join('\n')}`;
519
+
520
+ let text = (existingText || '# PROGRESS.md\n\nGlobal task index.\n\n## Cross-Task Decisions\n\n| Date | Decision | Reason |\n|------|----------|--------|\n').replace(/\r\n/g, '\n');
521
+ text = replaceSection(text, '## Active Task', '## Task Index', activeSection);
522
+ text = replaceSection(text, '## Task Index', '## Cross-Task Decisions', taskIndex);
523
+ return text.replace(/[ \t]+$/gm, '').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
524
+ }
525
+
526
+ function renderTaskProgress(existingText, taskId, state) {
527
+ const phase = displayPhase(state.phase);
528
+ let text = (existingText || `# ${taskId} - PROGRESS\n\nCompact heartbeat. Update on phase changes, blockers, failures, and closeout.\n`).replace(/\r\n/g, '\n');
529
+ const phaseLine = `- Phase: ${phase}`;
530
+
531
+ if (/^(?:-\s*)?Phase:\s*.*$/mi.test(text)) {
532
+ text = text.replace(/^(?:-\s*)?Phase:\s*.*$/mi, phaseLine);
533
+ } else if (text.includes('## Status')) {
534
+ text = text.replace(/## Status\s*\n+/, `## Status\n\n${phaseLine}\n`);
535
+ } else {
536
+ text = `${text.trimEnd()}\n\n## Status\n\n${phaseLine}\n`;
537
+ }
538
+
539
+ return text.replace(/[ \t]+$/gm, '').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
540
+ }
541
+
542
+ function replaceSection(text, startHeading, nextHeading, replacement) {
543
+ const start = text.indexOf(startHeading);
544
+ if (start === -1) {
545
+ const next = text.indexOf(nextHeading);
546
+ if (next === -1) return `${text.trimEnd()}\n\n${replacement.trimEnd()}\n`;
547
+ return `${text.slice(0, next).trimEnd()}\n\n${replacement.trimEnd()}\n\n${text.slice(next).trimStart()}`;
548
+ }
549
+ const next = text.indexOf(nextHeading, start + startHeading.length);
550
+ if (next === -1) return `${text.slice(0, start).trimEnd()}\n\n${replacement.trimEnd()}\n`;
551
+ return `${text.slice(0, start).trimEnd()}\n\n${replacement.trimEnd()}\n\n${text.slice(next).trimStart()}`;
552
+ }
553
+
554
+ function buildRows(tasks, activeTask, excluded = new Set()) {
555
+ const byId = new Map(tasks.map(task => [task.id, task]));
556
+ const rootOrder = [];
557
+ for (const task of tasks) {
558
+ if (task.rootRow && !rootOrder.includes(task.id)) rootOrder.push(task.id);
559
+ }
560
+ const ids = [];
561
+ if (activeTask && byId.has(activeTask) && !excluded.has(activeTask)) ids.push(activeTask);
562
+ for (const id of rootOrder) {
563
+ if (!ids.includes(id) && byId.has(id) && !excluded.has(id)) ids.push(id);
564
+ }
565
+ for (const task of tasks) {
566
+ if (!ids.includes(task.id) && !excluded.has(task.id)) ids.push(task.id);
567
+ }
568
+
569
+ return ids.map(id => {
570
+ const task = byId.get(id);
571
+ const phase = normalizePhase(task.desiredState?.phase) || task.phase;
572
+ return {
573
+ id,
574
+ goal: task.rootRow?.goal || task.state?.goal || task.state?.nextAction || taskTitle(id),
575
+ phase: displayPhase(phase),
576
+ closed: task.rootRow?.closed || '-',
577
+ };
578
+ });
579
+ }
580
+
581
+ function buildReconcilePlan({ activeOverride = undefined, transition = null } = {}) {
582
+ const now = new Date().toISOString();
583
+ const { rootProgress, tasks } = collectTasks();
584
+ const errors = [];
585
+ let activeTask = activeOverride === undefined ? rootProgress.activeTask : activeOverride;
586
+
587
+ if (activeTask) {
588
+ try {
589
+ ensureValidTaskId(activeTask);
590
+ } catch (err) {
591
+ errors.push(err.message);
592
+ }
593
+ if (!tasks.some(task => task.id === activeTask)) {
594
+ errors.push(`Active task "${activeTask}" does not exist under Harness/tasks/`);
595
+ }
596
+ }
597
+
598
+ for (const task of tasks) {
599
+ if (task.stateError) errors.push(`${task.id}: ${task.stateError}`);
600
+ if (transition && task.id === transition.taskId) {
601
+ if (transition.status) {
602
+ const status = normalizeStatus(transition.status);
603
+ if (!status) errors.push(`${task.id}: unknown transition status "${transition.status}"`);
604
+ if (status === 'active') activeTask = task.id;
605
+ if (rootProgress.activeTask === task.id && SAFE_ARCHIVE_STATUSES.has(status)) activeTask = null;
606
+ }
607
+ if (transition.phase && !normalizePhase(transition.phase)) {
608
+ errors.push(`${task.id}: unknown transition phase "${transition.phase}"`);
609
+ }
610
+ }
611
+ }
612
+
613
+ if (errors.length) {
614
+ return { ok: false, command: 'reconcile', dryRun: true, activeTask, errors, warnings: [], operations: [] };
615
+ }
616
+
617
+ const operations = [];
618
+ for (const task of tasks) {
619
+ let normalized = normalizeState(task, activeTask, now);
620
+ if (transition && task.id === transition.taskId) {
621
+ const nextState = { ...normalized.state };
622
+ if (transition.status) nextState.status = normalizeStatus(transition.status);
623
+ if (transition.phase) nextState.phase = normalizePhase(transition.phase);
624
+ if (transition.nextAction) nextState.nextAction = transition.nextAction;
625
+ nextState.updatedAt = now;
626
+ normalized = {
627
+ state: nextState,
628
+ changed: JSON.stringify(nextState) !== JSON.stringify(task.state),
629
+ reason: 'apply explicit transition',
630
+ };
631
+ }
632
+ task.desiredState = normalized.state;
633
+ if (normalized.changed) {
634
+ operations.push({
635
+ action: task.state ? 'write-state' : 'create-state',
636
+ taskId: task.id,
637
+ path: `Harness/tasks/${task.id}/STATE.json`,
638
+ reason: normalized.reason,
639
+ state: normalized.state,
640
+ });
641
+ }
642
+
643
+ const progressRel = `Harness/tasks/${task.id}/PROGRESS.md`;
644
+ const progressFile = path.join(root, ...progressRel.split('/'));
645
+ const existingProgress = readText(progressFile);
646
+ const desiredProgress = renderTaskProgress(existingProgress, task.id, normalized.state);
647
+ if (desiredProgress !== existingProgress.replace(/\r\n/g, '\n')) {
648
+ operations.push({
649
+ action: 'write-task-progress',
650
+ taskId: task.id,
651
+ path: progressRel,
652
+ reason: 'sync task PROGRESS.md phase from STATE.json',
653
+ text: desiredProgress,
654
+ });
655
+ }
656
+ }
657
+
658
+ const rows = buildRows(tasks, activeTask);
659
+ const desiredProgress = renderRootProgress(rootProgress.text, activeTask, rows);
660
+ if (desiredProgress !== rootProgress.text.replace(/\r\n/g, '\n')) {
661
+ operations.push({
662
+ action: 'rewrite-root-progress',
663
+ path: 'Harness/PROGRESS.md',
664
+ reason: 'sync active pointer and Task Index from task state',
665
+ text: desiredProgress,
666
+ });
667
+ }
668
+
669
+ return {
670
+ ok: true,
671
+ command: 'reconcile',
672
+ dryRun: !hasFlag('--apply'),
673
+ activeTask,
674
+ errors: [],
675
+ warnings: [],
676
+ operations,
677
+ };
678
+ }
679
+
680
+ function applyOperations(operations) {
681
+ for (const op of operations) {
682
+ if (op.action === 'write-state' || op.action === 'create-state') {
683
+ writeJsonAtomic(path.join(root, ...op.path.split('/')), op.state);
684
+ } else if (op.action === 'rewrite-root-progress' || op.action === 'write-task-progress') {
685
+ writeTextAtomic(path.join(root, ...op.path.split('/')), op.text);
686
+ }
687
+ }
688
+ }
689
+
690
+ function runList() {
691
+ const validation = validateState();
692
+ finish({ ...validation, command: 'list', ok: true }, 0);
693
+ }
694
+
695
+ function runValidate() {
696
+ const payload = validateState({ strict: hasFlag('--strict') });
697
+ finish(payload, payload.ok ? 0 : 1);
698
+ }
699
+
700
+ function runReconcile(overrides = {}) {
701
+ const plan = buildReconcilePlan(overrides);
702
+ if (overrides.commandLabel) plan.command = overrides.commandLabel;
703
+ if (plan.ok && hasFlag('--apply')) {
704
+ applyOperations(plan.operations);
705
+ plan.dryRun = false;
706
+ }
707
+ finish(plan, plan.ok ? 0 : 1);
708
+ }
709
+
710
+ function runSetActive() {
711
+ const taskId = args[1];
712
+ try {
713
+ ensureValidTaskId(taskId);
714
+ } catch (err) {
715
+ finish({ ok: false, command: 'set-active', errors: [err.message], warnings: [], operations: [] }, 1);
716
+ }
717
+ const previousApply = hasFlag('--apply');
718
+ if (!previousApply && !hasFlag('--dry-run')) args.push('--apply');
719
+ runReconcile({ activeOverride: taskId, commandLabel: 'set-active' });
720
+ }
721
+
722
+ function runTransition() {
723
+ const taskId = args[1];
724
+ try {
725
+ ensureValidTaskId(taskId);
726
+ } catch (err) {
727
+ finish({ ok: false, command: 'transition', errors: [err.message], warnings: [], operations: [] }, 1);
728
+ }
729
+ const status = flagValue('--status');
730
+ const phase = flagValue('--phase');
731
+ const nextAction = flagValue('--next');
732
+ if (!status && !phase && !nextAction) {
733
+ finish({ ok: false, command: 'transition', errors: ['transition requires --status, --phase, or --next'], warnings: [], operations: [] }, 1);
734
+ }
735
+ if (!hasFlag('--apply') && !hasFlag('--dry-run')) args.push('--apply');
736
+ runReconcile({ commandLabel: 'transition', transition: { taskId, status, phase, nextAction } });
737
+ }
738
+
739
+ function buildArchivePlan() {
740
+ const keepValue = Number.parseInt(flagValue('--keep', String(OUTER_TASK_CAP)), 10);
741
+ if (!Number.isInteger(keepValue) || keepValue < 0) {
742
+ return { ok: false, command: 'archive', errors: ['--keep must be a non-negative integer'], warnings: [], results: [] };
743
+ }
744
+
745
+ const taskFilter = flagValue('--task');
746
+ if (taskFilter) {
747
+ try {
748
+ ensureValidTaskId(taskFilter);
749
+ } catch (err) {
750
+ return { ok: false, command: 'archive', errors: [err.message], warnings: [], results: [] };
751
+ }
752
+ }
753
+
754
+ const { rootProgress, tasks } = collectTasks();
755
+ const selectedTasks = taskFilter ? tasks.filter(task => task.id === taskFilter) : tasks;
756
+ if (taskFilter && selectedTasks.length === 0) {
757
+ return { ok: false, command: 'archive', errors: [`Task "${taskFilter}" not found in Harness/tasks/`], warnings: [], results: [] };
758
+ }
759
+
760
+ const archiveable = [];
761
+ const skipped = [];
762
+ for (const task of selectedTasks) {
763
+ const eligibility = archiveEligibility(task, rootProgress.activeTask);
764
+ if (eligibility.ok) archiveable.push(task);
765
+ else skipped.push({ task, reason: eligibility.reason });
766
+ }
767
+
768
+ archiveable.sort((a, b) => a.mtimeMs - b.mtimeMs || a.id.localeCompare(b.id));
769
+ const toArchiveCount = taskFilter ? archiveable.length : Math.max(0, tasks.length - keepValue);
770
+ const toArchive = archiveable.slice(0, toArchiveCount);
771
+ const toArchiveIds = new Set(toArchive.map(task => task.id));
772
+ const keptArchiveable = archiveable.filter(task => !toArchiveIds.has(task.id));
773
+
774
+ const errors = [];
775
+ for (const task of toArchive) {
776
+ const year = new Date(task.mtimeMs).getFullYear().toString();
777
+ const dest = safeTaskPath('_archive', year, task.id);
778
+ if (fs.existsSync(dest)) errors.push(`Archive destination already exists: Harness/tasks/_archive/${year}/${task.id}`);
779
+ }
780
+ if (errors.length) {
781
+ return { ok: false, command: 'archive', errors, warnings: [], results: [] };
782
+ }
783
+
784
+ const results = [
785
+ ...toArchive.map(task => ({
786
+ dir: task.id,
787
+ year: new Date(task.mtimeMs).getFullYear().toString(),
788
+ action: hasFlag('--apply') ? 'archived' : 'would-archive',
789
+ status: hasFlag('--apply') ? 'moved' : 'dry-run',
790
+ })),
791
+ ...keptArchiveable.map(task => ({
792
+ dir: task.id,
793
+ year: new Date(task.mtimeMs).getFullYear().toString(),
794
+ action: 'kept',
795
+ status: `kept by --keep ${keepValue}`,
796
+ })),
797
+ ...skipped.map(({ task, reason }) => ({
798
+ dir: task.id,
799
+ year: null,
800
+ action: 'skipped',
801
+ status: reason,
802
+ })),
803
+ ];
804
+
805
+ return {
806
+ ok: true,
807
+ command: 'archive',
808
+ dryRun: !hasFlag('--apply'),
809
+ keep: keepValue,
810
+ scanned: selectedTasks.length,
811
+ archiveable: archiveable.length,
812
+ toArchive: toArchive.length,
813
+ kept: keptArchiveable.length,
814
+ skipped: skipped.length,
815
+ errors: [],
816
+ warnings: [],
817
+ results,
818
+ toArchiveIds,
819
+ tasks,
820
+ rootProgress,
821
+ };
822
+ }
823
+
824
+ function appendArchiveIndex(entries) {
825
+ if (entries.length === 0) return;
826
+ const indexPath = path.join(archiveDir, 'INDEX.md');
827
+ const existing = fs.existsSync(indexPath)
828
+ ? fs.readFileSync(indexPath, 'utf8').trimEnd() + '\n'
829
+ : '| Task | Year | Archived |\n|------|------|----------|\n';
830
+ const date = new Date().toISOString().slice(0, 10);
831
+ const lines = entries.map(entry => `| ${entry.id} | ${entry.year} | ${date} |`).join('\n');
832
+ writeTextAtomic(indexPath, `${existing}${lines}\n`);
833
+ }
834
+
835
+ function runArchive() {
836
+ const plan = buildArchivePlan();
837
+ if (!plan.ok) finish(plan, 1);
838
+
839
+ if (hasFlag('--apply')) {
840
+ const indexEntries = [];
841
+ for (const result of plan.results.filter(result => result.action === 'archived')) {
842
+ const src = safeTaskPath(result.dir);
843
+ const destDir = safeTaskPath('_archive', result.year);
844
+ const dest = safeTaskPath('_archive', result.year, result.dir);
845
+ fs.mkdirSync(destDir, { recursive: true });
846
+ fs.renameSync(src, dest);
847
+ const movedStatePath = path.join(dest, 'STATE.json');
848
+ const movedState = fs.existsSync(movedStatePath)
849
+ ? JSON.parse(fs.readFileSync(movedStatePath, 'utf8'))
850
+ : defaultState(result.dir, 'archived', 'archived', new Date().toISOString());
851
+ movedState.status = 'archived';
852
+ movedState.phase = 'archived';
853
+ movedState.updatedAt = new Date().toISOString();
854
+ writeJsonAtomic(movedStatePath, movedState);
855
+ const movedProgressPath = path.join(dest, 'PROGRESS.md');
856
+ writeTextAtomic(movedProgressPath, renderTaskProgress(readText(movedProgressPath), result.dir, movedState));
857
+ indexEntries.push({ id: result.dir, year: result.year });
858
+ }
859
+ appendArchiveIndex(indexEntries);
860
+
861
+ for (const task of plan.tasks) {
862
+ task.desiredState = task.state || defaultState(task.id, task.status, task.phase || 'intake', new Date().toISOString());
863
+ }
864
+ const rows = buildRows(plan.tasks, plan.rootProgress.activeTask, plan.toArchiveIds);
865
+ const desiredProgress = renderRootProgress(plan.rootProgress.text, plan.rootProgress.activeTask, rows);
866
+ writeTextAtomic(progressPath, desiredProgress);
867
+ plan.dryRun = false;
868
+ }
869
+
870
+ delete plan.tasks;
871
+ delete plan.rootProgress;
872
+ delete plan.toArchiveIds;
873
+ finish(plan, 0);
874
+ }
875
+
876
+ if (command === 'help' || hasFlag('--help') || hasFlag('-h')) usage();
877
+ if (command === 'list') runList();
878
+ if (command === 'validate') runValidate();
879
+ if (command === 'reconcile') runReconcile();
880
+ if (command === 'set-active') runSetActive();
881
+ if (command === 'transition') runTransition();
882
+ if (command === 'archive') runArchive();
883
+
884
+ finish({
885
+ ok: false,
886
+ command,
887
+ errors: [`Unknown command "${command}". Run node Harness/scripts/task-state.mjs --help.`],
888
+ warnings: [],
889
+ }, 1);