create-harness-vibe-coding 0.8.17 → 0.8.18
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 +9 -0
- package/README-CN.md +2 -0
- package/README.md +2 -0
- package/package.json +1 -1
- package/src/generator.js +613 -97
- package/src/index.js +173 -42
- package/src/prompts.js +18 -0
- package/templates/common/.claude/commands/wf-command-create.md +58 -0
- package/templates/common/.claude/commands/wf-help.md +4 -0
- package/templates/common/.claude/commands/wf-task-archive.md +26 -0
- package/templates/common/.claude/commands/wf-task-list.md +24 -0
- package/templates/common/.claude/commands/wf-task-record.md +24 -0
- package/templates/common/.claude/rules/ecc/common.md +1 -1
- package/templates/common/.claude/skills/wf-agents-docs/SKILL.md +15 -30
- package/templates/common/.claude/skills/wf-command-create/SKILL.md +37 -0
- package/templates/common/.claude/skills/wf-max/SKILL.md +1 -1
- package/templates/common/.claude/skills/wf-review/SKILL.md +29 -2
- package/templates/common/.claude/skills/wf-task-archive/SKILL.md +28 -0
- package/templates/common/.claude/skills/wf-task-list/SKILL.md +28 -0
- package/templates/common/.claude/skills/wf-task-record/SKILL.md +28 -0
- package/templates/common/.harness-version +66 -32
- package/templates/common/.opencode/commands/wf-command-create.md +61 -0
- package/templates/common/.opencode/commands/wf-help.md +4 -0
- package/templates/common/.opencode/commands/wf-task-archive.md +29 -0
- package/templates/common/.opencode/commands/wf-task-list.md +27 -0
- package/templates/common/.opencode/commands/wf-task-record.md +27 -0
- package/templates/common/CLAUDE.md +8 -6
- package/templates/common/Harness/MEMORY.md +9 -0
- package/templates/common/Harness/README.md +17 -36
- package/templates/common/Harness/ownership.manifest.json +87 -2
- package/templates/common/Harness/scripts/task-state.mjs +395 -5
- package/templates/common/Harness/scripts/validate-harness.mjs +411 -46
- package/templates/common/Harness/scripts/wf-remove.mjs +34 -2
- package/templates/common/Harness/specs/guides/SETUP.md +8 -0
- package/templates/common/Harness/specs/protocols/MEMORY_PROTOCOL.md +15 -0
- package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +9 -3
- package/templates/common/Harness/specs/runtime/command-surface.json +215 -0
- package/templates/common/Harness/specs/runtime/subagents.md +6 -0
- package/templates/common/Harness/specs/workflows/WF-MAX.md +5 -0
- package/templates/common/Harness/specs/workflows/WF-STATE.md +66 -0
- package/templates/common/Harness/tasks/_template/STATE.json +6 -0
|
@@ -71,6 +71,8 @@ const STATUS_ALIASES = new Map([
|
|
|
71
71
|
['need-user-decision', 'needs-user-decision'],
|
|
72
72
|
['close-out', 'closeout'],
|
|
73
73
|
]);
|
|
74
|
+
const OPEN_TASK_STATUSES = new Set(['active', 'blocked', 'in_progress', 'running', 'pending', 'needs-user-decision']);
|
|
75
|
+
const VALUE_FLAGS = new Set(['--keep', '--mode', '--phase', '--status', '--task', '--text', '--title', '--note', '--context']);
|
|
74
76
|
|
|
75
77
|
function hasFlag(name) {
|
|
76
78
|
return args.includes(name);
|
|
@@ -82,6 +84,22 @@ function flagValue(name, fallback = null) {
|
|
|
82
84
|
return args[index + 1];
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
function findTaskIdArg(startIndex = 1) {
|
|
88
|
+
for (let i = startIndex; i < args.length; i++) {
|
|
89
|
+
const a = args[i];
|
|
90
|
+
if (a.startsWith('--')) {
|
|
91
|
+
if (a.includes('=')) continue;
|
|
92
|
+
const next = args[i + 1];
|
|
93
|
+
if (VALUE_FLAGS.has(a) && next && !next.startsWith('--')) {
|
|
94
|
+
i++; // skip the value
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
return a;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
85
103
|
const outputJson = hasFlag('--json');
|
|
86
104
|
|
|
87
105
|
function print(payload) {
|
|
@@ -127,12 +145,15 @@ function usage() {
|
|
|
127
145
|
message: `Usage: node Harness/scripts/task-state.mjs <command> [options]
|
|
128
146
|
|
|
129
147
|
Commands:
|
|
130
|
-
list [--json] List task state.
|
|
131
|
-
validate [--strict] [--json] Validate state consistency.
|
|
148
|
+
list [--json] List task state with dependency/resume info.
|
|
149
|
+
validate [--strict] [--json] Validate state consistency (includes link checks).
|
|
132
150
|
reconcile [--dry-run|--apply] [--json] Normalize STATE.json and root PROGRESS.md.
|
|
133
151
|
set-active <task-id> [--dry-run] Set the single active task.
|
|
134
152
|
transition <task-id> --status <s> --phase <p> [--dry-run]
|
|
135
153
|
archive [--dry-run|--apply] [--keep n] [--task id] [--json]
|
|
154
|
+
record <task-id> [--create] [--text "description"] [--status <s>] [--mode <m>] [--dry-run|--apply] [--json]
|
|
155
|
+
Create or update a task record.
|
|
156
|
+
open [--json] List open (non-archived, active-status) tasks.
|
|
136
157
|
|
|
137
158
|
Archive defaults to dry-run and keeps ${OUTER_TASK_CAP} non-archived task capsules.`,
|
|
138
159
|
}, 0);
|
|
@@ -226,6 +247,23 @@ function defaultQueues() {
|
|
|
226
247
|
};
|
|
227
248
|
}
|
|
228
249
|
|
|
250
|
+
const VALID_MODES = new Set([
|
|
251
|
+
'direct',
|
|
252
|
+
'wf',
|
|
253
|
+
'wf-max',
|
|
254
|
+
'wf-auto',
|
|
255
|
+
'wf-auto-spark',
|
|
256
|
+
'wf-review',
|
|
257
|
+
'wf-browser',
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
function normalizeMode(value) {
|
|
261
|
+
if (value === null || value === undefined) return '';
|
|
262
|
+
const raw = String(value).trim().toLowerCase();
|
|
263
|
+
if (!raw) return '';
|
|
264
|
+
if (VALID_MODES.has(raw)) return raw;
|
|
265
|
+
return '';
|
|
266
|
+
}
|
|
229
267
|
function normalizeQueues(state) {
|
|
230
268
|
const source = state && typeof state.queues === 'object' && state.queues ? state.queues : state || {};
|
|
231
269
|
return {
|
|
@@ -398,6 +436,48 @@ function validateState({ strict = false } = {}) {
|
|
|
398
436
|
if (task.id === rootProgress.activeTask && normalizeStatus(task.state.status) && normalizeStatus(task.state.status) !== 'active') {
|
|
399
437
|
issue(`${task.id}: root Active Task points here but STATE.json status is "${normalizeStatus(task.state.status)}"`, true);
|
|
400
438
|
}
|
|
439
|
+
|
|
440
|
+
const links = task.state.links || {};
|
|
441
|
+
if (Array.isArray(links.dependsOn)) {
|
|
442
|
+
for (const depId of links.dependsOn) {
|
|
443
|
+
if (!taskIds.has(depId)) issue(`${task.id}: links.dependsOn references non-existent task "${depId}"`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (Array.isArray(links.blocks)) {
|
|
447
|
+
for (const blockId of links.blocks) {
|
|
448
|
+
if (!taskIds.has(blockId)) issue(`${task.id}: links.blocks references non-existent task "${blockId}"`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (Array.isArray(task.state.workItems)) {
|
|
452
|
+
const runningItems = task.state.workItems.filter(wi => wi && normalizeStatus(wi.status) === 'running');
|
|
453
|
+
if (runningItems.length > 0 && (!Array.isArray(task.state.dispatchLedger) || task.state.dispatchLedger.length === 0)) {
|
|
454
|
+
issue(`${task.id}: workItems has ${runningItems.length} running item(s) but no dispatchLedger entries`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const queues = normalizeQueues(task.state);
|
|
459
|
+
const queueMembership = new Map();
|
|
460
|
+
for (const queueName of ['ready', 'running', 'blocked', 'done']) {
|
|
461
|
+
for (const item of queues[queueName]) {
|
|
462
|
+
const itemId = typeof item === 'string' ? item : (item && typeof item.id === 'string' ? item.id : null);
|
|
463
|
+
if (!itemId) {
|
|
464
|
+
issue(`${task.id}: queues.${queueName} contains an item without an id`, true);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const priorQueue = queueMembership.get(itemId);
|
|
468
|
+
if (priorQueue) {
|
|
469
|
+
issue(`${task.id}: queue item "${itemId}" appears in both ${priorQueue} and ${queueName}`, true);
|
|
470
|
+
} else {
|
|
471
|
+
queueMembership.set(itemId, queueName);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const status = normalizeStatus(task.state.status);
|
|
476
|
+
const phase = normalizePhase(task.state.phase);
|
|
477
|
+
if ((SAFE_ARCHIVE_STATUSES.has(status) || SAFE_ARCHIVE_STATUSES.has(phase)) &&
|
|
478
|
+
(queues.ready.length > 0 || queues.running.length > 0 || queues.blocked.length > 0)) {
|
|
479
|
+
issue(`${task.id}: closed task has non-empty ready/running/blocked queues`, true);
|
|
480
|
+
}
|
|
401
481
|
}
|
|
402
482
|
|
|
403
483
|
const activeStateTasks = tasks.filter(task => normalizeStatus(task.state?.status) === 'active');
|
|
@@ -405,7 +485,7 @@ function validateState({ strict = false } = {}) {
|
|
|
405
485
|
issue(`Multiple STATE.json files are active: ${activeStateTasks.map(task => task.id).join(', ')}`, true);
|
|
406
486
|
}
|
|
407
487
|
if (tasks.length > OUTER_TASK_CAP) {
|
|
408
|
-
issue(`Harness/tasks/ has ${tasks.length} outer task capsules (cap ${OUTER_TASK_CAP}); run
|
|
488
|
+
issue(`Harness/tasks/ has ${tasks.length} outer task capsules (cap ${OUTER_TASK_CAP}); remind the user to run $wf-task-archive when they want to archive completed tasks`);
|
|
409
489
|
}
|
|
410
490
|
|
|
411
491
|
return {
|
|
@@ -688,8 +768,37 @@ function applyOperations(operations) {
|
|
|
688
768
|
}
|
|
689
769
|
|
|
690
770
|
function runList() {
|
|
691
|
-
const
|
|
692
|
-
|
|
771
|
+
const { rootProgress, tasks } = collectTasks();
|
|
772
|
+
|
|
773
|
+
const expandedTasks = tasks.map(task => {
|
|
774
|
+
const state = task.state || {};
|
|
775
|
+
const links = state.links || {};
|
|
776
|
+
const status = normalizeStatus(state.status) || task.status;
|
|
777
|
+
return {
|
|
778
|
+
id: task.id,
|
|
779
|
+
status,
|
|
780
|
+
phase: normalizePhase(state.phase) || task.phase,
|
|
781
|
+
rootPhase: task.rootPhase,
|
|
782
|
+
progressPhase: task.progressPhase,
|
|
783
|
+
dependsOn: Array.isArray(links.dependsOn) ? links.dependsOn : [],
|
|
784
|
+
blocks: Array.isArray(links.blocks) ? links.blocks : [],
|
|
785
|
+
statusDisplay: status || '-',
|
|
786
|
+
openTasks: OPEN_TASK_STATUSES.has(status),
|
|
787
|
+
nextAction: state.nextAction || null,
|
|
788
|
+
archive: (state ? archiveEligibility(task, rootProgress.activeTask) : { ok: false, reason: 'no state' }),
|
|
789
|
+
};
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
const payload = {
|
|
793
|
+
ok: true,
|
|
794
|
+
command: 'list',
|
|
795
|
+
taskCount: expandedTasks.length,
|
|
796
|
+
activeTask: rootProgress.activeTask,
|
|
797
|
+
tasks: expandedTasks,
|
|
798
|
+
errors: [],
|
|
799
|
+
warnings: [],
|
|
800
|
+
};
|
|
801
|
+
finish(payload, 0);
|
|
693
802
|
}
|
|
694
803
|
|
|
695
804
|
function runValidate() {
|
|
@@ -873,6 +982,285 @@ function runArchive() {
|
|
|
873
982
|
finish(plan, 0);
|
|
874
983
|
}
|
|
875
984
|
|
|
985
|
+
function readTemplateState() {
|
|
986
|
+
const templatePath = path.join(tasksDir, '_template', 'STATE.json');
|
|
987
|
+
if (!fs.existsSync(templatePath)) return null;
|
|
988
|
+
try {
|
|
989
|
+
return JSON.parse(fs.readFileSync(templatePath, 'utf8'));
|
|
990
|
+
} catch {
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function generateTaskId(title, note, context) {
|
|
996
|
+
// lowercase first, then sanitize — ensures uppercase letters survive as lowercase
|
|
997
|
+
const raw = (title || note || context || 'record')
|
|
998
|
+
.toLowerCase()
|
|
999
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
1000
|
+
.replace(/^-+|-+$/g, '');
|
|
1001
|
+
const parts = raw.split('-').filter(Boolean).slice(0, 3).join('-');
|
|
1002
|
+
// fallback: empty slug (pure CJK/emoji) defaults to 'task'
|
|
1003
|
+
const slug = parts.slice(0, 25) || 'task';
|
|
1004
|
+
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')}`;
|
|
1006
|
+
return `task-${slug}-${suffix}`;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function findMatchingTask(title, note, context) {
|
|
1010
|
+
const { tasks } = collectTasks();
|
|
1011
|
+
const openTasks = tasks.filter(t => OPEN_TASK_STATUSES.has(normalizeStatus(t.state?.status) || t.status));
|
|
1012
|
+
|
|
1013
|
+
// Step 1: title match (highest priority)
|
|
1014
|
+
if (title) {
|
|
1015
|
+
const slugKey = title.replace(/\s+/g, '-').toLowerCase();
|
|
1016
|
+
const matches = openTasks.filter(t => {
|
|
1017
|
+
if (t.id === `task-${slugKey}`) return true;
|
|
1018
|
+
if (t.id.includes(slugKey)) return true;
|
|
1019
|
+
const goal = (t.rootRow?.goal || t.state?.nextAction || '').toLowerCase();
|
|
1020
|
+
return slugKey.split('-').every(part => goal.includes(part));
|
|
1021
|
+
});
|
|
1022
|
+
if (matches.length === 1) return { matched: matches[0], candidates: null };
|
|
1023
|
+
if (matches.length > 1) return { matched: null, candidates: matches };
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// Step 2: note/context overlap match
|
|
1027
|
+
if (note || context) {
|
|
1028
|
+
const query = ((note || '') + ' ' + (context || '')).toLowerCase();
|
|
1029
|
+
const terms = query.split(/\s+/).filter(t => t.length > 3);
|
|
1030
|
+
if (terms.length === 0) return { matched: null, candidates: null };
|
|
1031
|
+
const scored = openTasks.map(t => {
|
|
1032
|
+
const fields = [t.state?.nextAction || '', t.state?.goal || '', t.rootRow?.goal || ''].join(' ').toLowerCase();
|
|
1033
|
+
const score = terms.filter(term => fields.includes(term)).length;
|
|
1034
|
+
return { task: t, score };
|
|
1035
|
+
}).filter(s => s.score > 0);
|
|
1036
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1037
|
+
if (scored.length === 0) return { matched: null, candidates: null };
|
|
1038
|
+
const bestScore = scored[0].score;
|
|
1039
|
+
const best = scored.filter(s => s.score === bestScore);
|
|
1040
|
+
if (best.length === 1 && bestScore >= Math.max(2, Math.ceil(terms.length * 0.5))) {
|
|
1041
|
+
return { matched: best[0].task, candidates: null };
|
|
1042
|
+
}
|
|
1043
|
+
if (best.length > 1) {
|
|
1044
|
+
return { matched: null, candidates: best.map(s => s.task) };
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return { matched: null, candidates: null };
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function runRecord() {
|
|
1052
|
+
const title = flagValue('--title');
|
|
1053
|
+
const note = flagValue('--note');
|
|
1054
|
+
const context = flagValue('--context');
|
|
1055
|
+
const forceNew = hasFlag('--new');
|
|
1056
|
+
|
|
1057
|
+
let taskId = findTaskIdArg(1);
|
|
1058
|
+
let createOrResume = false;
|
|
1059
|
+
|
|
1060
|
+
if (!taskId) {
|
|
1061
|
+
if (!title && !note && !context) {
|
|
1062
|
+
finish({ ok: false, command: 'record', errors: ['requires <task-id> or --title/--note/--context'], warnings: [] }, 1);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
if (!forceNew) {
|
|
1066
|
+
const { matched, candidates } = findMatchingTask(title, note, context);
|
|
1067
|
+
if (matched) {
|
|
1068
|
+
taskId = matched.id;
|
|
1069
|
+
} else if (candidates && candidates.length > 0) {
|
|
1070
|
+
finish({ ok: false, command: 'record', errors: [`Ambiguous match: ${candidates.map(t => t.id).join(', ')}. Use --new to force new, or specify --title more precisely.`], warnings: [] }, 1);
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
if (!taskId) {
|
|
1075
|
+
taskId = generateTaskId(title, note, context);
|
|
1076
|
+
createOrResume = true;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// --new uniqueness: probe existing task dirs, append incrementing suffix on collision
|
|
1080
|
+
if (forceNew && createOrResume) {
|
|
1081
|
+
const existingNames = new Set(listOuterTaskNames());
|
|
1082
|
+
let counter = 1;
|
|
1083
|
+
const baseId = taskId;
|
|
1084
|
+
while (existingNames.has(taskId)) {
|
|
1085
|
+
counter++;
|
|
1086
|
+
taskId = `${baseId}-${counter}`;
|
|
1087
|
+
}
|
|
1088
|
+
// if suffix made it invalid, fall back to base id
|
|
1089
|
+
try {
|
|
1090
|
+
ensureValidTaskId(taskId);
|
|
1091
|
+
} catch {
|
|
1092
|
+
taskId = baseId;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if (!taskId) {
|
|
1098
|
+
finish({ ok: false, command: 'record', errors: ['record requires a <task-id>'], warnings: [] }, 1);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
try {
|
|
1102
|
+
ensureValidTaskId(taskId);
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
finish({ ok: false, command: 'record', errors: [err.message], warnings: [] }, 1);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const isCreate = hasFlag('--create') || createOrResume;
|
|
1109
|
+
const isDryRun = hasFlag('--dry-run');
|
|
1110
|
+
const isApply = hasFlag('--apply');
|
|
1111
|
+
const actuallyApply = isApply && !isDryRun;
|
|
1112
|
+
|
|
1113
|
+
const existing = readState(taskId);
|
|
1114
|
+
if (!existing.state && !isCreate) {
|
|
1115
|
+
finish({ ok: false, command: 'record', errors: [`Task "${taskId}" not found; use --create to create`], warnings: [] }, 1);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const text = flagValue('--text');
|
|
1120
|
+
const statusRaw = flagValue('--status');
|
|
1121
|
+
const modeRaw = flagValue('--mode');
|
|
1122
|
+
|
|
1123
|
+
if (!existing.state && isCreate) {
|
|
1124
|
+
const now = new Date().toISOString();
|
|
1125
|
+
if (statusRaw) {
|
|
1126
|
+
const ns = normalizeStatus(statusRaw);
|
|
1127
|
+
if (!ns) {
|
|
1128
|
+
finish({ ok: false, command: 'record', errors: [`Invalid status "${statusRaw}". Valid: ${[...VALID_STATUSES].join(', ')}`], warnings: [] }, 1);
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
const status = statusRaw ? normalizeStatus(statusRaw) : 'pending';
|
|
1133
|
+
const mode = modeRaw || 'direct';
|
|
1134
|
+
const newState = defaultState(taskId, status, 'intake', now);
|
|
1135
|
+
newState.mode = mode;
|
|
1136
|
+
if (text) newState.nextAction = text;
|
|
1137
|
+
if (modeRaw) {
|
|
1138
|
+
const normalizedMode = normalizeMode(modeRaw);
|
|
1139
|
+
if (!normalizedMode) {
|
|
1140
|
+
finish({ ok: false, command: 'record', errors: [`Invalid mode "${modeRaw}". Valid: ${[...VALID_MODES].join(', ')}`], warnings: [] }, 1);
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
newState.mode = normalizedMode;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
const templateState = readTemplateState();
|
|
1147
|
+
if (templateState) {
|
|
1148
|
+
if (Array.isArray(templateState.acceptance)) newState.acceptance = [...templateState.acceptance];
|
|
1149
|
+
if (templateState.links) newState.links = JSON.parse(JSON.stringify(templateState.links));
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
if (actuallyApply) {
|
|
1153
|
+
const taskDir = safeTaskPath(taskId);
|
|
1154
|
+
fs.mkdirSync(taskDir, { recursive: true });
|
|
1155
|
+
writeJsonAtomic(path.join(taskDir, 'STATE.json'), newState);
|
|
1156
|
+
const progressFile = path.join(taskDir, 'PROGRESS.md');
|
|
1157
|
+
writeTextAtomic(progressFile, renderTaskProgress(readText(progressFile), taskId, newState));
|
|
1158
|
+
|
|
1159
|
+
const planFile = path.join(taskDir, 'PLAN.md');
|
|
1160
|
+
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`);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const rootText = readText(progressPath);
|
|
1165
|
+
const parsed = parseRootProgress();
|
|
1166
|
+
const newRow = { id: taskId, goal: text || taskTitle(taskId), phase: displayPhase('intake'), closed: '-' };
|
|
1167
|
+
parsed.rows.push(newRow);
|
|
1168
|
+
const rows = parsed.rows.map(r => ({ id: r.id, goal: r.goal, phase: r.phase, closed: r.closed }));
|
|
1169
|
+
const newRoot = renderRootProgress(rootText, parsed.activeTask, rows);
|
|
1170
|
+
writeTextAtomic(progressPath, newRoot);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
finish({
|
|
1174
|
+
ok: true,
|
|
1175
|
+
command: 'record',
|
|
1176
|
+
action: 'created',
|
|
1177
|
+
dryRun: !actuallyApply,
|
|
1178
|
+
taskId,
|
|
1179
|
+
state: newState,
|
|
1180
|
+
warnings: [],
|
|
1181
|
+
}, 0);
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
if (existing.state) {
|
|
1186
|
+
const now = new Date().toISOString();
|
|
1187
|
+
const updated = { ...existing.state };
|
|
1188
|
+
updated.updatedAt = now;
|
|
1189
|
+
if (statusRaw) {
|
|
1190
|
+
const ns = normalizeStatus(statusRaw);
|
|
1191
|
+
if (!ns) {
|
|
1192
|
+
finish({ ok: false, command: 'record', errors: [`Invalid status "${statusRaw}". Valid: ${[...VALID_STATUSES].join(', ')}`], warnings: [] }, 1);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
updated.status = ns;
|
|
1196
|
+
}
|
|
1197
|
+
if (modeRaw) {
|
|
1198
|
+
const normalizedMode = normalizeMode(modeRaw);
|
|
1199
|
+
if (!normalizedMode) {
|
|
1200
|
+
finish({ ok: false, command: 'record', errors: [`Invalid mode "${modeRaw}". Valid: ${[...VALID_MODES].join(', ')}`], warnings: [] }, 1);
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
updated.mode = normalizedMode;
|
|
1204
|
+
}
|
|
1205
|
+
if (text) updated.nextAction = text;
|
|
1206
|
+
|
|
1207
|
+
if (actuallyApply) {
|
|
1208
|
+
writeJsonAtomic(existing.path, updated);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
finish({
|
|
1212
|
+
ok: true,
|
|
1213
|
+
command: 'record',
|
|
1214
|
+
action: 'updated',
|
|
1215
|
+
dryRun: !actuallyApply,
|
|
1216
|
+
taskId,
|
|
1217
|
+
state: updated,
|
|
1218
|
+
warnings: [],
|
|
1219
|
+
}, 0);
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function runOpen() {
|
|
1225
|
+
const { rootProgress, tasks } = collectTasks();
|
|
1226
|
+
|
|
1227
|
+
const openTasks = tasks.filter(task => {
|
|
1228
|
+
const status = normalizeStatus(task.state?.status) || task.status;
|
|
1229
|
+
return OPEN_TASK_STATUSES.has(status);
|
|
1230
|
+
}).map(task => {
|
|
1231
|
+
const state = task.state || {};
|
|
1232
|
+
const links = state.links || {};
|
|
1233
|
+
const status = normalizeStatus(state.status) || task.status;
|
|
1234
|
+
const dependsOn = Array.isArray(links.dependsOn) ? links.dependsOn : [];
|
|
1235
|
+
const openDepTasks = dependsOn.filter(depId => {
|
|
1236
|
+
const depTask = tasks.find(t => t.id === depId);
|
|
1237
|
+
if (!depTask) return false;
|
|
1238
|
+
const depStatus = normalizeStatus(depTask.state?.status) || depTask.status;
|
|
1239
|
+
return OPEN_TASK_STATUSES.has(depStatus);
|
|
1240
|
+
});
|
|
1241
|
+
return {
|
|
1242
|
+
id: task.id,
|
|
1243
|
+
status,
|
|
1244
|
+
phase: normalizePhase(state.phase) || task.phase,
|
|
1245
|
+
dependsOn,
|
|
1246
|
+
blocks: Array.isArray(links.blocks) ? links.blocks : [],
|
|
1247
|
+
blockedByOpenDeps: openDepTasks,
|
|
1248
|
+
nextAction: state.nextAction || null,
|
|
1249
|
+
statusDisplay: status || '-',
|
|
1250
|
+
openTasks: true,
|
|
1251
|
+
};
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
finish({
|
|
1255
|
+
ok: true,
|
|
1256
|
+
command: 'open',
|
|
1257
|
+
taskCount: openTasks.length,
|
|
1258
|
+
tasks: openTasks,
|
|
1259
|
+
errors: [],
|
|
1260
|
+
warnings: [],
|
|
1261
|
+
}, 0);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
876
1264
|
if (command === 'help' || hasFlag('--help') || hasFlag('-h')) usage();
|
|
877
1265
|
if (command === 'list') runList();
|
|
878
1266
|
if (command === 'validate') runValidate();
|
|
@@ -880,6 +1268,8 @@ if (command === 'reconcile') runReconcile();
|
|
|
880
1268
|
if (command === 'set-active') runSetActive();
|
|
881
1269
|
if (command === 'transition') runTransition();
|
|
882
1270
|
if (command === 'archive') runArchive();
|
|
1271
|
+
if (command === 'record') runRecord();
|
|
1272
|
+
if (command === 'open') runOpen();
|
|
883
1273
|
|
|
884
1274
|
finish({
|
|
885
1275
|
ok: false,
|