create-harness-vibe-coding 0.8.18 → 0.8.19

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 (128) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +1 -0
  3. package/package.json +5 -2
  4. package/src/index.js +383 -267
  5. package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js +17 -0
  6. package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js.map +1 -0
  7. package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js +7 -0
  8. package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js.map +1 -0
  9. package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js +12 -0
  10. package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js.map +1 -0
  11. package/src/ui/dist/assets/TaskList-BN4r8s1i.js +50 -0
  12. package/src/ui/dist/assets/TaskList-BN4r8s1i.js.map +1 -0
  13. package/src/ui/dist/assets/TerminalDrawer-6GBZ9nXN.css +32 -0
  14. package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js +74 -0
  15. package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js.map +1 -0
  16. package/src/ui/dist/assets/WorkflowRoute-BnuhLJ6X.css +1 -0
  17. package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js +39 -0
  18. package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js.map +1 -0
  19. package/src/ui/dist/assets/index-BOVYWntB.js +117 -0
  20. package/src/ui/dist/assets/index-BOVYWntB.js.map +1 -0
  21. package/src/ui/dist/assets/index-DaUObq0H.css +1 -0
  22. package/src/ui/dist/assets/maximize-2-LNrr_rKr.js +7 -0
  23. package/src/ui/dist/assets/maximize-2-LNrr_rKr.js.map +1 -0
  24. package/src/ui/dist/assets/plus-QuTJyoT3.js +7 -0
  25. package/src/ui/dist/assets/plus-QuTJyoT3.js.map +1 -0
  26. package/src/ui/dist/assets/proxy-D0vUbds5.js +2 -0
  27. package/src/ui/dist/assets/proxy-D0vUbds5.js.map +1 -0
  28. package/src/ui/dist/assets/refresh-cw-JNmExijd.js +7 -0
  29. package/src/ui/dist/assets/refresh-cw-JNmExijd.js.map +1 -0
  30. package/src/ui/dist/assets/terminal-BBQvnOAK.js +7 -0
  31. package/src/ui/dist/assets/terminal-BBQvnOAK.js.map +1 -0
  32. package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js +7 -0
  33. package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js.map +1 -0
  34. package/src/ui/dist/assets/x-DoIOGAJz.js +7 -0
  35. package/src/ui/dist/assets/x-DoIOGAJz.js.map +1 -0
  36. package/src/ui/dist/index.html +18 -0
  37. package/src/ui/index.html +17 -0
  38. package/src/ui/package.json +33 -0
  39. package/src/ui/pnpm-lock.yaml +1582 -0
  40. package/src/ui/pnpm-workspace.yaml +2 -0
  41. package/src/ui/src/App.tsx +86 -0
  42. package/src/ui/src/api.ts +93 -0
  43. package/src/ui/src/components/AgentsRoute.tsx +503 -0
  44. package/src/ui/src/components/Footer.tsx +69 -0
  45. package/src/ui/src/components/Header.tsx +175 -0
  46. package/src/ui/src/components/LoadingView.tsx +22 -0
  47. package/src/ui/src/components/RolesRoute.tsx +169 -0
  48. package/src/ui/src/components/SettingsRoute.tsx +166 -0
  49. package/src/ui/src/components/TaskList.tsx +388 -0
  50. package/src/ui/src/components/TerminalDrawer.tsx +611 -0
  51. package/src/ui/src/components/WorkflowRoute.tsx +670 -0
  52. package/src/ui/src/hooks/useReducedMotion.ts +17 -0
  53. package/src/ui/src/hooks/useServerConnection.ts +114 -0
  54. package/src/ui/src/index.css +281 -0
  55. package/src/ui/src/main.tsx +11 -0
  56. package/src/ui/src/types.ts +108 -0
  57. package/src/ui/tsconfig.json +21 -0
  58. package/src/ui/vite.config.ts +19 -0
  59. package/src/wf-ui-server/__tests__/a2a-store.test.mjs +79 -0
  60. package/src/wf-ui-server/__tests__/peer-capsule.test.mjs +268 -0
  61. package/src/wf-ui-server/__tests__/pty-adapter.test.mjs +70 -0
  62. package/src/wf-ui-server/__tests__/runtime-config.test.mjs +43 -0
  63. package/src/wf-ui-server/__tests__/runtime-detector.test.mjs +90 -0
  64. package/src/wf-ui-server/__tests__/security.test.mjs +59 -0
  65. package/src/wf-ui-server/__tests__/server.integration.test.mjs +150 -0
  66. package/src/wf-ui-server/__tests__/session-registry.test.mjs +238 -0
  67. package/src/wf-ui-server/__tests__/settings.test.mjs +135 -0
  68. package/src/wf-ui-server/__tests__/task-parser.test.mjs +200 -0
  69. package/src/wf-ui-server/__tests__/terminal-store.test.mjs +138 -0
  70. package/src/wf-ui-server/__tests__/token.test.mjs +48 -0
  71. package/src/wf-ui-server/__tests__/ws-events.integration.test.mjs +419 -0
  72. package/src/wf-ui-server/__tests__/ws-terminal.integration.test.mjs +383 -0
  73. package/src/wf-ui-server/a2a-store.mjs +296 -0
  74. package/src/wf-ui-server/peer-capsule.mjs +213 -0
  75. package/src/wf-ui-server/pty-adapter.mjs +155 -0
  76. package/src/wf-ui-server/runtime-config.mjs +153 -0
  77. package/src/wf-ui-server/runtime-detector.mjs +374 -0
  78. package/src/wf-ui-server/security.mjs +67 -0
  79. package/src/wf-ui-server/server.mjs +849 -0
  80. package/src/wf-ui-server/session-registry.mjs +204 -0
  81. package/src/wf-ui-server/settings.mjs +100 -0
  82. package/src/wf-ui-server/task-parser.mjs +133 -0
  83. package/src/wf-ui-server/terminal-store.mjs +225 -0
  84. package/src/wf-ui-server/token.mjs +41 -0
  85. package/src/wf-ui-server/ws-events.mjs +354 -0
  86. package/src/wf-ui-server/ws-terminal.mjs +473 -0
  87. package/templates/common/.claude/commands/wf-help.md +1 -0
  88. package/templates/common/.claude/commands/wf-task-list.md +2 -0
  89. package/templates/common/.claude/commands/wf-ui.md +26 -0
  90. package/templates/common/.claude/commands/wf-update.md +54 -9
  91. package/templates/common/.claude/rules/ecc/common.md +1 -1
  92. package/templates/common/.claude/skills/wf-auto/SKILL.md +3 -3
  93. package/templates/common/.claude/skills/wf-auto-spark/SKILL.md +2 -2
  94. package/templates/common/.claude/skills/wf-ui/SKILL.md +78 -0
  95. package/templates/common/.claude/skills/wf-update/SKILL.md +55 -58
  96. package/templates/common/.harness-version +63 -39
  97. package/templates/common/.opencode/commands/wf-help.md +1 -0
  98. package/templates/common/.opencode/commands/wf-task-list.md +2 -0
  99. package/templates/common/.opencode/commands/wf-ui.md +26 -0
  100. package/templates/common/.opencode/commands/wf-update.md +54 -9
  101. package/templates/common/CLAUDE.md +1 -1
  102. package/templates/common/Harness/MEMORY.md +2 -0
  103. package/templates/common/Harness/README.md +6 -4
  104. package/templates/common/Harness/a2a/role-graph.json +79 -0
  105. package/templates/common/Harness/a2a/runtime-registry.json +5 -0
  106. package/templates/common/Harness/a2a/skills/terminal-control.json +15 -0
  107. package/templates/common/Harness/ownership.manifest.json +57 -2
  108. package/templates/common/Harness/scripts/README.md +134 -0
  109. package/templates/common/Harness/scripts/a2a-terminal.mjs +191 -0
  110. package/templates/common/Harness/scripts/context-budget.mjs +1 -1
  111. package/templates/common/Harness/scripts/sync-host-global.mjs +278 -0
  112. package/templates/common/Harness/scripts/task-state.mjs +556 -23
  113. package/templates/common/Harness/scripts/validate-harness.mjs +229 -19
  114. package/templates/common/Harness/scripts/wf-remove.mjs +8 -3
  115. package/templates/common/Harness/scripts/wf-update-check.mjs +37 -9
  116. package/templates/common/Harness/scripts/wf-update-runner.mjs +325 -0
  117. package/templates/common/Harness/settings.json +35 -0
  118. package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +7 -2
  119. package/templates/common/Harness/specs/runtime/command-surface.json +14 -0
  120. package/templates/common/Harness/specs/workflows/WF-AUTO-SPARK.md +8 -8
  121. package/templates/common/Harness/specs/workflows/WF-AUTO.md +12 -12
  122. package/templates/common/Harness/tasks/_template/NAMING.md +16 -16
  123. package/templates/common/Harness/tasks/_template/PLAN.md +17 -60
  124. package/templates/common/Harness/tasks/_template/PROBLEM.md +18 -0
  125. package/templates/common/Harness/tasks/_template/PROGRESS.md +9 -14
  126. package/templates/common/Harness/tasks/_template/REFERENCES.md +26 -0
  127. package/templates/common/Harness/tasks/_template/ARTIFACTS.md +0 -3
  128. package/templates/common/Harness/tasks/_template/NOTES.md +0 -3
