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.
- package/CHANGELOG.md +13 -0
- package/README.md +1 -0
- package/package.json +5 -2
- package/src/index.js +383 -267
- package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js +17 -0
- package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js.map +1 -0
- package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js +7 -0
- package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js.map +1 -0
- package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js +12 -0
- package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js.map +1 -0
- package/src/ui/dist/assets/TaskList-BN4r8s1i.js +50 -0
- package/src/ui/dist/assets/TaskList-BN4r8s1i.js.map +1 -0
- package/src/ui/dist/assets/TerminalDrawer-6GBZ9nXN.css +32 -0
- package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js +74 -0
- package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js.map +1 -0
- package/src/ui/dist/assets/WorkflowRoute-BnuhLJ6X.css +1 -0
- package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js +39 -0
- package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js.map +1 -0
- package/src/ui/dist/assets/index-BOVYWntB.js +117 -0
- package/src/ui/dist/assets/index-BOVYWntB.js.map +1 -0
- package/src/ui/dist/assets/index-DaUObq0H.css +1 -0
- package/src/ui/dist/assets/maximize-2-LNrr_rKr.js +7 -0
- package/src/ui/dist/assets/maximize-2-LNrr_rKr.js.map +1 -0
- package/src/ui/dist/assets/plus-QuTJyoT3.js +7 -0
- package/src/ui/dist/assets/plus-QuTJyoT3.js.map +1 -0
- package/src/ui/dist/assets/proxy-D0vUbds5.js +2 -0
- package/src/ui/dist/assets/proxy-D0vUbds5.js.map +1 -0
- package/src/ui/dist/assets/refresh-cw-JNmExijd.js +7 -0
- package/src/ui/dist/assets/refresh-cw-JNmExijd.js.map +1 -0
- package/src/ui/dist/assets/terminal-BBQvnOAK.js +7 -0
- package/src/ui/dist/assets/terminal-BBQvnOAK.js.map +1 -0
- package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js +7 -0
- package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js.map +1 -0
- package/src/ui/dist/assets/x-DoIOGAJz.js +7 -0
- package/src/ui/dist/assets/x-DoIOGAJz.js.map +1 -0
- package/src/ui/dist/index.html +18 -0
- package/src/ui/index.html +17 -0
- package/src/ui/package.json +33 -0
- package/src/ui/pnpm-lock.yaml +1582 -0
- package/src/ui/pnpm-workspace.yaml +2 -0
- package/src/ui/src/App.tsx +86 -0
- package/src/ui/src/api.ts +93 -0
- package/src/ui/src/components/AgentsRoute.tsx +503 -0
- package/src/ui/src/components/Footer.tsx +69 -0
- package/src/ui/src/components/Header.tsx +175 -0
- package/src/ui/src/components/LoadingView.tsx +22 -0
- package/src/ui/src/components/RolesRoute.tsx +169 -0
- package/src/ui/src/components/SettingsRoute.tsx +166 -0
- package/src/ui/src/components/TaskList.tsx +388 -0
- package/src/ui/src/components/TerminalDrawer.tsx +611 -0
- package/src/ui/src/components/WorkflowRoute.tsx +670 -0
- package/src/ui/src/hooks/useReducedMotion.ts +17 -0
- package/src/ui/src/hooks/useServerConnection.ts +114 -0
- package/src/ui/src/index.css +281 -0
- package/src/ui/src/main.tsx +11 -0
- package/src/ui/src/types.ts +108 -0
- package/src/ui/tsconfig.json +21 -0
- package/src/ui/vite.config.ts +19 -0
- package/src/wf-ui-server/__tests__/a2a-store.test.mjs +79 -0
- package/src/wf-ui-server/__tests__/peer-capsule.test.mjs +268 -0
- package/src/wf-ui-server/__tests__/pty-adapter.test.mjs +70 -0
- package/src/wf-ui-server/__tests__/runtime-config.test.mjs +43 -0
- package/src/wf-ui-server/__tests__/runtime-detector.test.mjs +90 -0
- package/src/wf-ui-server/__tests__/security.test.mjs +59 -0
- package/src/wf-ui-server/__tests__/server.integration.test.mjs +150 -0
- package/src/wf-ui-server/__tests__/session-registry.test.mjs +238 -0
- package/src/wf-ui-server/__tests__/settings.test.mjs +135 -0
- package/src/wf-ui-server/__tests__/task-parser.test.mjs +200 -0
- package/src/wf-ui-server/__tests__/terminal-store.test.mjs +138 -0
- package/src/wf-ui-server/__tests__/token.test.mjs +48 -0
- package/src/wf-ui-server/__tests__/ws-events.integration.test.mjs +419 -0
- package/src/wf-ui-server/__tests__/ws-terminal.integration.test.mjs +383 -0
- package/src/wf-ui-server/a2a-store.mjs +296 -0
- package/src/wf-ui-server/peer-capsule.mjs +213 -0
- package/src/wf-ui-server/pty-adapter.mjs +155 -0
- package/src/wf-ui-server/runtime-config.mjs +153 -0
- package/src/wf-ui-server/runtime-detector.mjs +374 -0
- package/src/wf-ui-server/security.mjs +67 -0
- package/src/wf-ui-server/server.mjs +849 -0
- package/src/wf-ui-server/session-registry.mjs +204 -0
- package/src/wf-ui-server/settings.mjs +100 -0
- package/src/wf-ui-server/task-parser.mjs +133 -0
- package/src/wf-ui-server/terminal-store.mjs +225 -0
- package/src/wf-ui-server/token.mjs +41 -0
- package/src/wf-ui-server/ws-events.mjs +354 -0
- package/src/wf-ui-server/ws-terminal.mjs +473 -0
- package/templates/common/.claude/commands/wf-help.md +1 -0
- package/templates/common/.claude/commands/wf-task-list.md +2 -0
- package/templates/common/.claude/commands/wf-ui.md +26 -0
- package/templates/common/.claude/commands/wf-update.md +54 -9
- package/templates/common/.claude/rules/ecc/common.md +1 -1
- package/templates/common/.claude/skills/wf-auto/SKILL.md +3 -3
- package/templates/common/.claude/skills/wf-auto-spark/SKILL.md +2 -2
- package/templates/common/.claude/skills/wf-ui/SKILL.md +78 -0
- package/templates/common/.claude/skills/wf-update/SKILL.md +55 -58
- package/templates/common/.harness-version +63 -39
- package/templates/common/.opencode/commands/wf-help.md +1 -0
- package/templates/common/.opencode/commands/wf-task-list.md +2 -0
- package/templates/common/.opencode/commands/wf-ui.md +26 -0
- package/templates/common/.opencode/commands/wf-update.md +54 -9
- package/templates/common/CLAUDE.md +1 -1
- package/templates/common/Harness/MEMORY.md +2 -0
- package/templates/common/Harness/README.md +6 -4
- package/templates/common/Harness/a2a/role-graph.json +79 -0
- package/templates/common/Harness/a2a/runtime-registry.json +5 -0
- package/templates/common/Harness/a2a/skills/terminal-control.json +15 -0
- package/templates/common/Harness/ownership.manifest.json +57 -2
- package/templates/common/Harness/scripts/README.md +134 -0
- package/templates/common/Harness/scripts/a2a-terminal.mjs +191 -0
- package/templates/common/Harness/scripts/context-budget.mjs +1 -1
- package/templates/common/Harness/scripts/sync-host-global.mjs +278 -0
- package/templates/common/Harness/scripts/task-state.mjs +556 -23
- package/templates/common/Harness/scripts/validate-harness.mjs +229 -19
- package/templates/common/Harness/scripts/wf-remove.mjs +8 -3
- package/templates/common/Harness/scripts/wf-update-check.mjs +37 -9
- package/templates/common/Harness/scripts/wf-update-runner.mjs +325 -0
- package/templates/common/Harness/settings.json +35 -0
- package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +7 -2
- package/templates/common/Harness/specs/runtime/command-surface.json +14 -0
- package/templates/common/Harness/specs/workflows/WF-AUTO-SPARK.md +8 -8
- package/templates/common/Harness/specs/workflows/WF-AUTO.md +12 -12
- package/templates/common/Harness/tasks/_template/NAMING.md +16 -16
- package/templates/common/Harness/tasks/_template/PLAN.md +17 -60
- package/templates/common/Harness/tasks/_template/PROBLEM.md +18 -0
- package/templates/common/Harness/tasks/_template/PROGRESS.md +9 -14
- package/templates/common/Harness/tasks/_template/REFERENCES.md +26 -0
- package/templates/common/Harness/tasks/_template/ARTIFACTS.md +0 -3
- 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', '
|
|
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'
|
|
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.
|
|
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
|
-
|
|
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
|
|
886
|
-
const
|
|
887
|
-
|
|
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
|
-
|
|
1019
|
+
...dateParts(task.mtimeMs),
|
|
897
1020
|
action: hasFlag('--apply') ? 'archived' : 'would-archive',
|
|
898
|
-
status: hasFlag('--apply')
|
|
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
|
-
|
|
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
|
|
934
|
-
|
|
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
|
-
|
|
941
|
-
|
|
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
|
-
|
|
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 = `${
|
|
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
|
|
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,
|