@@ -11,7 +11,7 @@ const args = process.argv.slice(2);
11
11
  const command = args[0] && !args[0].startsWith('--') ? args[0] : 'help';
12
12
 
13
13
  const OUTER_TASK_CAP = 5;
14
- const RESERVED = new Set(['_template', '_archive', 'auto']);
14
+ const RESERVED = new Set(['_template', '_archive', 'continuous']);
15
15
  const TASK_ID_RE = /^task-[a-z]+(-[a-z0-9]+){1,4}$/;
16
16
  const NEVER_ARCHIVE_STATUSES = new Set([
17
17
  'active',
@@ -108,7 +108,41 @@ function print(payload) {
108
108
  return;
109
109
  }
110
110
 
111
- if (payload.command === 'list' || payload.command === 'validate') {
111
+ if (payload.command === 'list') {
112
+ console.log(`Active Task: ${payload.activeTask || 'None'}`);
113
+ console.log(`Tasks: ${payload.taskCount}`);
114
+ for (const task of payload.tasks || []) {
115
+ const deps = task.dependsOn?.length ? ` dependsOn: [${task.dependsOn.join(', ')}]` : '';
116
+ const blocks = task.blocks?.length ? ` blocks: [${task.blocks.join(', ')}]` : '';
117
+ console.log(`- ${task.id}: status=${task.status || '-'} phase=${task.phase || '-'}${deps}${blocks}`);
118
+ }
119
+
120
+ // Render graph
121
+ const g = payload.graph;
122
+ if (g) {
123
+ console.log('');
124
+ console.log('=== Task Graph ===');
125
+ if (g.roots.length > 0) {
126
+ console.log(`\nRoots (${g.roots.length} tasks, no internal dependencies):`);
127
+ for (const id of g.roots) console.log(` → ${id}`);
128
+ }
129
+ if (g.depEdges.length > 0) {
130
+ console.log(`\nDependency chains (${g.depEdges.length} edges):`);
131
+ for (const e of g.depEdges) console.log(` ${e.from} ──▶ ${e.to}`);
132
+ }
133
+ if (g.blockEdges.length > 0) {
134
+ console.log(`\nBlocks (${g.blockEdges.length} edges):`);
135
+ for (const e of g.blockEdges) console.log(` ${e.from} ▸▸ ${e.to}`);
136
+ }
137
+ if (g.orphanedDeps.length > 0) {
138
+ console.log(`\nOrphaned dependencies (${g.orphanedDeps.length}, target not in active tasks):`);
139
+ for (const o of g.orphanedDeps) console.log(` ${o.task} ──▶ ${o.missingDep} (missing)`);
140
+ }
141
+ if (g.roots.length === 0 && g.depEdges.length === 0 && g.blockEdges.length === 0) {
142
+ console.log(' (no relationships — all tasks are independent)');
143
+ }
144
+ }
145
+ } else if (payload.command === 'validate') {
112
146
  console.log(`Active Task: ${payload.activeTask || 'None'}`);
113
147
  console.log(`Tasks: ${payload.taskCount}`);
114
148
  for (const task of payload.tasks || []) {
@@ -122,7 +156,7 @@ function print(payload) {
122
156
  if (payload.dryRun) console.log('[DRY RUN] No files moved. Use --apply to execute.');
123
157
  console.log(`Scanned: ${payload.scanned}, Archiveable: ${payload.archiveable}, To archive: ${payload.toArchive}, Kept: ${payload.kept}, Skipped: ${payload.skipped}`);
124
158
  for (const r of payload.results) {
125
- const suffix = r.year ? ` -> _archive/${r.year}` : '';
159
+ const suffix = r.path ? ` -> _archive/${r.path}` : '';
126
160
  console.log(`- ${r.action}: ${r.dir} (${r.status})${suffix}`);
127
161
  }
128
162
  } else {
@@ -151,6 +185,14 @@ Commands:
151
185
  set-active <task-id> [--dry-run] Set the single active task.
152
186
  transition <task-id> --status <s> --phase <p> [--dry-run]
153
187
  archive [--dry-run|--apply] [--keep n] [--task id] [--json]
188
+ Archive eligible tasks to _archive/YYYY/MM/DD/.
189
+ Explicit --apply (no --task filter) archives ALL.
190
+ history list [--year YYYY] [--month MM] [--json]
191
+ List archived tasks, optional year/month filter.
192
+ history search <keyword> [--json] Full-text search archived PLAN/PROGRESS/PROBLEM/REFERENCES.
193
+ history load <task-id> [--json] Load one archived task's full record.
194
+ history delete <task-id> [--dry-run|--apply] [--json]
195
+ Delete an archived task (audit trail written).
154
196
  record <task-id> [--create] [--text "description"] [--status <s>] [--mode <m>] [--dry-run|--apply] [--json]
155
197
  Create or update a task record.
156
198
  open [--json] List open (non-archived, active-status) tasks.
@@ -247,6 +289,18 @@ function defaultQueues() {
247
289
  };
248
290
  }
249
291
 
292
+ function defaultTaskRuntime() {
293
+ if (process.env.HARNESS_DEFAULT_RUNTIME) return process.env.HARNESS_DEFAULT_RUNTIME;
294
+ const settingsPath = path.join(harnessDir, 'settings.json');
295
+ try {
296
+ if (!fs.existsSync(settingsPath)) return 'codex';
297
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
298
+ return settings?.terminal?.defaultRuntime || 'codex';
299
+ } catch {
300
+ return 'codex';
301
+ }
302
+ }
303
+
250
304
  const VALID_MODES = new Set([
251
305
  'direct',
252
306
  'wf',
@@ -535,11 +589,14 @@ function desiredStatusForTask(task, activeTask, desiredPhase) {
535
589
  }
536
590
 
537
591
  function defaultState(taskId, status, phase, now) {
592
+ const runtime = defaultTaskRuntime();
538
593
  return {
539
594
  schemaVersion: 1,
540
595
  taskId,
541
596
  status,
542
597
  mode: 'direct',
598
+ defaultRuntime: runtime,
599
+ defaultAgentRuntime: runtime,
543
600
  tier: 'none',
544
601
  phase,
545
602
  gate: null,
@@ -568,6 +625,8 @@ function normalizeState(task, activeTask, now) {
568
625
  state.status = status;
569
626
  state.phase = phase;
570
627
  if (!state.mode) state.mode = 'direct';
628
+ if (!state.defaultRuntime) state.defaultRuntime = defaultTaskRuntime();
629
+ if (!state.defaultAgentRuntime) state.defaultAgentRuntime = state.defaultRuntime;
571
630
  if (!state.tier) state.tier = 'none';
572
631
  if (!Object.prototype.hasOwnProperty.call(state, 'gate')) state.gate = null;
573
632
  if (!Object.prototype.hasOwnProperty.call(state, 'activeQuestion')) state.activeQuestion = null;
@@ -767,6 +826,38 @@ function applyOperations(operations) {
767
826
  }
768
827
  }
769
828
 
829
+ function buildListGraph(expandedTasks) {
830
+ const byId = new Map(expandedTasks.map(t => [t.id, t]));
831
+ const graph = { roots: [], depEdges: [], blockEdges: [], orphanedDeps: [] };
832
+
833
+ // Roots: tasks with no dependsOn pointing to other active tasks (or empty dependsOn)
834
+ // Non-roots: tasks whose dependsOn includes at least one other active task
835
+ const hasInternalDep = new Set();
836
+ const incomingBlocks = new Map(); // taskId → who blocks it (for reverse lookup)
837
+
838
+ for (const task of expandedTasks) {
839
+ for (const depId of task.dependsOn) {
840
+ if (byId.has(depId)) {
841
+ hasInternalDep.add(task.id);
842
+ graph.depEdges.push({ from: depId, to: task.id });
843
+ } else if (depId) {
844
+ graph.orphanedDeps.push({ task: task.id, missingDep: depId });
845
+ }
846
+ }
847
+ for (const blockId of task.blocks) {
848
+ if (byId.has(blockId)) {
849
+ graph.blockEdges.push({ from: task.id, to: blockId });
850
+ }
851
+ if (!incomingBlocks.has(blockId)) incomingBlocks.set(blockId, []);
852
+ incomingBlocks.get(blockId).push(task.id);
853
+ }
854
+ }
855
+
856
+ graph.roots = expandedTasks.filter(t => !hasInternalDep.has(t.id));
857
+
858
+ return graph;
859
+ }
860
+
770
861
  function runList() {
771
862
  const { rootProgress, tasks } = collectTasks();
772
863
 
@@ -789,12 +880,20 @@ function runList() {
789
880
  };
790
881
  });
791
882
 
883
+ const graph = buildListGraph(expandedTasks);
884
+
792
885
  const payload = {
793
886
  ok: true,
794
887
  command: 'list',
795
888
  taskCount: expandedTasks.length,
796
889
  activeTask: rootProgress.activeTask,
797
890
  tasks: expandedTasks,
891
+ graph: {
892
+ roots: graph.roots.map(t => t.id),
893
+ depEdges: graph.depEdges,
894
+ blockEdges: graph.blockEdges,
895
+ orphanedDeps: graph.orphanedDeps,
896
+ },
798
897
  errors: [],
799
898
  warnings: [],
800
899
  };
@@ -874,38 +973,67 @@ function buildArchivePlan() {
874
973
  else skipped.push({ task, reason: eligibility.reason });
875
974
  }
876
975
 
976
+ const dryRun = !hasFlag('--apply');
977
+ const explicitTrigger = dryRun === false && !taskFilter;
877
978
  archiveable.sort((a, b) => a.mtimeMs - b.mtimeMs || a.id.localeCompare(b.id));
878
- const toArchiveCount = taskFilter ? archiveable.length : Math.max(0, tasks.length - keepValue);
979
+
980
+ // Explicit user trigger (no --task filter, with --apply): archive ALL eligible tasks.
981
+ // Explicit --task targets the selected eligible task in both dry-run and apply.
982
+ // Auto/scheduled/dry-run: only archive tasks exceeding --keep cap.
983
+ const toArchiveCount = taskFilter
984
+ ? archiveable.length
985
+ : explicitTrigger
986
+ ? archiveable.length
987
+ : Math.max(0, tasks.length - keepValue);
879
988
  const toArchive = archiveable.slice(0, toArchiveCount);
880
989
  const toArchiveIds = new Set(toArchive.map(task => task.id));
881
990
  const keptArchiveable = archiveable.filter(task => !toArchiveIds.has(task.id));
882
991
 
883
992
  const errors = [];
884
993
  for (const task of toArchive) {
885
- const year = new Date(task.mtimeMs).getFullYear().toString();
886
- const dest = safeTaskPath('_archive', year, task.id);
887
- if (fs.existsSync(dest)) errors.push(`Archive destination already exists: Harness/tasks/_archive/${year}/${task.id}`);
994
+ const mtime = new Date(task.mtimeMs);
995
+ const year = mtime.getFullYear().toString();
996
+ const month = String(mtime.getMonth() + 1).padStart(2, '0');
997
+ const day = String(mtime.getDate()).padStart(2, '0');
998
+ const dest = safeTaskPath('_archive', year, month, day, task.id);
999
+ const relPath = `_archive/${year}/${month}/${day}/${task.id}`;
1000
+ if (fs.existsSync(dest)) errors.push(`Archive destination already exists: Harness/tasks/${relPath}`);
888
1001
  }
889
1002
  if (errors.length) {
890
1003
  return { ok: false, command: 'archive', errors, warnings: [], results: [] };
891
1004
  }
892
1005
 
1006
+ function dateParts(ts) {
1007
+ const d = new Date(ts);
1008
+ return {
1009
+ year: d.getFullYear().toString(),
1010
+ month: String(d.getMonth() + 1).padStart(2, '0'),
1011
+ day: String(d.getDate()).padStart(2, '0'),
1012
+ path: `${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')}`,
1013
+ };
1014
+ }
1015
+
893
1016
  const results = [
894
1017
  ...toArchive.map(task => ({
895
1018
  dir: task.id,
896
- year: new Date(task.mtimeMs).getFullYear().toString(),
1019
+ ...dateParts(task.mtimeMs),
897
1020
  action: hasFlag('--apply') ? 'archived' : 'would-archive',
898
- status: hasFlag('--apply') ? 'moved' : 'dry-run',
1021
+ status: hasFlag('--apply')
1022
+ ? (explicitTrigger ? 'explicit --apply: all eligible' : 'moved')
1023
+ : (taskFilter ? 'dry-run' : `dry-run (${toArchiveCount} of ${archiveable.length} eligible)`),
899
1024
  })),
900
1025
  ...keptArchiveable.map(task => ({
901
1026
  dir: task.id,
902
- year: new Date(task.mtimeMs).getFullYear().toString(),
1027
+ ...dateParts(task.mtimeMs),
903
1028
  action: 'kept',
904
1029
  status: `kept by --keep ${keepValue}`,
905
1030
  })),
906
1031
  ...skipped.map(({ task, reason }) => ({
907
1032
  dir: task.id,
908
1033
  year: null,
1034
+ month: null,
1035
+ day: null,
1036
+ path: null,
909
1037
  action: 'skipped',
910
1038
  status: reason,
911
1039
  })),
@@ -927,18 +1055,195 @@ function buildArchivePlan() {
927
1055
  toArchiveIds,
928
1056
  tasks,
929
1057
  rootProgress,
1058
+ graphGenerated: hasFlag('--apply') && archiveable.length > 0,
930
1059
  };
931
1060
  }
932
1061
 
933
- function appendArchiveIndex(entries) {
934
- if (entries.length === 0) return;
1062
+ function buildArchiveGraphIndex() {
1063
+ // Walk _archive/YYYY/MM/DD/task-id/ for all archived tasks
1064
+ const allArchived = [];
1065
+ if (fs.existsSync(archiveDir)) {
1066
+ const yearDirs = fs.readdirSync(archiveDir, { withFileTypes: true })
1067
+ .filter(e => e.isDirectory() && /^\d{4}$/.test(e.name))
1068
+ .map(e => e.name).sort();
1069
+ for (const year of yearDirs) {
1070
+ const yearPath = path.join(archiveDir, year);
1071
+ const monthDirs = fs.readdirSync(yearPath, { withFileTypes: true })
1072
+ .filter(e => e.isDirectory() && /^\d{2}$/.test(e.name))
1073
+ .map(e => e.name).sort();
1074
+ for (const month of monthDirs) {
1075
+ const monthPath = path.join(yearPath, month);
1076
+ const dayDirs = fs.readdirSync(monthPath, { withFileTypes: true })
1077
+ .filter(e => e.isDirectory() && /^\d{2}$/.test(e.name))
1078
+ .map(e => e.name).sort();
1079
+ for (const day of dayDirs) {
1080
+ const dayPath = path.join(monthPath, day);
1081
+ const taskDirs = fs.readdirSync(dayPath, { withFileTypes: true })
1082
+ .filter(e => e.isDirectory()).map(e => e.name);
1083
+ for (const taskId of taskDirs) {
1084
+ const statePath = path.join(dayPath, taskId, 'STATE.json');
1085
+ const state = fs.existsSync(statePath) ? JSON.parse(fs.readFileSync(statePath, 'utf8')) : null;
1086
+ const planPath = path.join(dayPath, taskId, 'PLAN.md');
1087
+ const planText = fs.existsSync(planPath) ? fs.readFileSync(planPath, 'utf8') : '';
1088
+ const progressPath2 = path.join(dayPath, taskId, 'PROGRESS.md');
1089
+ const progressText = fs.existsSync(progressPath2) ? fs.readFileSync(progressPath2, 'utf8') : '';
1090
+ allArchived.push({ id: taskId, year, month, day, state, planText, progressText });
1091
+ }
1092
+ }
1093
+ }
1094
+ }
1095
+ }
1096
+
1097
+ if (allArchived.length === 0) return null;
1098
+
1099
+ const byId = new Map(allArchived.map(t => [t.id, t]));
1100
+ const graphLines = [];
1101
+ graphLines.push('## Task Graph\n');
1102
+
1103
+ // Identify root tasks (no dependsOn, or all dependsOn are archived but not in this set)
1104
+ const hasDep = new Set();
1105
+ for (const task of allArchived) {
1106
+ const deps = task.state?.links?.dependsOn || [];
1107
+ for (const depId of deps) {
1108
+ if (byId.has(depId)) hasDep.add(task.id);
1109
+ }
1110
+ }
1111
+
1112
+ // Root tasks
1113
+ const roots = allArchived.filter(t => !hasDep.has(t.id));
1114
+ if (roots.length > 0) {
1115
+ graphLines.push('### Roots (no dependencies)');
1116
+ for (const task of roots) {
1117
+ const deps = (task.state?.links?.dependsOn || []).map(id => `\`${id}\``).join(', ') || 'none';
1118
+ const blocks = (task.state?.links?.blocks || []).map(id => `\`${id}\``).join(', ') || 'none';
1119
+ graphLines.push(`- \`${task.id}\` → blocks: ${blocks}`);
1120
+ }
1121
+ }
1122
+
1123
+ // Build dependency chains
1124
+ const visited = new Set();
1125
+ function chainLines(taskId, indent) {
1126
+ if (visited.has(taskId)) return [];
1127
+ visited.add(taskId);
1128
+ const task = byId.get(taskId);
1129
+ if (!task) return [];
1130
+ const deps = task.state?.links?.dependsOn || [];
1131
+ const lines = [];
1132
+ if (deps.length > 0) {
1133
+ const depList = deps.map(depId => {
1134
+ const depTask = byId.get(depId);
1135
+ return depTask
1136
+ ? `[\`${depId}\`](#${depId.toLowerCase().replace(/-/g, '')})`
1137
+ : `\`${depId}\``;
1138
+ }).join(' → ');
1139
+ lines.push(`${indent}- \`${taskId}\` ← depends on: ${depList}`);
1140
+ } else {
1141
+ lines.push(`${indent}- \`${taskId}\``);
1142
+ }
1143
+ for (const depId of deps) {
1144
+ lines.push(...chainLines(depId, indent + ' '));
1145
+ }
1146
+ return lines;
1147
+ }
1148
+
1149
+ // Show dependency relationships
1150
+ const depGraph = [];
1151
+ for (const task of allArchived) {
1152
+ const deps = task.state?.links?.dependsOn || [];
1153
+ for (const depId of deps) {
1154
+ if (byId.has(depId)) {
1155
+ depGraph.push({ from: depId, to: task.id });
1156
+ }
1157
+ }
1158
+ }
1159
+
1160
+ if (depGraph.length > 0) {
1161
+ graphLines.push('\n### Dependencies');
1162
+ graphLines.push('```\n' + depGraph.map(e => ` ${e.from} ──▶ ${e.to}`).join('\n') + '\n```\n');
1163
+ graphLines.push('| From | To |');
1164
+ graphLines.push('|------|----|');
1165
+ for (const e of depGraph) {
1166
+ graphLines.push(`| \`${e.from}\` | \`${e.to}\` |`);
1167
+ }
1168
+ }
1169
+
1170
+ // Blocks relationships
1171
+ const blocksGraph = [];
1172
+ for (const task of allArchived) {
1173
+ const blocks = task.state?.links?.blocks || [];
1174
+ for (const blockId of blocks) {
1175
+ if (byId.has(blockId)) {
1176
+ blocksGraph.push({ from: task.id, to: blockId });
1177
+ }
1178
+ }
1179
+ }
1180
+ if (blocksGraph.length > 0) {
1181
+ graphLines.push('\n### Blocks (completion triggers)');
1182
+ graphLines.push('| Task | Unblocks |');
1183
+ graphLines.push('|------|----------|');
1184
+ for (const e of blocksGraph) {
1185
+ graphLines.push(`| \`${e.from}\` | \`${e.to}\` |`);
1186
+ }
1187
+ }
1188
+
1189
+ // Per-task detail section
1190
+ graphLines.push('\n## Task Details\n');
1191
+ const detailOrder = allArchived.slice().sort((a, b) => {
1192
+ if (a.year !== b.year) return b.year.localeCompare(a.year);
1193
+ if (a.month !== b.month) return b.month.localeCompare(a.month);
1194
+ if (a.day !== b.day) return b.day.localeCompare(a.day);
1195
+ return a.id.localeCompare(b.id);
1196
+ });
1197
+ for (const task of detailOrder) {
1198
+ const anchor = task.id.toLowerCase().replace(/-/g, '');
1199
+ const phase = task.state?.phase || '-';
1200
+ const status = task.state?.status || '-';
1201
+ const goal = (task.planText.match(/^## Goal\s*\n+([\s\S]*?)(?=\n## |$)/mi) || ['', ''])[1].trim().slice(0, 120) || (task.state?.nextAction || '-');
1202
+ const deps = (task.state?.links?.dependsOn || []).join(', ') || '-';
1203
+ const blocks = (task.state?.links?.blocks || []).join(', ') || '-';
1204
+ graphLines.push(`### \`${task.id}\` {#${anchor}}`);
1205
+ graphLines.push(`- **Archived:** ${task.year}/${task.month}/${task.day}`);
1206
+ graphLines.push(`- **Status:** ${status} **Phase:** ${phase}`);
1207
+ graphLines.push(`- **Goal:** ${goal.slice(0, 120)}${goal !== '-' && goal.length >= 120 ? '...' : ''}`);
1208
+ graphLines.push(`- **Depends on:** ${deps} **Blocks:** ${blocks}`);
1209
+ }
1210
+
1211
+ return graphLines.join('\n');
1212
+ }
1213
+
1214
+ function appendArchiveIndex(entries, graphContent) {
1215
+ if (entries.length === 0 && !graphContent) return;
935
1216
  const indexPath = path.join(archiveDir, 'INDEX.md');
936
- const existing = fs.existsSync(indexPath)
937
- ? fs.readFileSync(indexPath, 'utf8').trimEnd() + '\n'
938
- : '| Task | Year | Archived |\n|------|------|----------|\n';
939
1217
  const date = new Date().toISOString().slice(0, 10);
940
- const lines = entries.map(entry => `| ${entry.id} | ${entry.year} | ${date} |`).join('\n');
941
- writeTextAtomic(indexPath, `${existing}${lines}\n`);
1218
+
1219
+ let existing = '';
1220
+ if (fs.existsSync(indexPath)) {
1221
+ const raw = fs.readFileSync(indexPath, 'utf8');
1222
+ // Strip old graph section if present so we regenerate fresh
1223
+ const graphStart = raw.indexOf('\n## Task Graph\n');
1224
+ existing = graphStart >= 0 ? raw.slice(0, graphStart).trimEnd() : raw.trimEnd();
1225
+ }
1226
+ if (existing && !existing.endsWith('\n')) existing += '\n';
1227
+
1228
+ const lines = [];
1229
+ lines.push(existing);
1230
+ if (entries.length > 0) {
1231
+ if (!existing || !existing.includes('| Task | Path | Archived |')) {
1232
+ lines.push('| Task | Path | Archived |');
1233
+ lines.push('|------|------|----------|');
1234
+ }
1235
+ for (const entry of entries) {
1236
+ const entryPath = `${entry.year}/${entry.month}/${entry.day}`;
1237
+ lines.push(`| \`${entry.id}\` | ${entryPath} | ${date} |`);
1238
+ }
1239
+ }
1240
+
1241
+ if (graphContent) {
1242
+ lines.push('');
1243
+ lines.push(graphContent);
1244
+ }
1245
+
1246
+ writeTextAtomic(indexPath, lines.join('\n') + '\n');
942
1247
  }
943
1248
 
944
1249
  function runArchive() {
@@ -949,8 +1254,8 @@ function runArchive() {
949
1254
  const indexEntries = [];
950
1255
  for (const result of plan.results.filter(result => result.action === 'archived')) {
951
1256
  const src = safeTaskPath(result.dir);
952
- const destDir = safeTaskPath('_archive', result.year);
953
- const dest = safeTaskPath('_archive', result.year, result.dir);
1257
+ const destDir = safeTaskPath('_archive', result.year, result.month, result.day);
1258
+ const dest = safeTaskPath('_archive', result.year, result.month, result.day, result.dir);
954
1259
  fs.mkdirSync(destDir, { recursive: true });
955
1260
  fs.renameSync(src, dest);
956
1261
  const movedStatePath = path.join(dest, 'STATE.json');
@@ -963,9 +1268,10 @@ function runArchive() {
963
1268
  writeJsonAtomic(movedStatePath, movedState);
964
1269
  const movedProgressPath = path.join(dest, 'PROGRESS.md');
965
1270
  writeTextAtomic(movedProgressPath, renderTaskProgress(readText(movedProgressPath), result.dir, movedState));
966
- indexEntries.push({ id: result.dir, year: result.year });
1271
+ indexEntries.push({ id: result.dir, year: result.year, month: result.month, day: result.day });
967
1272
  }
968
- appendArchiveIndex(indexEntries);
1273
+ const graphContent = buildArchiveGraphIndex();
1274
+ appendArchiveIndex(indexEntries, graphContent);
969
1275
 
970
1276
  for (const task of plan.tasks) {
971
1277
  task.desiredState = task.state || defaultState(task.id, task.status, task.phase || 'intake', new Date().toISOString());
@@ -1002,7 +1308,7 @@ function generateTaskId(title, note, context) {
1002
1308
  // fallback: empty slug (pure CJK/emoji) defaults to 'task'
1003
1309
  const slug = parts.slice(0, 25) || 'task';
1004
1310
  const now = new Date();
1005
- const suffix = `${now.getFullYear().toString().slice(2)}${String(now.getMonth()+1).padStart(2,'0')}${String(now.getDate()).padStart(2,'0')}`;
1311
+ const suffix = `${String(now.getMonth()+1).padStart(2,'0')}${String(now.getDate()).padStart(2,'0')}`;
1006
1312
  return `task-${slug}-${suffix}`;
1007
1313
  }
1008
1314
 
@@ -1158,7 +1464,15 @@ function runRecord() {
1158
1464
 
1159
1465
  const planFile = path.join(taskDir, 'PLAN.md');
1160
1466
  if (!fs.existsSync(planFile)) {
1161
- writeTextAtomic(planFile, `# ${taskId} - PLAN\n\n## Goal\n\n${text || taskTitle(taskId)}\n\n## Scope\n\n.\n\n## Decisions\n\n.\n\n## Acceptance\n\n.\n`);
1467
+ writeTextAtomic(planFile, `# ${taskId} - PLAN\n\n## Goal\n\n${text || taskTitle(taskId)}\n\n## Scope\n\nWrite set:\n-\n\nForbidden:\n-\n\n## Decisions\n\n| # | Decision | Reason | Date |\n|---|----------|--------|------|\n\n## Acceptance\n\n| ID | Criterion | Evidence | Status |\n|----|-----------|----------|--------|\n| AC-001 | | | pending |\n\n## Risks\n\n| Risk | Mitigation | Status |\n|------|------------|--------|\n`);
1468
+ }
1469
+ const problemFile = path.join(taskDir, 'PROBLEM.md');
1470
+ if (!fs.existsSync(problemFile)) {
1471
+ writeTextAtomic(problemFile, `# ${taskId} - PROBLEM\n\n## Active\n\n| ID | Problem | Root cause | Fix | Status |\n|----|---------|------------|-----|--------|\n\n## Resolved\n\n| ID | Problem | Root cause | Fix | Resolved |\n|----|---------|------------|-----|----------|\n`);
1472
+ }
1473
+ const refFile = path.join(taskDir, 'REFERENCES.md');
1474
+ if (!fs.existsSync(refFile)) {
1475
+ writeTextAtomic(refFile, `# ${taskId} - REFERENCES\n\n## Logs\n\n| Description | File / Command | Date |\n|-------------|---------------|------|\n\n## Evidence\n\n| What | Pointer | Verified |\n|------|---------|----------|\n\n## Links\n\n| Description | URL / Path |\n|-------------|------------|\n\n## Notes\n\n-\n`);
1162
1476
  }
1163
1477
 
1164
1478
  const rootText = readText(progressPath);
@@ -1186,6 +1500,8 @@ function runRecord() {
1186
1500
  const now = new Date().toISOString();
1187
1501
  const updated = { ...existing.state };
1188
1502
  updated.updatedAt = now;
1503
+ if (!updated.defaultRuntime) updated.defaultRuntime = defaultTaskRuntime();
1504
+ if (!updated.defaultAgentRuntime) updated.defaultAgentRuntime = updated.defaultRuntime;
1189
1505
  if (statusRaw) {
1190
1506
  const ns = normalizeStatus(statusRaw);
1191
1507
  if (!ns) {
@@ -1261,6 +1577,211 @@ function runOpen() {
1261
1577
  }, 0);
1262
1578
  }
1263
1579
 
1580
+ // ---- history ----
1581
+ function walkArchived(filter = {}) {
1582
+ const results = [];
1583
+ if (!fs.existsSync(archiveDir)) return results;
1584
+
1585
+ function addTask(taskPath, year, month, day) {
1586
+ const taskId = path.basename(taskPath);
1587
+ if (taskId.startsWith('_') || taskId.startsWith('.')) return;
1588
+ if (filter.taskId && taskId !== filter.taskId) return;
1589
+ const statePath = path.join(taskPath, 'STATE.json');
1590
+ const state = fs.existsSync(statePath) ? JSON.parse(fs.readFileSync(statePath, 'utf8')) : null;
1591
+ results.push({
1592
+ id: taskId,
1593
+ year, month, day,
1594
+ archivePath: `${year}/${month}/${day}`,
1595
+ fullPath: taskPath,
1596
+ mtime: fs.statSync(taskPath).mtime.toISOString(),
1597
+ state,
1598
+ });
1599
+ }
1600
+
1601
+ // Walk new layout: _archive/YYYY/MM/DD/task-id/
1602
+ // Walk old layout: _archive/YYYY/task-id/ (legacy, auto-assign month=01, day=01)
1603
+ const entries = fs.readdirSync(archiveDir, { withFileTypes: true });
1604
+ const yearDirs = entries
1605
+ .filter(e => e.isDirectory() && /^\d{4}$/.test(e.name) && (!filter.year || e.name === filter.year))
1606
+ .map(e => e.name).sort();
1607
+
1608
+ for (const year of yearDirs) {
1609
+ const yearPath = path.join(archiveDir, year);
1610
+ const yearEntries = fs.readdirSync(yearPath, { withFileTypes: true });
1611
+
1612
+ // New layout: month dirs (two-digit)
1613
+ const monthDirs = yearEntries
1614
+ .filter(e => e.isDirectory() && /^\d{2}$/.test(e.name) && (!filter.month || e.name === filter.month));
1615
+ const hasMonthDirs = monthDirs.length > 0;
1616
+
1617
+ if (hasMonthDirs) {
1618
+ for (const month of monthDirs.map(e => e.name).sort()) {
1619
+ const monthPath = path.join(yearPath, month);
1620
+ const dayDirs = fs.readdirSync(monthPath, { withFileTypes: true })
1621
+ .filter(e => e.isDirectory() && /^\d{2}$/.test(e.name))
1622
+ .map(e => e.name).sort();
1623
+ for (const day of dayDirs) {
1624
+ const dayPath = path.join(monthPath, day);
1625
+ const taskDirs = fs.readdirSync(dayPath, { withFileTypes: true })
1626
+ .filter(e => e.isDirectory());
1627
+ for (const taskDir of taskDirs) {
1628
+ addTask(path.join(dayPath, taskDir.name), year, month, day);
1629
+ }
1630
+ }
1631
+ }
1632
+ }
1633
+
1634
+ // Legacy layout: task dirs directly under year
1635
+ const legacyTasks = yearEntries
1636
+ .filter(e => e.isDirectory() && !/^\d{2}$/.test(e.name) && e.name !== '_deleted.jsonl');
1637
+ for (const taskDir of legacyTasks) {
1638
+ addTask(path.join(yearPath, taskDir.name), year, '01', '01');
1639
+ }
1640
+ }
1641
+ return results;
1642
+ }
1643
+
1644
+ function searchArchivedText(keyword) {
1645
+ const results = [];
1646
+ const archived = walkArchived();
1647
+ const kw = keyword.toLowerCase();
1648
+ for (const task of archived) {
1649
+ const files = ['PLAN.md', 'PROGRESS.md', 'PROBLEM.md', 'REFERENCES.md', 'STATE.json'];
1650
+ const hits = [];
1651
+ for (const f of files) {
1652
+ const fp = path.join(task.fullPath, f);
1653
+ if (!fs.existsSync(fp)) continue;
1654
+ const content = fs.readFileSync(fp, 'utf8');
1655
+ const lines = content.split('\n');
1656
+ for (let i = 0; i < lines.length; i++) {
1657
+ if (lines[i].toLowerCase().includes(kw)) {
1658
+ hits.push({ file: f, line: i + 1, snippet: lines[i].trim().slice(0, 120) });
1659
+ }
1660
+ }
1661
+ }
1662
+ if (hits.length > 0) {
1663
+ results.push({ id: task.id, archivePath: task.archivePath, hits });
1664
+ }
1665
+ }
1666
+ return results;
1667
+ }
1668
+
1669
+ function runHistoryList() {
1670
+ const year = flagValue('--year');
1671
+ const month = flagValue('--month');
1672
+ const tasks = walkArchived({ year, month });
1673
+ const payload = {
1674
+ ok: true,
1675
+ command: 'history/list',
1676
+ count: tasks.length,
1677
+ tasks: tasks.map(t => ({
1678
+ id: t.id,
1679
+ archivePath: t.archivePath,
1680
+ mtime: t.mtime,
1681
+ status: t.state?.status || '-',
1682
+ phase: t.state?.phase || '-',
1683
+ })),
1684
+ };
1685
+ finish(payload, 0);
1686
+ }
1687
+
1688
+ function runHistorySearch() {
1689
+ const keyword = args[2] || flagValue('--keyword');
1690
+ if (!keyword) {
1691
+ finish({ ok: false, command: 'history/search', errors: ['history search requires <keyword>'], warnings: [], count: 0, results: [] }, 1);
1692
+ return;
1693
+ }
1694
+ const results = searchArchivedText(keyword);
1695
+ finish({
1696
+ ok: true,
1697
+ command: 'history/search',
1698
+ keyword,
1699
+ count: results.length,
1700
+ results,
1701
+ }, 0);
1702
+ }
1703
+
1704
+ function runHistoryLoad() {
1705
+ const taskId = args[2];
1706
+ if (!taskId) {
1707
+ finish({ ok: false, command: 'history/load', errors: ['history load requires <task-id>'], warnings: [] }, 1);
1708
+ return;
1709
+ }
1710
+ const tasks = walkArchived({ taskId });
1711
+ if (tasks.length === 0) {
1712
+ finish({ ok: false, command: 'history/load', errors: [`Task "${taskId}" not found in archive`], warnings: [] }, 1);
1713
+ return;
1714
+ }
1715
+ const task = tasks[0];
1716
+ const result = {
1717
+ id: task.id,
1718
+ archivePath: task.archivePath,
1719
+ mtime: task.mtime,
1720
+ state: task.state,
1721
+ files: {},
1722
+ };
1723
+ for (const f of ['PLAN.md', 'PROGRESS.md', 'PROBLEM.md', 'REFERENCES.md']) {
1724
+ const fp = path.join(task.fullPath, f);
1725
+ if (fs.existsSync(fp)) {
1726
+ result.files[f] = fs.readFileSync(fp, 'utf8');
1727
+ }
1728
+ }
1729
+ finish({ ok: true, command: 'history/load', task: result }, 0);
1730
+ }
1731
+
1732
+ function runHistoryDelete() {
1733
+ const taskId = args[2];
1734
+ if (!taskId) {
1735
+ finish({ ok: false, command: 'history/delete', errors: ['history delete requires <task-id>'], warnings: [] }, 1);
1736
+ return;
1737
+ }
1738
+ const tasks = walkArchived({ taskId });
1739
+ if (tasks.length === 0) {
1740
+ finish({ ok: false, command: 'history/delete', errors: [`Task "${taskId}" not found in archive`], warnings: [] }, 1);
1741
+ return;
1742
+ }
1743
+ const task = tasks[0];
1744
+ if (hasFlag('--apply')) {
1745
+ // Audit: write a deletion record before removing
1746
+ const auditPath = path.join(archiveDir, '_deleted.jsonl');
1747
+ const auditEntry = JSON.stringify({
1748
+ id: task.id,
1749
+ archivePath: task.archivePath,
1750
+ deletedAt: new Date().toISOString(),
1751
+ deletedBy: process.env.USER || process.env.USERNAME || 'unknown',
1752
+ });
1753
+ fs.appendFileSync(auditPath, auditEntry + '\n');
1754
+ fs.rmSync(task.fullPath, { recursive: true, force: true });
1755
+ // Clean up empty day/month/year dirs
1756
+ const dayPath = path.dirname(task.fullPath);
1757
+ const monthPath = path.dirname(dayPath);
1758
+ const yearPath = path.dirname(monthPath);
1759
+ for (const dir of [dayPath, monthPath, yearPath]) {
1760
+ try {
1761
+ if (fs.readdirSync(dir).length === 0) fs.rmdirSync(dir);
1762
+ } catch {}
1763
+ }
1764
+ finish({
1765
+ ok: true,
1766
+ command: 'history/delete',
1767
+ action: 'deleted',
1768
+ taskId: task.id,
1769
+ archivePath: task.archivePath,
1770
+ auditSaved: true,
1771
+ }, 0);
1772
+ } else {
1773
+ finish({
1774
+ ok: true,
1775
+ command: 'history/delete',
1776
+ dryRun: true,
1777
+ taskId: task.id,
1778
+ archivePath: task.archivePath,
1779
+ message: 'Dry run. Use --apply to delete.',
1780
+ }, 0);
1781
+ }
1782
+ }
1783
+
1784
+ // ---- dispatch ----
1264
1785
  if (command === 'help' || hasFlag('--help') || hasFlag('-h')) usage();
1265
1786
  if (command === 'list') runList();
1266
1787
  if (command === 'validate') runValidate();
@@ -1270,6 +1791,18 @@ if (command === 'transition') runTransition();
1270
1791
  if (command === 'archive') runArchive();
1271
1792
  if (command === 'record') runRecord();
1272
1793
  if (command === 'open') runOpen();
1794
+ if (command === 'history') {
1795
+ const sub = args[1];
1796
+ if (sub === 'list') runHistoryList();
1797
+ else if (sub === 'search') runHistorySearch();
1798
+ else if (sub === 'load') runHistoryLoad();
1799
+ else if (sub === 'delete') runHistoryDelete();
1800
+ else finish({
1801
+ ok: false, command: 'history',
1802
+ errors: [`Unknown history subcommand "${sub}". Try: list, search <kw>, load <id>, delete <id>`],
1803
+ warnings: [],
1804
+ }, 1);
1805
+ }
1273
1806
 
1274
1807
  finish({
1275
1808
  ok: false,