@task0/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/main.js +4385 -0
- package/package.json +57 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,4385 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// ../../packages/shared/dist/node/yaml.js
|
|
13
|
+
import fs2 from "fs";
|
|
14
|
+
import path2 from "path";
|
|
15
|
+
import yaml2 from "js-yaml";
|
|
16
|
+
function readYaml(filePath) {
|
|
17
|
+
if (!fs2.existsSync(filePath))
|
|
18
|
+
return null;
|
|
19
|
+
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
20
|
+
return yaml2.load(raw);
|
|
21
|
+
}
|
|
22
|
+
function readContext(taskDir) {
|
|
23
|
+
const files = fs2.readdirSync(taskDir).filter((f) => f.endsWith(".md"));
|
|
24
|
+
if (files.length === 0)
|
|
25
|
+
return void 0;
|
|
26
|
+
const content = fs2.readFileSync(path2.join(taskDir, files[0]), "utf-8");
|
|
27
|
+
const stripped = content.replace(/^---[\s\S]*?---\s*/, "");
|
|
28
|
+
return stripped.trim() || void 0;
|
|
29
|
+
}
|
|
30
|
+
var init_yaml = __esm({
|
|
31
|
+
"../../packages/shared/dist/node/yaml.js"() {
|
|
32
|
+
"use strict";
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// ../../packages/shared/dist/object-id.js
|
|
37
|
+
function encodeBase62(n, len) {
|
|
38
|
+
let s = "";
|
|
39
|
+
let v = n;
|
|
40
|
+
for (let i = 0; i < len; i++) {
|
|
41
|
+
s = BASE62[Number(v % BASE)] + s;
|
|
42
|
+
v /= BASE;
|
|
43
|
+
}
|
|
44
|
+
return s;
|
|
45
|
+
}
|
|
46
|
+
function formatObjectId(type, value) {
|
|
47
|
+
return `${RESOURCE_PREFIXES[type]}_${value}`;
|
|
48
|
+
}
|
|
49
|
+
function generateObjectId(type) {
|
|
50
|
+
const bytes = new Uint8Array(4);
|
|
51
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
52
|
+
let hex = "";
|
|
53
|
+
for (const b of bytes)
|
|
54
|
+
hex += b.toString(16).padStart(2, "0");
|
|
55
|
+
const value = encodeBase62(BigInt("0x" + hex) % MOD, ID_LEN);
|
|
56
|
+
return formatObjectId(type, value);
|
|
57
|
+
}
|
|
58
|
+
function isTaskObjectId(id) {
|
|
59
|
+
return TASK_ID_RE.test(id);
|
|
60
|
+
}
|
|
61
|
+
var RESOURCE_PREFIXES, PREFIX_TO_TYPE, BASE62, ID_LEN, BASE, MOD, TASK_ID_RE, PREFIXES_PATTERN, OBJECT_ID_RE;
|
|
62
|
+
var init_object_id = __esm({
|
|
63
|
+
"../../packages/shared/dist/object-id.js"() {
|
|
64
|
+
"use strict";
|
|
65
|
+
RESOURCE_PREFIXES = {
|
|
66
|
+
task: "tsk",
|
|
67
|
+
project: "prj",
|
|
68
|
+
runtime: "rt",
|
|
69
|
+
issue: "iss",
|
|
70
|
+
inbox: "ibx",
|
|
71
|
+
inbox_note: "note",
|
|
72
|
+
deck: "deck",
|
|
73
|
+
kanban: "kb",
|
|
74
|
+
column: "col",
|
|
75
|
+
task_source: "src",
|
|
76
|
+
event: "evt",
|
|
77
|
+
okr_plan: "okr",
|
|
78
|
+
objective: "obj",
|
|
79
|
+
key_result: "kr",
|
|
80
|
+
milestone: "ms",
|
|
81
|
+
task_kr_link: "krl",
|
|
82
|
+
task_comment: "cmt",
|
|
83
|
+
agent: "agt",
|
|
84
|
+
daemon: "dmn"
|
|
85
|
+
};
|
|
86
|
+
PREFIX_TO_TYPE = Object.fromEntries(Object.entries(RESOURCE_PREFIXES).map(([k, v]) => [v, k]));
|
|
87
|
+
BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
88
|
+
ID_LEN = 5;
|
|
89
|
+
BASE = 62n;
|
|
90
|
+
MOD = BASE ** BigInt(ID_LEN);
|
|
91
|
+
TASK_ID_RE = /^tsk_[A-Za-z0-9]{5,12}$/;
|
|
92
|
+
PREFIXES_PATTERN = Object.values(RESOURCE_PREFIXES).join("|");
|
|
93
|
+
OBJECT_ID_RE = new RegExp(`\\b(?:${PREFIXES_PATTERN})_[A-Za-z0-9]{5,12}\\b`, "g");
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ../../packages/shared/dist/types/task.js
|
|
98
|
+
function isActiveTaskStatus(status) {
|
|
99
|
+
return ACTIVE_TASK_STATUS_SET.has(status);
|
|
100
|
+
}
|
|
101
|
+
var TASK_STATUS_VALUES, ACTIVE_TASK_STATUSES, ATTENTION_TASK_STATUSES, ACTIVE_TASK_STATUS_SET, ATTENTION_TASK_STATUS_SET;
|
|
102
|
+
var init_task = __esm({
|
|
103
|
+
"../../packages/shared/dist/types/task.js"() {
|
|
104
|
+
"use strict";
|
|
105
|
+
TASK_STATUS_VALUES = [
|
|
106
|
+
"todo",
|
|
107
|
+
"triaging",
|
|
108
|
+
"triaged",
|
|
109
|
+
"planning",
|
|
110
|
+
"planned",
|
|
111
|
+
"refining",
|
|
112
|
+
"refined",
|
|
113
|
+
"executing",
|
|
114
|
+
"blocked",
|
|
115
|
+
"done",
|
|
116
|
+
"archived"
|
|
117
|
+
];
|
|
118
|
+
ACTIVE_TASK_STATUSES = [
|
|
119
|
+
"triaging",
|
|
120
|
+
"planning",
|
|
121
|
+
"refining",
|
|
122
|
+
"executing"
|
|
123
|
+
];
|
|
124
|
+
ATTENTION_TASK_STATUSES = [
|
|
125
|
+
"blocked"
|
|
126
|
+
];
|
|
127
|
+
ACTIVE_TASK_STATUS_SET = new Set(ACTIVE_TASK_STATUSES);
|
|
128
|
+
ATTENTION_TASK_STATUS_SET = new Set(ATTENTION_TASK_STATUSES);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ../../packages/shared/dist/node/scanner.js
|
|
133
|
+
import fs3 from "fs";
|
|
134
|
+
import path3 from "path";
|
|
135
|
+
function scanProject(projectPath, sourceName) {
|
|
136
|
+
const absPath = path3.resolve(projectPath);
|
|
137
|
+
const resolvedSourceName = sourceName ?? path3.basename(absPath);
|
|
138
|
+
const errors = [];
|
|
139
|
+
const tasks = [];
|
|
140
|
+
const repairs = [];
|
|
141
|
+
const projectYml = path3.join(absPath, "task0.yml");
|
|
142
|
+
const projectConfig = readYaml(projectYml);
|
|
143
|
+
if (!projectConfig)
|
|
144
|
+
return { tasks, errors: [`${projectYml} not found`], repairs };
|
|
145
|
+
if (projectConfig.kind !== "project")
|
|
146
|
+
return { tasks, errors: [`Invalid: kind="${projectConfig.kind}"`], repairs };
|
|
147
|
+
const tasksDir = path3.join(absPath, projectConfig.tasks_dir);
|
|
148
|
+
if (!fs3.existsSync(tasksDir))
|
|
149
|
+
return { tasks, errors: [`tasks_dir not found: ${tasksDir}`], repairs };
|
|
150
|
+
const entries = fs3.readdirSync(tasksDir, { withFileTypes: true });
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
if (!entry.isDirectory())
|
|
153
|
+
continue;
|
|
154
|
+
const taskDir = path3.join(tasksDir, entry.name);
|
|
155
|
+
const taskYml = path3.join(taskDir, "task0.yml");
|
|
156
|
+
const raw = readYaml(taskYml);
|
|
157
|
+
if (!raw || raw.kind !== "task")
|
|
158
|
+
continue;
|
|
159
|
+
let objectId = raw.object_id;
|
|
160
|
+
if (!objectId) {
|
|
161
|
+
objectId = generateObjectId("task");
|
|
162
|
+
repairs.push({
|
|
163
|
+
taskDir,
|
|
164
|
+
taskYml,
|
|
165
|
+
dirName: entry.name,
|
|
166
|
+
reason: "missing_object_id"
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (typeof raw.id === "string" && raw.id !== entry.name) {
|
|
170
|
+
repairs.push({
|
|
171
|
+
taskDir,
|
|
172
|
+
taskYml,
|
|
173
|
+
dirName: entry.name,
|
|
174
|
+
reason: "id_mismatch",
|
|
175
|
+
currentId: raw.id,
|
|
176
|
+
expectedId: entry.name
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
const id = raw.id || entry.name;
|
|
180
|
+
const title = raw.title;
|
|
181
|
+
const status = raw.status;
|
|
182
|
+
if (!id || !title || !status) {
|
|
183
|
+
errors.push(`${entry.name}: missing required fields (id, title, status)`);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (!VALID_STATUSES.has(status)) {
|
|
187
|
+
errors.push(`${entry.name}: invalid status "${status}"`);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const context = readContext(taskDir);
|
|
191
|
+
const stat = fs3.statSync(taskYml);
|
|
192
|
+
const tags = raw.tags || [];
|
|
193
|
+
const summary = raw.summary || void 0;
|
|
194
|
+
const displayTitle = summary?.title || title;
|
|
195
|
+
const displayTags = summary?.tags ?? tags;
|
|
196
|
+
tasks.push({
|
|
197
|
+
id,
|
|
198
|
+
object_id: objectId,
|
|
199
|
+
metadata: raw.metadata || void 0,
|
|
200
|
+
kind: "task",
|
|
201
|
+
title,
|
|
202
|
+
description: raw.description || void 0,
|
|
203
|
+
project: resolvedSourceName,
|
|
204
|
+
status,
|
|
205
|
+
current_step: raw.current_step || "",
|
|
206
|
+
tags,
|
|
207
|
+
created_at: raw.created_at || stat.birthtime.toISOString(),
|
|
208
|
+
context: context || "",
|
|
209
|
+
project_root: absPath,
|
|
210
|
+
task_dir: taskDir,
|
|
211
|
+
last_update: stat.mtime.toISOString(),
|
|
212
|
+
linked_issues: raw.linked_issues || [],
|
|
213
|
+
workflow: raw.workflow || void 0,
|
|
214
|
+
summary,
|
|
215
|
+
comments: raw.comments || void 0,
|
|
216
|
+
display_title: displayTitle,
|
|
217
|
+
display_tags: displayTags
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return { tasks, errors, repairs };
|
|
221
|
+
}
|
|
222
|
+
var VALID_STATUSES;
|
|
223
|
+
var init_scanner = __esm({
|
|
224
|
+
"../../packages/shared/dist/node/scanner.js"() {
|
|
225
|
+
"use strict";
|
|
226
|
+
init_object_id();
|
|
227
|
+
init_task();
|
|
228
|
+
init_yaml();
|
|
229
|
+
VALID_STATUSES = new Set(TASK_STATUS_VALUES);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ../../packages/shared/dist/node/open-questions.js
|
|
234
|
+
import fs4 from "fs";
|
|
235
|
+
import path4 from "path";
|
|
236
|
+
function readOpenQuestions(taskDir, issueFiles) {
|
|
237
|
+
const result = [];
|
|
238
|
+
for (const file of issueFiles) {
|
|
239
|
+
const m = file.match(/^ISSUE-(\d+)\.md$/);
|
|
240
|
+
if (!m)
|
|
241
|
+
continue;
|
|
242
|
+
const fullPath = path4.join(taskDir, file);
|
|
243
|
+
if (!fs4.existsSync(fullPath))
|
|
244
|
+
continue;
|
|
245
|
+
const md = fs4.readFileSync(fullPath, "utf-8");
|
|
246
|
+
const match = md.match(OPEN_QUESTIONS_SECTION_RE);
|
|
247
|
+
if (!match)
|
|
248
|
+
continue;
|
|
249
|
+
const body = match[1].trim();
|
|
250
|
+
if (!body || /^none\b/i.test(body))
|
|
251
|
+
continue;
|
|
252
|
+
const bullets = body.split("\n").filter((line) => /^\s*[-*]\s+\S/.test(line));
|
|
253
|
+
if (bullets.length === 0)
|
|
254
|
+
continue;
|
|
255
|
+
result.push({ file, index: m[1], bullets });
|
|
256
|
+
}
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
var OPEN_QUESTIONS_SECTION_RE;
|
|
260
|
+
var init_open_questions = __esm({
|
|
261
|
+
"../../packages/shared/dist/node/open-questions.js"() {
|
|
262
|
+
"use strict";
|
|
263
|
+
OPEN_QUESTIONS_SECTION_RE = /## Open Questions\s*\n([\s\S]*?)(\n## |\n*$)/i;
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ../../packages/shared/dist/node/task-state.js
|
|
268
|
+
import fs5 from "fs";
|
|
269
|
+
import os from "os";
|
|
270
|
+
import path5 from "path";
|
|
271
|
+
import yaml3 from "js-yaml";
|
|
272
|
+
function findProjectRoot(start = process.cwd()) {
|
|
273
|
+
let dir = path5.resolve(start);
|
|
274
|
+
while (true) {
|
|
275
|
+
const ymlPath = path5.join(dir, "task0.yml");
|
|
276
|
+
if (fs5.existsSync(ymlPath)) {
|
|
277
|
+
try {
|
|
278
|
+
const raw = yaml3.load(fs5.readFileSync(ymlPath, "utf-8"));
|
|
279
|
+
if (raw?.kind === "project")
|
|
280
|
+
return dir;
|
|
281
|
+
} catch {
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const parent = path5.dirname(dir);
|
|
285
|
+
if (parent === dir)
|
|
286
|
+
return null;
|
|
287
|
+
dir = parent;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function readProjectConfig(projectRoot) {
|
|
291
|
+
return yaml3.load(fs5.readFileSync(path5.join(projectRoot, "task0.yml"), "utf-8"));
|
|
292
|
+
}
|
|
293
|
+
function resolveTasksDir(projectRoot, projectConfig) {
|
|
294
|
+
const cfg = projectConfig ?? readProjectConfig(projectRoot);
|
|
295
|
+
return path5.isAbsolute(cfg.tasks_dir) ? cfg.tasks_dir : path5.join(projectRoot, cfg.tasks_dir);
|
|
296
|
+
}
|
|
297
|
+
function resolveTaskByObjectId(objectId, projectRoot) {
|
|
298
|
+
const root = projectRoot ?? findProjectRoot();
|
|
299
|
+
if (!root) {
|
|
300
|
+
throw new Error("Not inside a task0 project (no task0.yml with kind: project found).");
|
|
301
|
+
}
|
|
302
|
+
if (!isTaskObjectId(objectId)) {
|
|
303
|
+
throw new Error(`Expected a task object_id like 'tsk_XXXXX', got '${objectId}'. Directory names are no longer accepted; run 'task0 task list' to find the object_id.`);
|
|
304
|
+
}
|
|
305
|
+
const tasksDir = resolveTasksDir(root);
|
|
306
|
+
const entries = fs5.readdirSync(tasksDir, { withFileTypes: true });
|
|
307
|
+
for (const entry of entries) {
|
|
308
|
+
if (!entry.isDirectory())
|
|
309
|
+
continue;
|
|
310
|
+
const taskDir = path5.join(tasksDir, entry.name);
|
|
311
|
+
const taskYml = path5.join(taskDir, "task0.yml");
|
|
312
|
+
if (!fs5.existsSync(taskYml))
|
|
313
|
+
continue;
|
|
314
|
+
try {
|
|
315
|
+
const raw = yaml3.load(fs5.readFileSync(taskYml, "utf-8"));
|
|
316
|
+
if (raw && raw.object_id === objectId) {
|
|
317
|
+
return { projectRoot: root, tasksDir, taskDir, taskYml };
|
|
318
|
+
}
|
|
319
|
+
} catch {
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
throw new Error(`No task with object_id '${objectId}' found under ${tasksDir}. If tasks pre-date object_id, run 'task0 task migrate' to seed them.`);
|
|
323
|
+
}
|
|
324
|
+
function readTaskYaml(taskYml) {
|
|
325
|
+
if (!fs5.existsSync(taskYml))
|
|
326
|
+
throw new Error(`task0.yml not found: ${taskYml}`);
|
|
327
|
+
return yaml3.load(fs5.readFileSync(taskYml, "utf-8")) || {};
|
|
328
|
+
}
|
|
329
|
+
function writeTaskYaml(taskYml, data) {
|
|
330
|
+
fs5.writeFileSync(taskYml, yaml3.dump(data, { lineWidth: 120 }), "utf-8");
|
|
331
|
+
}
|
|
332
|
+
function taskYamlLockPath(taskDir) {
|
|
333
|
+
return path5.join(taskDir, TASK_YAML_LOCKFILE);
|
|
334
|
+
}
|
|
335
|
+
function isProcessAlive(pid) {
|
|
336
|
+
try {
|
|
337
|
+
process.kill(pid, 0);
|
|
338
|
+
return true;
|
|
339
|
+
} catch {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function readTaskYamlLockInfo(file) {
|
|
344
|
+
try {
|
|
345
|
+
return JSON.parse(fs5.readFileSync(file, "utf-8"));
|
|
346
|
+
} catch {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function writeTaskYamlLockInfo(file, info) {
|
|
351
|
+
const fd = fs5.openSync(file, "wx");
|
|
352
|
+
try {
|
|
353
|
+
fs5.writeFileSync(fd, JSON.stringify(info, null, 2), "utf-8");
|
|
354
|
+
} finally {
|
|
355
|
+
fs5.closeSync(fd);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function sleep(ms) {
|
|
359
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
360
|
+
}
|
|
361
|
+
async function acquireTaskYamlLock(taskDir) {
|
|
362
|
+
const file = taskYamlLockPath(taskDir);
|
|
363
|
+
const info = {
|
|
364
|
+
pid: process.pid,
|
|
365
|
+
hostname: os.hostname(),
|
|
366
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
367
|
+
};
|
|
368
|
+
const deadline = Date.now() + TASK_YAML_LOCK_TIMEOUT_MS;
|
|
369
|
+
let backoffMs = TASK_YAML_LOCK_RETRY_BASE_MS;
|
|
370
|
+
while (Date.now() < deadline) {
|
|
371
|
+
try {
|
|
372
|
+
writeTaskYamlLockInfo(file, info);
|
|
373
|
+
return info;
|
|
374
|
+
} catch (err) {
|
|
375
|
+
if (err.code !== "EEXIST")
|
|
376
|
+
throw err;
|
|
377
|
+
}
|
|
378
|
+
const existing = readTaskYamlLockInfo(file);
|
|
379
|
+
if (existing && existing.hostname === info.hostname && !isProcessAlive(existing.pid)) {
|
|
380
|
+
try {
|
|
381
|
+
fs5.unlinkSync(file);
|
|
382
|
+
continue;
|
|
383
|
+
} catch (err) {
|
|
384
|
+
if (err.code !== "ENOENT") {
|
|
385
|
+
throw err;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
await sleep(backoffMs);
|
|
390
|
+
backoffMs = Math.min(backoffMs * 2, TASK_YAML_LOCK_RETRY_MAX_MS);
|
|
391
|
+
}
|
|
392
|
+
throw new Error(`Timed out acquiring task YAML lock for ${taskDir}`);
|
|
393
|
+
}
|
|
394
|
+
function releaseTaskYamlLock(taskDir, info) {
|
|
395
|
+
const file = taskYamlLockPath(taskDir);
|
|
396
|
+
const existing = readTaskYamlLockInfo(file);
|
|
397
|
+
if (!existing)
|
|
398
|
+
return;
|
|
399
|
+
if (existing.pid !== info.pid || existing.hostname !== info.hostname || existing.started_at !== info.started_at) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
fs5.unlinkSync(file);
|
|
404
|
+
} catch (err) {
|
|
405
|
+
if (err.code !== "ENOENT") {
|
|
406
|
+
throw err;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function withTaskYamlLock(taskDir, fn) {
|
|
411
|
+
const key = path5.resolve(taskDir);
|
|
412
|
+
const prev = yamlLocks.get(key) ?? Promise.resolve();
|
|
413
|
+
const next = prev.then(async () => {
|
|
414
|
+
const lock = await acquireTaskYamlLock(taskDir);
|
|
415
|
+
try {
|
|
416
|
+
return await fn();
|
|
417
|
+
} finally {
|
|
418
|
+
releaseTaskYamlLock(taskDir, lock);
|
|
419
|
+
}
|
|
420
|
+
}, async () => {
|
|
421
|
+
const lock = await acquireTaskYamlLock(taskDir);
|
|
422
|
+
try {
|
|
423
|
+
return await fn();
|
|
424
|
+
} finally {
|
|
425
|
+
releaseTaskYamlLock(taskDir, lock);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
const tracked = next.then(() => {
|
|
429
|
+
}, () => {
|
|
430
|
+
}).then(() => {
|
|
431
|
+
if (yamlLocks.get(key) === tracked)
|
|
432
|
+
yamlLocks.delete(key);
|
|
433
|
+
});
|
|
434
|
+
yamlLocks.set(key, tracked);
|
|
435
|
+
return next;
|
|
436
|
+
}
|
|
437
|
+
function readTaskWorkflow(taskYml) {
|
|
438
|
+
const raw = readTaskYaml(taskYml);
|
|
439
|
+
return raw.workflow || {};
|
|
440
|
+
}
|
|
441
|
+
async function updateTaskWorkflow(taskYml, patch) {
|
|
442
|
+
const taskDir = path5.dirname(taskYml);
|
|
443
|
+
return withTaskYamlLock(taskDir, () => {
|
|
444
|
+
const raw = readTaskYaml(taskYml);
|
|
445
|
+
const current = raw.workflow || {};
|
|
446
|
+
const merged = { ...current, ...patch, updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
447
|
+
if (patch.runtimes) {
|
|
448
|
+
merged.runtimes = { ...current.runtimes || {}, ...patch.runtimes };
|
|
449
|
+
}
|
|
450
|
+
if (patch.decisions) {
|
|
451
|
+
const next = { ...current.decisions || {} };
|
|
452
|
+
for (const [key, value] of Object.entries(patch.decisions)) {
|
|
453
|
+
next[key] = { ...next[key] || {}, ...value };
|
|
454
|
+
}
|
|
455
|
+
merged.decisions = next;
|
|
456
|
+
}
|
|
457
|
+
raw.workflow = merged;
|
|
458
|
+
writeTaskYaml(taskYml, raw);
|
|
459
|
+
return merged;
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
var yamlLocks, TASK_YAML_LOCKFILE, TASK_YAML_LOCK_TIMEOUT_MS, TASK_YAML_LOCK_RETRY_BASE_MS, TASK_YAML_LOCK_RETRY_MAX_MS;
|
|
463
|
+
var init_task_state = __esm({
|
|
464
|
+
"../../packages/shared/dist/node/task-state.js"() {
|
|
465
|
+
"use strict";
|
|
466
|
+
init_object_id();
|
|
467
|
+
yamlLocks = /* @__PURE__ */ new Map();
|
|
468
|
+
TASK_YAML_LOCKFILE = ".task0.yml.lock";
|
|
469
|
+
TASK_YAML_LOCK_TIMEOUT_MS = 5e3;
|
|
470
|
+
TASK_YAML_LOCK_RETRY_BASE_MS = 10;
|
|
471
|
+
TASK_YAML_LOCK_RETRY_MAX_MS = 100;
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// ../../packages/shared/dist/types/error-report.js
|
|
476
|
+
var ERROR_REPORT_SCHEMA_VERSION;
|
|
477
|
+
var init_error_report = __esm({
|
|
478
|
+
"../../packages/shared/dist/types/error-report.js"() {
|
|
479
|
+
"use strict";
|
|
480
|
+
ERROR_REPORT_SCHEMA_VERSION = 1;
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// ../../packages/shared/dist/node/redact.js
|
|
485
|
+
function redactString(input) {
|
|
486
|
+
if (!input)
|
|
487
|
+
return { value: input, redacted: false };
|
|
488
|
+
let value = input;
|
|
489
|
+
let redacted = false;
|
|
490
|
+
for (const { re, replace } of REDACTION_PATTERNS) {
|
|
491
|
+
value = value.replace(re, (...args) => {
|
|
492
|
+
redacted = true;
|
|
493
|
+
return replace(...args);
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
return { value, redacted };
|
|
497
|
+
}
|
|
498
|
+
function redactArgv(argv) {
|
|
499
|
+
let redacted = false;
|
|
500
|
+
const out = argv.map((a) => {
|
|
501
|
+
const r = redactString(a);
|
|
502
|
+
if (r.redacted)
|
|
503
|
+
redacted = true;
|
|
504
|
+
return r.value;
|
|
505
|
+
});
|
|
506
|
+
return { argv: out, redacted };
|
|
507
|
+
}
|
|
508
|
+
function filterEnv(env = process.env) {
|
|
509
|
+
const out = {};
|
|
510
|
+
for (const [key, rawValue] of Object.entries(env)) {
|
|
511
|
+
if (rawValue === void 0)
|
|
512
|
+
continue;
|
|
513
|
+
const allowed = DEFAULT_ENV_ALLOWLIST.has(key) || key.startsWith(LC_PREFIX) || key.startsWith(TASK0_PREFIX);
|
|
514
|
+
if (!allowed)
|
|
515
|
+
continue;
|
|
516
|
+
if (SECRET_KEY_RE.test(key) && rawValue.length > 0) {
|
|
517
|
+
out[key] = REDACTED_VALUE;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
const { value } = redactString(rawValue);
|
|
521
|
+
out[key] = value;
|
|
522
|
+
}
|
|
523
|
+
return out;
|
|
524
|
+
}
|
|
525
|
+
var REDACTED, REDACTION_PATTERNS, DEFAULT_ENV_ALLOWLIST, LC_PREFIX, TASK0_PREFIX, SECRET_KEY_RE, REDACTED_VALUE;
|
|
526
|
+
var init_redact = __esm({
|
|
527
|
+
"../../packages/shared/dist/node/redact.js"() {
|
|
528
|
+
"use strict";
|
|
529
|
+
REDACTED = "[REDACTED]";
|
|
530
|
+
REDACTION_PATTERNS = [
|
|
531
|
+
{
|
|
532
|
+
re: /Bearer\s+[A-Za-z0-9._~+/=-]+/gi,
|
|
533
|
+
replace: () => `Bearer ${REDACTED}`
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
re: /Authorization\s*:\s*Bearer\s+[A-Za-z0-9._~+/=-]+/gi,
|
|
537
|
+
replace: () => `Authorization: Bearer ${REDACTED}`
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
re: /\b([A-Z0-9_]*(?:TOKEN|KEY|SECRET|PASSWORD))\s*=\s*([^\s"'&]+)/gi,
|
|
541
|
+
replace: (_m, name) => `${name}=${REDACTED}`
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
re: /\b(token|key|secret|password)\s*=\s*([^\s"'&]+)/gi,
|
|
545
|
+
replace: (_m, name) => `${name}=${REDACTED}`
|
|
546
|
+
}
|
|
547
|
+
];
|
|
548
|
+
DEFAULT_ENV_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
549
|
+
"PATH",
|
|
550
|
+
"SHELL",
|
|
551
|
+
"LANG",
|
|
552
|
+
"TERM",
|
|
553
|
+
"TERM_PROGRAM",
|
|
554
|
+
"TERM_PROGRAM_VERSION",
|
|
555
|
+
"COLORTERM",
|
|
556
|
+
"CI"
|
|
557
|
+
]);
|
|
558
|
+
LC_PREFIX = "LC_";
|
|
559
|
+
TASK0_PREFIX = "TASK0_";
|
|
560
|
+
SECRET_KEY_RE = /(?:TOKEN|KEY|SECRET|PASSWORD)$/;
|
|
561
|
+
REDACTED_VALUE = "[REDACTED]";
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// ../../packages/shared/dist/node/error-reports.js
|
|
566
|
+
import fs6 from "fs";
|
|
567
|
+
import os2 from "os";
|
|
568
|
+
import path6 from "path";
|
|
569
|
+
import { spawnSync } from "child_process";
|
|
570
|
+
import crypto from "crypto";
|
|
571
|
+
function task0Home() {
|
|
572
|
+
const override = process.env.TASK0_HOME;
|
|
573
|
+
if (override && override.length > 0)
|
|
574
|
+
return override;
|
|
575
|
+
return path6.join(os2.homedir(), ".task0");
|
|
576
|
+
}
|
|
577
|
+
function errorsRoot() {
|
|
578
|
+
return path6.join(task0Home(), "errors");
|
|
579
|
+
}
|
|
580
|
+
function createErrorReportId() {
|
|
581
|
+
return `err_${crypto.randomBytes(4).toString("hex")}`;
|
|
582
|
+
}
|
|
583
|
+
function toUtcBasicTs(date) {
|
|
584
|
+
const iso = date.toISOString();
|
|
585
|
+
return iso.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
586
|
+
}
|
|
587
|
+
function errorReportDirName(date, id) {
|
|
588
|
+
return `${toUtcBasicTs(date)}-${id}`;
|
|
589
|
+
}
|
|
590
|
+
function runGit(args, cwd) {
|
|
591
|
+
try {
|
|
592
|
+
const res = spawnSync("git", args, {
|
|
593
|
+
cwd,
|
|
594
|
+
timeout: 1e3,
|
|
595
|
+
encoding: "utf-8",
|
|
596
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
597
|
+
});
|
|
598
|
+
if (res.status !== 0 || res.error)
|
|
599
|
+
return null;
|
|
600
|
+
const out = (res.stdout ?? "").toString().trim();
|
|
601
|
+
return out.length > 0 ? out : null;
|
|
602
|
+
} catch {
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
function readGitContext(cwd) {
|
|
607
|
+
const root = runGit(["rev-parse", "--show-toplevel"], cwd);
|
|
608
|
+
if (!root)
|
|
609
|
+
return null;
|
|
610
|
+
const head = runGit(["rev-parse", "HEAD"], cwd);
|
|
611
|
+
const branch = runGit(["branch", "--show-current"], cwd);
|
|
612
|
+
return { root, head, branch };
|
|
613
|
+
}
|
|
614
|
+
function normalizeError(input) {
|
|
615
|
+
if (input instanceof Error) {
|
|
616
|
+
return {
|
|
617
|
+
name: input.name || "Error",
|
|
618
|
+
message: input.message || "",
|
|
619
|
+
stack: input.stack || ""
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
const message = typeof input === "string" ? input : safeStringify(input);
|
|
623
|
+
return {
|
|
624
|
+
name: "NonError",
|
|
625
|
+
message,
|
|
626
|
+
stack: message
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
function safeStringify(value) {
|
|
630
|
+
try {
|
|
631
|
+
return JSON.stringify(value);
|
|
632
|
+
} catch {
|
|
633
|
+
return String(value);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function buildErrorReport(input) {
|
|
637
|
+
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
638
|
+
const id = createErrorReportId();
|
|
639
|
+
const normalized = normalizeError(input.error);
|
|
640
|
+
const argvResult = redactArgv(input.argv);
|
|
641
|
+
const messageResult = redactString(normalized.message);
|
|
642
|
+
const stackResult = redactString(normalized.stack);
|
|
643
|
+
const env = filterEnv(input.env ?? process.env);
|
|
644
|
+
const command = ["task0", ...argvResult.argv].join(" ");
|
|
645
|
+
const git = input.includeGit === false ? null : readGitContext(input.cwd);
|
|
646
|
+
return {
|
|
647
|
+
schema_version: ERROR_REPORT_SCHEMA_VERSION,
|
|
648
|
+
id,
|
|
649
|
+
captured_at: now.toISOString(),
|
|
650
|
+
origin: input.origin,
|
|
651
|
+
command,
|
|
652
|
+
argv: argvResult.argv,
|
|
653
|
+
cwd: input.cwd,
|
|
654
|
+
task0_version: input.task0Version,
|
|
655
|
+
node_version: process.version,
|
|
656
|
+
platform: process.platform,
|
|
657
|
+
arch: process.arch,
|
|
658
|
+
pid: process.pid,
|
|
659
|
+
ppid: typeof process.ppid === "number" ? process.ppid : 0,
|
|
660
|
+
git,
|
|
661
|
+
error: {
|
|
662
|
+
name: normalized.name,
|
|
663
|
+
message: messageResult.value,
|
|
664
|
+
stack: stackResult.value,
|
|
665
|
+
exit_code: input.exitCode ?? null
|
|
666
|
+
},
|
|
667
|
+
env,
|
|
668
|
+
redaction: {
|
|
669
|
+
argv_redacted: argvResult.redacted,
|
|
670
|
+
message_redacted: messageResult.redacted,
|
|
671
|
+
stack_redacted: stackResult.redacted,
|
|
672
|
+
env_strategy: "allowlist"
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
function writeErrorReportSync(report, rootOverride) {
|
|
677
|
+
const root = rootOverride ?? errorsRoot();
|
|
678
|
+
const dirName = errorReportDirName(new Date(report.captured_at), report.id);
|
|
679
|
+
const dir = path6.join(root, dirName);
|
|
680
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
681
|
+
const finalPath = path6.join(dir, REPORT_FILENAME);
|
|
682
|
+
const tmpPath = path6.join(dir, TMP_FILENAME);
|
|
683
|
+
const json = JSON.stringify(report, null, 2);
|
|
684
|
+
fs6.writeFileSync(tmpPath, json, "utf-8");
|
|
685
|
+
fs6.renameSync(tmpPath, finalPath);
|
|
686
|
+
return { dir, path: finalPath };
|
|
687
|
+
}
|
|
688
|
+
function parseReportDir(name) {
|
|
689
|
+
const m = /^(\d{8}T\d{6}Z)-(err_[0-9a-f]{8})$/.exec(name);
|
|
690
|
+
if (!m)
|
|
691
|
+
return null;
|
|
692
|
+
return { ts: m[1], id: m[2] };
|
|
693
|
+
}
|
|
694
|
+
function listErrorReports(rootOverride) {
|
|
695
|
+
const root = rootOverride ?? errorsRoot();
|
|
696
|
+
const result = {
|
|
697
|
+
reports: [],
|
|
698
|
+
skipped: { unsupported_schema: 0, unreadable: 0 }
|
|
699
|
+
};
|
|
700
|
+
let entries;
|
|
701
|
+
try {
|
|
702
|
+
entries = fs6.readdirSync(root, { withFileTypes: true });
|
|
703
|
+
} catch (err) {
|
|
704
|
+
if (err.code === "ENOENT")
|
|
705
|
+
return result;
|
|
706
|
+
throw err;
|
|
707
|
+
}
|
|
708
|
+
for (const entry of entries) {
|
|
709
|
+
if (!entry.isDirectory())
|
|
710
|
+
continue;
|
|
711
|
+
const parsed = parseReportDir(entry.name);
|
|
712
|
+
if (!parsed)
|
|
713
|
+
continue;
|
|
714
|
+
const dir = path6.join(root, entry.name);
|
|
715
|
+
const file = path6.join(dir, REPORT_FILENAME);
|
|
716
|
+
let raw;
|
|
717
|
+
try {
|
|
718
|
+
raw = fs6.readFileSync(file, "utf-8");
|
|
719
|
+
} catch {
|
|
720
|
+
result.skipped.unreadable += 1;
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
let data = null;
|
|
724
|
+
try {
|
|
725
|
+
data = JSON.parse(raw);
|
|
726
|
+
} catch {
|
|
727
|
+
result.skipped.unreadable += 1;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (!data || data.schema_version !== ERROR_REPORT_SCHEMA_VERSION) {
|
|
731
|
+
result.skipped.unsupported_schema += 1;
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
let size = 0;
|
|
735
|
+
try {
|
|
736
|
+
size = fs6.statSync(file).size;
|
|
737
|
+
} catch {
|
|
738
|
+
}
|
|
739
|
+
result.reports.push({
|
|
740
|
+
id: data.id ?? parsed.id,
|
|
741
|
+
dir,
|
|
742
|
+
path: file,
|
|
743
|
+
captured_at: data.captured_at ?? "",
|
|
744
|
+
command: data.command ?? "",
|
|
745
|
+
error_name: data.error?.name ?? "",
|
|
746
|
+
error_message: data.error?.message ?? "",
|
|
747
|
+
size_bytes: size
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
result.reports.sort((a, b) => {
|
|
751
|
+
if (a.captured_at === b.captured_at)
|
|
752
|
+
return b.id.localeCompare(a.id);
|
|
753
|
+
return b.captured_at.localeCompare(a.captured_at);
|
|
754
|
+
});
|
|
755
|
+
return result;
|
|
756
|
+
}
|
|
757
|
+
function resolveErrorReport(query, rootOverride) {
|
|
758
|
+
const list = listErrorReports(rootOverride);
|
|
759
|
+
const exact = list.reports.find((r) => r.id === query);
|
|
760
|
+
if (exact)
|
|
761
|
+
return { kind: "hit", summary: exact };
|
|
762
|
+
const prefixMatches = list.reports.filter((r) => r.id.startsWith(query));
|
|
763
|
+
if (prefixMatches.length === 1)
|
|
764
|
+
return { kind: "hit", summary: prefixMatches[0] };
|
|
765
|
+
if (prefixMatches.length > 1) {
|
|
766
|
+
return { kind: "ambiguous", query, candidates: prefixMatches };
|
|
767
|
+
}
|
|
768
|
+
return { kind: "miss", query };
|
|
769
|
+
}
|
|
770
|
+
function readErrorReport(summary) {
|
|
771
|
+
const raw = fs6.readFileSync(summary.path, "utf-8");
|
|
772
|
+
return JSON.parse(raw);
|
|
773
|
+
}
|
|
774
|
+
function pruneErrorReports(opts, rootOverride) {
|
|
775
|
+
const list = listErrorReports(rootOverride);
|
|
776
|
+
const removed = [];
|
|
777
|
+
if (opts.all) {
|
|
778
|
+
for (const r of list.reports) {
|
|
779
|
+
if (removeReportDir(r.dir))
|
|
780
|
+
removed.push(r.dir);
|
|
781
|
+
}
|
|
782
|
+
return { removed, kept: 0 };
|
|
783
|
+
}
|
|
784
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
785
|
+
if (typeof opts.olderThanMs === "number" && opts.olderThanMs >= 0) {
|
|
786
|
+
const cutoff = Date.now() - opts.olderThanMs;
|
|
787
|
+
for (const r of list.reports) {
|
|
788
|
+
const t = Date.parse(r.captured_at);
|
|
789
|
+
if (!Number.isFinite(t))
|
|
790
|
+
continue;
|
|
791
|
+
if (t < cutoff)
|
|
792
|
+
toRemove.add(r.dir);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
if (typeof opts.keep === "number" && opts.keep >= 0) {
|
|
796
|
+
for (let i = opts.keep; i < list.reports.length; i += 1) {
|
|
797
|
+
toRemove.add(list.reports[i].dir);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
for (const dir of toRemove) {
|
|
801
|
+
if (removeReportDir(dir))
|
|
802
|
+
removed.push(dir);
|
|
803
|
+
}
|
|
804
|
+
const kept = list.reports.length - removed.length;
|
|
805
|
+
return { removed, kept };
|
|
806
|
+
}
|
|
807
|
+
function removeReportDir(dir) {
|
|
808
|
+
try {
|
|
809
|
+
fs6.rmSync(dir, { recursive: true, force: true });
|
|
810
|
+
return true;
|
|
811
|
+
} catch {
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
var REPORT_FILENAME, TMP_FILENAME;
|
|
816
|
+
var init_error_reports = __esm({
|
|
817
|
+
"../../packages/shared/dist/node/error-reports.js"() {
|
|
818
|
+
"use strict";
|
|
819
|
+
init_error_report();
|
|
820
|
+
init_redact();
|
|
821
|
+
REPORT_FILENAME = "report.json";
|
|
822
|
+
TMP_FILENAME = "report.json.tmp";
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
// ../../packages/shared/dist/node/file-lock.js
|
|
827
|
+
import fs7 from "fs";
|
|
828
|
+
import os3 from "os";
|
|
829
|
+
import path7 from "path";
|
|
830
|
+
var init_file_lock = __esm({
|
|
831
|
+
"../../packages/shared/dist/node/file-lock.js"() {
|
|
832
|
+
"use strict";
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// ../../packages/shared/dist/node/tmux.js
|
|
837
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
838
|
+
var init_tmux = __esm({
|
|
839
|
+
"../../packages/shared/dist/node/tmux.js"() {
|
|
840
|
+
"use strict";
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
// ../../packages/shared/dist/node/index.js
|
|
845
|
+
var init_node = __esm({
|
|
846
|
+
"../../packages/shared/dist/node/index.js"() {
|
|
847
|
+
"use strict";
|
|
848
|
+
init_yaml();
|
|
849
|
+
init_scanner();
|
|
850
|
+
init_open_questions();
|
|
851
|
+
init_task_state();
|
|
852
|
+
init_error_reports();
|
|
853
|
+
init_redact();
|
|
854
|
+
init_file_lock();
|
|
855
|
+
init_tmux();
|
|
856
|
+
init_error_report();
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
// src/core/task-state.ts
|
|
861
|
+
var task_state_exports = {};
|
|
862
|
+
__export(task_state_exports, {
|
|
863
|
+
findProjectRoot: () => findProjectRoot,
|
|
864
|
+
latestArtifact: () => latestArtifact,
|
|
865
|
+
nextArtifactIndex: () => nextArtifactIndex,
|
|
866
|
+
readProjectConfig: () => readProjectConfig,
|
|
867
|
+
readTaskYaml: () => readTaskYaml,
|
|
868
|
+
readWorkflow: () => readWorkflow,
|
|
869
|
+
resolveTaskByObjectId: () => resolveTaskByObjectId,
|
|
870
|
+
resolveTasksDir: () => resolveTasksDir,
|
|
871
|
+
updateWorkflow: () => updateWorkflow,
|
|
872
|
+
withTaskYamlLock: () => withTaskYamlLock,
|
|
873
|
+
writeTaskYaml: () => writeTaskYaml
|
|
874
|
+
});
|
|
875
|
+
import fs9 from "fs";
|
|
876
|
+
function readWorkflow(taskYml) {
|
|
877
|
+
return readTaskWorkflow(taskYml);
|
|
878
|
+
}
|
|
879
|
+
async function updateWorkflow(taskYml, patch) {
|
|
880
|
+
return updateTaskWorkflow(taskYml, patch);
|
|
881
|
+
}
|
|
882
|
+
function nextArtifactIndex(taskDir, prefix, ext = "md") {
|
|
883
|
+
const pattern = new RegExp(`^${prefix}-(\\d+).*\\.${ext}$`);
|
|
884
|
+
const entries = fs9.readdirSync(taskDir);
|
|
885
|
+
const indices = entries.map((name) => name.match(pattern)?.[1]).filter((v) => v != null).map(Number);
|
|
886
|
+
const next = indices.length > 0 ? Math.max(...indices) + 1 : 1;
|
|
887
|
+
return String(next).padStart(2, "0");
|
|
888
|
+
}
|
|
889
|
+
function latestArtifact(taskDir, pattern) {
|
|
890
|
+
if (!fs9.existsSync(taskDir)) return null;
|
|
891
|
+
const matches = fs9.readdirSync(taskDir).filter((name) => pattern.test(name));
|
|
892
|
+
if (matches.length === 0) return null;
|
|
893
|
+
matches.sort();
|
|
894
|
+
return matches[matches.length - 1] || null;
|
|
895
|
+
}
|
|
896
|
+
var init_task_state2 = __esm({
|
|
897
|
+
"src/core/task-state.ts"() {
|
|
898
|
+
"use strict";
|
|
899
|
+
init_node();
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
// src/main.ts
|
|
904
|
+
import { Command as Command22 } from "commander";
|
|
905
|
+
|
|
906
|
+
// src/commands/source.ts
|
|
907
|
+
import { Command } from "commander";
|
|
908
|
+
import path8 from "path";
|
|
909
|
+
import chalk from "chalk";
|
|
910
|
+
|
|
911
|
+
// src/core/config.ts
|
|
912
|
+
import fs from "fs";
|
|
913
|
+
import path from "path";
|
|
914
|
+
import yaml from "js-yaml";
|
|
915
|
+
var CONFIG_DIR = path.join(
|
|
916
|
+
process.env.HOME || process.env.USERPROFILE || "~",
|
|
917
|
+
".config",
|
|
918
|
+
"task0"
|
|
919
|
+
);
|
|
920
|
+
var CONFIG_FILE = path.join(CONFIG_DIR, "config.yml");
|
|
921
|
+
function ensureConfigDir() {
|
|
922
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
923
|
+
}
|
|
924
|
+
function defaultConfig() {
|
|
925
|
+
return { sources: [] };
|
|
926
|
+
}
|
|
927
|
+
function loadConfig() {
|
|
928
|
+
if (!fs.existsSync(CONFIG_FILE)) return defaultConfig();
|
|
929
|
+
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
930
|
+
const data = yaml.load(raw);
|
|
931
|
+
return {
|
|
932
|
+
...data ?? {},
|
|
933
|
+
sources: Array.isArray(data?.sources) ? data.sources : []
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
function saveConfig(config) {
|
|
937
|
+
ensureConfigDir();
|
|
938
|
+
fs.writeFileSync(CONFIG_FILE, yaml.dump(config, { lineWidth: 120 }), "utf-8");
|
|
939
|
+
}
|
|
940
|
+
function addSource(entry) {
|
|
941
|
+
const config = loadConfig();
|
|
942
|
+
const existing = config.sources.findIndex((s) => s.name === entry.name);
|
|
943
|
+
if (existing >= 0) {
|
|
944
|
+
config.sources[existing] = entry;
|
|
945
|
+
} else {
|
|
946
|
+
config.sources.push(entry);
|
|
947
|
+
}
|
|
948
|
+
saveConfig(config);
|
|
949
|
+
}
|
|
950
|
+
function removeSource(name) {
|
|
951
|
+
const config = loadConfig();
|
|
952
|
+
const idx = config.sources.findIndex((s) => s.name === name);
|
|
953
|
+
if (idx < 0) return false;
|
|
954
|
+
config.sources.splice(idx, 1);
|
|
955
|
+
saveConfig(config);
|
|
956
|
+
return true;
|
|
957
|
+
}
|
|
958
|
+
function getSource(name) {
|
|
959
|
+
return loadConfig().sources.find((s) => s.name === name);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// src/commands/source.ts
|
|
963
|
+
init_node();
|
|
964
|
+
|
|
965
|
+
// src/types.ts
|
|
966
|
+
init_task();
|
|
967
|
+
|
|
968
|
+
// src/commands/source.ts
|
|
969
|
+
var source = new Command("source").description("Manage task sources");
|
|
970
|
+
source.command("add <path>").description("Add a local project as task source").option("-n, --name <name>", "Source name (defaults to directory name)").action((inputPath, opts) => {
|
|
971
|
+
const absPath = path8.resolve(inputPath);
|
|
972
|
+
const name = opts.name || path8.basename(absPath);
|
|
973
|
+
const result = scanProject(absPath);
|
|
974
|
+
if (result.errors.length > 0 && result.tasks.length === 0) {
|
|
975
|
+
for (const err of result.errors) console.error(chalk.red(` error: ${err}`));
|
|
976
|
+
process.exit(1);
|
|
977
|
+
}
|
|
978
|
+
addSource({ name, type: "project", path: absPath, enabled: true });
|
|
979
|
+
console.log(chalk.green(`Added source "${name}" \u2192 ${absPath}`));
|
|
980
|
+
console.log(` ${result.tasks.length} tasks found`);
|
|
981
|
+
if (result.errors.length > 0) {
|
|
982
|
+
for (const err of result.errors) console.warn(chalk.yellow(` warn: ${err}`));
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
source.command("list").description("List registered task sources").action(() => {
|
|
986
|
+
const config = loadConfig();
|
|
987
|
+
if (config.sources.length === 0) {
|
|
988
|
+
console.log("No sources registered. Use `task0 source add <path>` to add one.");
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
for (const s of config.sources) {
|
|
992
|
+
const status = s.enabled ? chalk.green("\u25CF") : chalk.dim("\u25CB");
|
|
993
|
+
console.log(`${status} ${s.name} ${chalk.dim(s.type)} ${s.path}`);
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
source.command("remove <name>").description("Remove a task source").action((name) => {
|
|
997
|
+
if (removeSource(name)) {
|
|
998
|
+
console.log(chalk.green(`Removed source "${name}"`));
|
|
999
|
+
} else {
|
|
1000
|
+
console.error(chalk.red(`Source "${name}" not found`));
|
|
1001
|
+
process.exit(1);
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
source.command("scan [name]").description("Scan source(s) and display tasks").option("--json", "Output as JSON").action((name, opts) => {
|
|
1005
|
+
const config = loadConfig();
|
|
1006
|
+
const sources = name ? (() => {
|
|
1007
|
+
const s = getSource(name);
|
|
1008
|
+
if (!s) {
|
|
1009
|
+
console.error(chalk.red(`Source "${name}" not found`));
|
|
1010
|
+
process.exit(1);
|
|
1011
|
+
}
|
|
1012
|
+
return [s];
|
|
1013
|
+
})() : config.sources.filter((s) => s.enabled);
|
|
1014
|
+
if (sources.length === 0) {
|
|
1015
|
+
console.log("No sources to scan.");
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const allTasks = [];
|
|
1019
|
+
for (const s of sources) {
|
|
1020
|
+
const result = scanProject(s.path, s.name);
|
|
1021
|
+
if (opts.json) {
|
|
1022
|
+
allTasks.push(...result.tasks.map((t) => ({ ...t, _source: s.name })));
|
|
1023
|
+
} else {
|
|
1024
|
+
console.log(chalk.bold(`
|
|
1025
|
+
${s.name}`) + chalk.dim(` (${s.path})`));
|
|
1026
|
+
if (result.errors.length > 0) {
|
|
1027
|
+
for (const err of result.errors) console.warn(chalk.yellow(` warn: ${err}`));
|
|
1028
|
+
}
|
|
1029
|
+
const objectIdWidth = Math.max(...result.tasks.map((t) => (t.object_id || "").length), 9);
|
|
1030
|
+
for (const t of result.tasks) {
|
|
1031
|
+
const statusColor2 = isActiveTaskStatus(t.status) ? chalk.green : t.status === "blocked" ? chalk.red : t.status === "todo" ? chalk.yellow : t.status === "done" ? chalk.dim : chalk.white;
|
|
1032
|
+
const objectId = chalk.cyan((t.object_id || "-").padEnd(objectIdWidth));
|
|
1033
|
+
console.log(` ${objectId} ${statusColor2(t.status.padEnd(8))} ${chalk.dim(t.id)} ${t.title}`);
|
|
1034
|
+
}
|
|
1035
|
+
console.log(chalk.dim(` ${result.tasks.length} tasks`));
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
if (opts.json) {
|
|
1039
|
+
console.log(JSON.stringify(allTasks, null, 2));
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
// src/commands/project.ts
|
|
1044
|
+
import { Command as Command2 } from "commander";
|
|
1045
|
+
import fs8 from "fs";
|
|
1046
|
+
import path9 from "path";
|
|
1047
|
+
import yaml4 from "js-yaml";
|
|
1048
|
+
import chalk2 from "chalk";
|
|
1049
|
+
|
|
1050
|
+
// ../../packages/shared/dist/index.js
|
|
1051
|
+
init_object_id();
|
|
1052
|
+
|
|
1053
|
+
// src/commands/project.ts
|
|
1054
|
+
var project = new Command2("project").description("Manage projects");
|
|
1055
|
+
function readProjectObjectId(projectPath) {
|
|
1056
|
+
const ymlPath = path9.join(projectPath, "task0.yml");
|
|
1057
|
+
if (!fs8.existsSync(ymlPath)) return "-";
|
|
1058
|
+
try {
|
|
1059
|
+
const raw = yaml4.load(fs8.readFileSync(ymlPath, "utf-8"));
|
|
1060
|
+
const id = raw && typeof raw.object_id === "string" ? raw.object_id : "";
|
|
1061
|
+
return id || "-";
|
|
1062
|
+
} catch {
|
|
1063
|
+
return "-";
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
project.command("list").description("List registered projects").action(() => {
|
|
1067
|
+
const projects = loadConfig().sources.filter((s) => s.type === "project");
|
|
1068
|
+
if (projects.length === 0) {
|
|
1069
|
+
console.log("No projects registered. Use `task0 source add <path>` to add one.");
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
const rows = projects.map((p) => ({ p, objectId: readProjectObjectId(p.path) }));
|
|
1073
|
+
const objectIdWidth = Math.max(...rows.map((r) => r.objectId.length), 9);
|
|
1074
|
+
const nameWidth = Math.max(...rows.map((r) => r.p.name.length), 4);
|
|
1075
|
+
for (const { p, objectId } of rows) {
|
|
1076
|
+
const status = p.enabled ? chalk2.green("\u25CF") : chalk2.dim("\u25CB");
|
|
1077
|
+
const oid = chalk2.cyan(objectId.padEnd(objectIdWidth));
|
|
1078
|
+
const name = p.name.padEnd(nameWidth);
|
|
1079
|
+
console.log(`${status} ${oid} ${name} ${chalk2.dim(p.path)}`);
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
project.command("init").description("Initialize task0.yml in the current directory").option("-d, --tasks-dir <dir>", "Tasks directory", ".task0/tasks").action((opts) => {
|
|
1083
|
+
const cwd = process.cwd();
|
|
1084
|
+
const ymlPath = path9.join(cwd, "task0.yml");
|
|
1085
|
+
if (fs8.existsSync(ymlPath)) {
|
|
1086
|
+
console.error(chalk2.yellow("task0.yml already exists"));
|
|
1087
|
+
process.exit(1);
|
|
1088
|
+
}
|
|
1089
|
+
const config = { kind: "project", object_id: generateObjectId("project"), tasks_dir: opts.tasksDir };
|
|
1090
|
+
fs8.writeFileSync(ymlPath, yaml4.dump(config), "utf-8");
|
|
1091
|
+
const tasksDir = path9.join(cwd, opts.tasksDir);
|
|
1092
|
+
fs8.mkdirSync(tasksDir, { recursive: true });
|
|
1093
|
+
console.log(chalk2.green("Initialized task0 project"));
|
|
1094
|
+
console.log(` ${ymlPath}`);
|
|
1095
|
+
console.log(` ${tasksDir}/`);
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
// src/commands/task.ts
|
|
1099
|
+
import { Command as Command8 } from "commander";
|
|
1100
|
+
import { execSync } from "child_process";
|
|
1101
|
+
import fs14 from "fs";
|
|
1102
|
+
import path12 from "path";
|
|
1103
|
+
import yaml5 from "js-yaml";
|
|
1104
|
+
import chalk8 from "chalk";
|
|
1105
|
+
|
|
1106
|
+
// src/lib/api.ts
|
|
1107
|
+
var DEFAULT_BASE = "http://127.0.0.1:4318";
|
|
1108
|
+
function apiBaseUrl() {
|
|
1109
|
+
return process.env.TASK0_API_URL || DEFAULT_BASE;
|
|
1110
|
+
}
|
|
1111
|
+
async function request(method, pathname, body) {
|
|
1112
|
+
const url = apiBaseUrl().replace(/\/$/, "") + pathname;
|
|
1113
|
+
let res;
|
|
1114
|
+
try {
|
|
1115
|
+
res = await fetch(url, {
|
|
1116
|
+
method,
|
|
1117
|
+
headers: body !== void 0 ? { "content-type": "application/json" } : void 0,
|
|
1118
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
1119
|
+
});
|
|
1120
|
+
} catch (error2) {
|
|
1121
|
+
const err = new Error(
|
|
1122
|
+
`Cannot reach task0 API at ${apiBaseUrl()}. Start it with \`task0 serve\` (or \`task0 ui\` for dev).`
|
|
1123
|
+
);
|
|
1124
|
+
err.cause = error2;
|
|
1125
|
+
throw err;
|
|
1126
|
+
}
|
|
1127
|
+
const text = await res.text();
|
|
1128
|
+
let parsed = null;
|
|
1129
|
+
try {
|
|
1130
|
+
parsed = text ? JSON.parse(text) : null;
|
|
1131
|
+
} catch {
|
|
1132
|
+
parsed = text;
|
|
1133
|
+
}
|
|
1134
|
+
if (!res.ok) {
|
|
1135
|
+
const message = parsed?.error || text || res.statusText;
|
|
1136
|
+
const err = new Error(`API ${method} ${pathname} failed (${res.status}): ${message}`);
|
|
1137
|
+
err.status = res.status;
|
|
1138
|
+
err.body = parsed;
|
|
1139
|
+
throw err;
|
|
1140
|
+
}
|
|
1141
|
+
return parsed;
|
|
1142
|
+
}
|
|
1143
|
+
var api = {
|
|
1144
|
+
get: (path21) => request("GET", path21),
|
|
1145
|
+
post: (path21, body) => request("POST", path21, body ?? {}),
|
|
1146
|
+
put: (path21, body) => request("PUT", path21, body ?? {}),
|
|
1147
|
+
patch: (path21, body) => request("PATCH", path21, body ?? {}),
|
|
1148
|
+
del: (path21) => request("DELETE", path21)
|
|
1149
|
+
};
|
|
1150
|
+
|
|
1151
|
+
// src/commands/task/triage.ts
|
|
1152
|
+
import { Command as Command3 } from "commander";
|
|
1153
|
+
import chalk3 from "chalk";
|
|
1154
|
+
import fs10 from "fs";
|
|
1155
|
+
import path10 from "path";
|
|
1156
|
+
|
|
1157
|
+
// src/core/runtime-wait.ts
|
|
1158
|
+
async function getRuntime(id) {
|
|
1159
|
+
return api.get(`/api/runtimes/${encodeURIComponent(id)}`);
|
|
1160
|
+
}
|
|
1161
|
+
async function waitForRuntime(id, opts = {}) {
|
|
1162
|
+
const interval = opts.intervalMs ?? 2e3;
|
|
1163
|
+
const deadline = opts.timeoutMs ? Date.now() + opts.timeoutMs : null;
|
|
1164
|
+
let lastStatus = "";
|
|
1165
|
+
while (true) {
|
|
1166
|
+
const snap = await getRuntime(id);
|
|
1167
|
+
if (snap.status !== lastStatus) {
|
|
1168
|
+
lastStatus = snap.status;
|
|
1169
|
+
opts.onTick?.(snap);
|
|
1170
|
+
}
|
|
1171
|
+
if (snap.status === "done" || snap.status === "error") return snap;
|
|
1172
|
+
if (deadline && Date.now() > deadline) {
|
|
1173
|
+
throw new Error(`Timeout waiting for runtime ${id} (last status: ${snap.status})`);
|
|
1174
|
+
}
|
|
1175
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// src/core/model-options.ts
|
|
1180
|
+
function resolveModelOptions(agent2, cli, warn = () => {
|
|
1181
|
+
}) {
|
|
1182
|
+
const entry = loadConfig().agentModels?.[agent2];
|
|
1183
|
+
const known = entry?.models ?? [];
|
|
1184
|
+
const cliModel = cli.model?.trim() || void 0;
|
|
1185
|
+
let model = cliModel || entry?.defaultModel?.trim() || void 0;
|
|
1186
|
+
if (cliModel) {
|
|
1187
|
+
const byAlias = known.find((m) => m.alias && m.alias === cliModel);
|
|
1188
|
+
if (byAlias) {
|
|
1189
|
+
model = byAlias.id;
|
|
1190
|
+
} else if (!known.some((m) => m.id === cliModel)) {
|
|
1191
|
+
warn(`model "${cliModel}" not in cached list for ${agent2} (run \`task0 models refresh\`); forwarding as-is`);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
const effort = cli.effort?.trim() || entry?.defaultEffort?.trim() || void 0;
|
|
1195
|
+
return {
|
|
1196
|
+
...model ? { model } : {},
|
|
1197
|
+
...effort ? { effort } : {}
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// src/commands/task/triage.ts
|
|
1202
|
+
init_task_state2();
|
|
1203
|
+
var ISSUE_DETAIL_RE = /^ISSUE-\d+\.md$/;
|
|
1204
|
+
var TRIAGE_SKILL_NAME = "triage";
|
|
1205
|
+
function resolveSkillFilePath(projectRoot, skillName) {
|
|
1206
|
+
const p = path10.join(projectRoot, ".claude", "skills", skillName, "SKILL.md");
|
|
1207
|
+
return fs10.existsSync(p) ? p : null;
|
|
1208
|
+
}
|
|
1209
|
+
var triage = new Command3("triage").description("Decompose IDEA into ISSUE.md + ISSUE-NN.md").argument("<objectId>", "Task object_id (tsk_XXXXX)").option("--agent <name>", "Agent (claude-code|codex)", "claude-code").option("--model <id>", "Model id or alias (see `task0 models refresh`)").option("--effort <level>", "Reasoning effort (e.g. low|medium|high)").option("--idea <file>", "IDEA file (default: latest IDEA-NN.md)").option("--force", "Overwrite existing ISSUE files").option("--wait", "Wait for completion").option("--json", "Output JSON").action(async (objectId, opts) => {
|
|
1210
|
+
try {
|
|
1211
|
+
const loc = resolveTaskByObjectId(objectId);
|
|
1212
|
+
const ideaFile = opts.idea || latestArtifact(loc.taskDir, /^IDEA-\d+\.md$/);
|
|
1213
|
+
if (!ideaFile) {
|
|
1214
|
+
console.error(chalk3.red(`No IDEA-NN.md found in ${loc.taskDir}. Pass --idea or create one first.`));
|
|
1215
|
+
process.exit(1);
|
|
1216
|
+
}
|
|
1217
|
+
const existingIssues = listIssueArtifacts(loc.taskDir);
|
|
1218
|
+
if (existingIssues.length > 0) {
|
|
1219
|
+
if (!opts.force) {
|
|
1220
|
+
console.error(chalk3.red(
|
|
1221
|
+
`ISSUE files already exist: ${existingIssues.join(", ")}. Pass --force to overwrite.`
|
|
1222
|
+
));
|
|
1223
|
+
process.exit(1);
|
|
1224
|
+
}
|
|
1225
|
+
for (const name of existingIssues) {
|
|
1226
|
+
fs10.rmSync(path10.join(loc.taskDir, name), { force: true });
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
if (opts.model || opts.effort) {
|
|
1230
|
+
resolveModelOptions(
|
|
1231
|
+
opts.agent,
|
|
1232
|
+
{ model: opts.model, effort: opts.effort },
|
|
1233
|
+
opts.json ? void 0 : (m) => console.error(chalk3.dim(m))
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
const skillFilePath = resolveSkillFilePath(loc.projectRoot, TRIAGE_SKILL_NAME);
|
|
1237
|
+
const prompt = [
|
|
1238
|
+
`task_id: ${objectId}`,
|
|
1239
|
+
`Skill: ${TRIAGE_SKILL_NAME}`,
|
|
1240
|
+
`Idea file: ${ideaFile}`
|
|
1241
|
+
].join("\n");
|
|
1242
|
+
const body = {
|
|
1243
|
+
task_id: objectId,
|
|
1244
|
+
prompt,
|
|
1245
|
+
...skillFilePath ? { skill_file_path: skillFilePath } : {}
|
|
1246
|
+
};
|
|
1247
|
+
const resp = await api.post(
|
|
1248
|
+
`/api/agents/${encodeURIComponent(opts.agent)}/run`,
|
|
1249
|
+
body
|
|
1250
|
+
);
|
|
1251
|
+
const runtimeId = resp.runtime.id;
|
|
1252
|
+
if (!opts.json) console.log(chalk3.green(`triage runtime ${runtimeId}`));
|
|
1253
|
+
await updateWorkflow(loc.taskYml, { runtimes: { triage: runtimeId } });
|
|
1254
|
+
if (opts.wait) {
|
|
1255
|
+
const final = await waitForRuntime(runtimeId, {
|
|
1256
|
+
onTick: (s) => {
|
|
1257
|
+
if (!opts.json) console.log(chalk3.dim(`[triage] ${s.status}${s.phase ? " " + s.phase : ""}`));
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
if (final.status !== "done") {
|
|
1261
|
+
console.error(chalk3.red(`triage failed: ${final.error || "unknown"}`));
|
|
1262
|
+
process.exit(1);
|
|
1263
|
+
}
|
|
1264
|
+
const hasOverview = fs10.existsSync(path10.join(loc.taskDir, "ISSUE.md"));
|
|
1265
|
+
const issueFiles = fs10.readdirSync(loc.taskDir).filter((name) => ISSUE_DETAIL_RE.test(name)).sort();
|
|
1266
|
+
if (!hasOverview || issueFiles.length === 0) {
|
|
1267
|
+
const missing = [];
|
|
1268
|
+
if (!hasOverview) missing.push("ISSUE.md");
|
|
1269
|
+
if (issueFiles.length === 0) missing.push("ISSUE-NN.md (\u22651)");
|
|
1270
|
+
console.error(chalk3.red(`triage completed but missing: ${missing.join(", ")}`));
|
|
1271
|
+
process.exit(1);
|
|
1272
|
+
}
|
|
1273
|
+
let blockingQuestionCount = 0;
|
|
1274
|
+
for (const name of issueFiles) {
|
|
1275
|
+
const content = fs10.readFileSync(path10.join(loc.taskDir, name), "utf-8");
|
|
1276
|
+
blockingQuestionCount += countBlockingQuestions(content);
|
|
1277
|
+
}
|
|
1278
|
+
await updateWorkflow(loc.taskYml, {
|
|
1279
|
+
phase: "triaged",
|
|
1280
|
+
issue_overview: "ISSUE.md",
|
|
1281
|
+
issue_files: issueFiles
|
|
1282
|
+
});
|
|
1283
|
+
if (!opts.json) {
|
|
1284
|
+
console.log(chalk3.green(`ISSUE.md (+${issueFiles.length} details)`));
|
|
1285
|
+
console.log(chalk3.dim(`blockingQuestionCount: ${blockingQuestionCount}`));
|
|
1286
|
+
} else {
|
|
1287
|
+
console.log(JSON.stringify({
|
|
1288
|
+
runtimeId,
|
|
1289
|
+
issueOverview: "ISSUE.md",
|
|
1290
|
+
issueFiles,
|
|
1291
|
+
blockingQuestionCount
|
|
1292
|
+
}, null, 2));
|
|
1293
|
+
}
|
|
1294
|
+
process.exit(blockingQuestionCount > 0 ? 2 : 0);
|
|
1295
|
+
}
|
|
1296
|
+
if (opts.json) console.log(JSON.stringify({ runtimeId }, null, 2));
|
|
1297
|
+
} catch (err) {
|
|
1298
|
+
console.error(chalk3.red(err.message));
|
|
1299
|
+
process.exit(1);
|
|
1300
|
+
}
|
|
1301
|
+
});
|
|
1302
|
+
function listIssueArtifacts(taskDir) {
|
|
1303
|
+
if (!fs10.existsSync(taskDir)) return [];
|
|
1304
|
+
return fs10.readdirSync(taskDir).filter((name) => name === "ISSUE.md" || ISSUE_DETAIL_RE.test(name)).sort();
|
|
1305
|
+
}
|
|
1306
|
+
function countBlockingQuestions(md) {
|
|
1307
|
+
const match = md.match(/## Open Questions\s*\n([\s\S]*?)(\n## |\n*$)/i);
|
|
1308
|
+
if (!match) return 0;
|
|
1309
|
+
const body = match[1].trim();
|
|
1310
|
+
if (!body || /^none\b/i.test(body)) return 0;
|
|
1311
|
+
const bullets = body.split("\n").filter((line) => /^\s*[-*]\s+\S/.test(line));
|
|
1312
|
+
return bullets.length;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// src/commands/task/exec.ts
|
|
1316
|
+
import { Command as Command4 } from "commander";
|
|
1317
|
+
import chalk4 from "chalk";
|
|
1318
|
+
import fs11 from "fs";
|
|
1319
|
+
import path11 from "path";
|
|
1320
|
+
init_task_state2();
|
|
1321
|
+
var PLAN_EXECUTE_SKILL_NAME = "plan-execute";
|
|
1322
|
+
function resolveSkillFilePath2(projectRoot, skillName) {
|
|
1323
|
+
const p = path11.join(projectRoot, ".claude", "skills", skillName, "SKILL.md");
|
|
1324
|
+
return fs11.existsSync(p) ? p : null;
|
|
1325
|
+
}
|
|
1326
|
+
var exec = new Command4("exec").description("Execute a plan against the task (cwd = project root; agent sets up its own worktree if needed)").argument("<objectId>", "Task object_id (tsk_XXXXX)").option("--agent <name>", "Agent (claude-code|codex|cursor)", "claude-code").option("--model <id>", "Model id or alias (see `task0 models refresh`)").option("--effort <level>", "Reasoning effort (e.g. low|medium|high)").option("--plan <file>", "Plan file (default: refined plan, else latest PLAN)").option("--no-commit", "Skip commit").option("--wait", "Wait for completion").option("--json", "Output JSON").action(async (objectId, opts) => {
|
|
1327
|
+
try {
|
|
1328
|
+
const loc = resolveTaskByObjectId(objectId);
|
|
1329
|
+
const wf = readWorkflow(loc.taskYml);
|
|
1330
|
+
const planFile = opts.plan || wf.refined_plan_file || latestArtifact(loc.taskDir, /^PLAN-\d+-refined\.md$/) || latestArtifact(loc.taskDir, /^PLAN-\d+-[a-z-]+\.md$/);
|
|
1331
|
+
if (!planFile) {
|
|
1332
|
+
console.error(chalk4.red("No plan file found. Run `task0 plan generate` / `task0 plan refine` first, or pass --plan."));
|
|
1333
|
+
process.exit(1);
|
|
1334
|
+
}
|
|
1335
|
+
if (opts.model || opts.effort) {
|
|
1336
|
+
resolveModelOptions(
|
|
1337
|
+
opts.agent,
|
|
1338
|
+
{ model: opts.model, effort: opts.effort },
|
|
1339
|
+
opts.json ? void 0 : (m) => console.error(chalk4.dim(m))
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
const skillFilePath = resolveSkillFilePath2(loc.projectRoot, PLAN_EXECUTE_SKILL_NAME);
|
|
1343
|
+
const prompt = [
|
|
1344
|
+
`task_id: ${objectId}`,
|
|
1345
|
+
`Skill: ${PLAN_EXECUTE_SKILL_NAME}`,
|
|
1346
|
+
`Plan file: ${planFile}`,
|
|
1347
|
+
opts.commit ? "Commit your changes when done." : "Do NOT commit \u2014 leave changes staged for review."
|
|
1348
|
+
// PR creation is delegated to SKILL.md; the CLI no longer flips a
|
|
1349
|
+
// server-side flag for it. If the operator wants a PR they can say
|
|
1350
|
+
// so in --additional-prompt or invoke the skill again with that.
|
|
1351
|
+
].filter(Boolean).join("\n");
|
|
1352
|
+
const body = {
|
|
1353
|
+
task_id: objectId,
|
|
1354
|
+
prompt,
|
|
1355
|
+
...skillFilePath ? { skill_file_path: skillFilePath } : {}
|
|
1356
|
+
};
|
|
1357
|
+
const resp = await api.post(
|
|
1358
|
+
`/api/agents/${encodeURIComponent(opts.agent)}/run`,
|
|
1359
|
+
body
|
|
1360
|
+
);
|
|
1361
|
+
const runtimeId = resp.runtime.id;
|
|
1362
|
+
if (!opts.json) console.log(chalk4.green(`exec runtime ${runtimeId} (plan: ${planFile})`));
|
|
1363
|
+
await updateWorkflow(loc.taskYml, { phase: "executing", runtimes: { exec: runtimeId } });
|
|
1364
|
+
if (opts.wait) {
|
|
1365
|
+
const final = await waitForRuntime(runtimeId, {
|
|
1366
|
+
onTick: (s) => {
|
|
1367
|
+
if (!opts.json) console.log(chalk4.dim(`[exec] ${s.status}${s.phase ? " " + s.phase : ""}`));
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
if (final.status === "done") {
|
|
1371
|
+
if (!opts.json) console.log(chalk4.green("exec completed"));
|
|
1372
|
+
if (opts.json) console.log(JSON.stringify({ runtimeId, planFile, result: final.result }, null, 2));
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
console.error(chalk4.red(`exec failed: ${final.error || "unknown"}`));
|
|
1376
|
+
process.exit(1);
|
|
1377
|
+
}
|
|
1378
|
+
if (opts.json) console.log(JSON.stringify({ runtimeId, planFile }, null, 2));
|
|
1379
|
+
} catch (err) {
|
|
1380
|
+
console.error(chalk4.red(err.message));
|
|
1381
|
+
process.exit(1);
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
// src/commands/task/summarize.ts
|
|
1386
|
+
import { Command as Command5 } from "commander";
|
|
1387
|
+
import chalk5 from "chalk";
|
|
1388
|
+
init_task_state2();
|
|
1389
|
+
var summarize = new Command5("summarize").description("Generate a concise title and tag set for a task (preview by default)").argument("<objectId>", "Task object_id (tsk_XXXXX)").option("--apply", "Write the generated summary to task0.yml (preview-only by default)").option("--json", "Output JSON").action(async (objectId, opts) => {
|
|
1390
|
+
try {
|
|
1391
|
+
resolveTaskByObjectId(objectId);
|
|
1392
|
+
const resp = await api.post(
|
|
1393
|
+
`/api/tasks/${encodeURIComponent(objectId)}/summarize`,
|
|
1394
|
+
{ apply: Boolean(opts.apply) }
|
|
1395
|
+
);
|
|
1396
|
+
if (opts.json) {
|
|
1397
|
+
console.log(JSON.stringify(resp, null, 2));
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
const { summary, applied } = resp;
|
|
1401
|
+
const tagList = summary.tags.length > 0 ? summary.tags.join(", ") : chalk5.dim("(none)");
|
|
1402
|
+
const label = applied ? chalk5.green("applied") : chalk5.yellow("preview");
|
|
1403
|
+
console.log(`${label}`);
|
|
1404
|
+
console.log(` title: ${summary.title}`);
|
|
1405
|
+
console.log(` tags: ${tagList}`);
|
|
1406
|
+
console.log(chalk5.dim(` at: ${summary.summarized_at}`));
|
|
1407
|
+
if (!applied) {
|
|
1408
|
+
console.log(chalk5.dim("\nRun again with --apply to write summary to task0.yml."));
|
|
1409
|
+
}
|
|
1410
|
+
} catch (err) {
|
|
1411
|
+
console.error(chalk5.red(err.message));
|
|
1412
|
+
process.exit(1);
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
// src/commands/task/comment.ts
|
|
1417
|
+
import fs12 from "fs";
|
|
1418
|
+
import { Command as Command6 } from "commander";
|
|
1419
|
+
import chalk6 from "chalk";
|
|
1420
|
+
var comment = new Command6("comment").description("Manage comments on a task");
|
|
1421
|
+
function fail(err) {
|
|
1422
|
+
console.error(chalk6.red(err.message));
|
|
1423
|
+
process.exit(1);
|
|
1424
|
+
}
|
|
1425
|
+
function readBodyFromOpts(opts) {
|
|
1426
|
+
if (opts.body !== void 0) return opts.body;
|
|
1427
|
+
if (opts.file === "-") return fs12.readFileSync(0, "utf-8");
|
|
1428
|
+
if (opts.file) return fs12.readFileSync(opts.file, "utf-8");
|
|
1429
|
+
throw new Error("Provide --body or --file");
|
|
1430
|
+
}
|
|
1431
|
+
function preview(body, width = 60) {
|
|
1432
|
+
const single = body.replace(/\s+/g, " ").trim();
|
|
1433
|
+
return single.length > width ? single.slice(0, width - 1) + "\u2026" : single;
|
|
1434
|
+
}
|
|
1435
|
+
function printRow(c) {
|
|
1436
|
+
const id = chalk6.cyan(c.id.padEnd(14));
|
|
1437
|
+
const author = chalk6.magenta(c.author.padEnd(12));
|
|
1438
|
+
console.log(`${id} ${author} ${preview(c.body)} ${chalk6.dim(c.updated_at)}`);
|
|
1439
|
+
}
|
|
1440
|
+
function printDetail(c) {
|
|
1441
|
+
console.log(`${chalk6.bold("id:")} ${c.id}`);
|
|
1442
|
+
console.log(`${chalk6.bold("author:")} ${c.author}`);
|
|
1443
|
+
console.log(`${chalk6.bold("created:")} ${c.created_at}`);
|
|
1444
|
+
console.log(`${chalk6.bold("updated:")} ${c.updated_at}`);
|
|
1445
|
+
console.log(chalk6.bold("body:"));
|
|
1446
|
+
console.log(c.body.split("\n").map((l) => " " + l).join("\n"));
|
|
1447
|
+
}
|
|
1448
|
+
async function resolveCommentByObjectId(cmtId) {
|
|
1449
|
+
const { type, resource } = await api.get(`/api/resolve/${encodeURIComponent(cmtId)}`);
|
|
1450
|
+
if (type !== "task_comment" || !resource.task_object_id) {
|
|
1451
|
+
throw new Error(`object ${cmtId} is not a task_comment (got ${type})`);
|
|
1452
|
+
}
|
|
1453
|
+
const { task_object_id, ...rest } = resource;
|
|
1454
|
+
return { taskObjectId: task_object_id, comment: rest };
|
|
1455
|
+
}
|
|
1456
|
+
comment.command("list <taskId>").description("List comments on a task (taskId is short id or tsk_)").option("--json", "Output JSON").action(async (taskId, opts) => {
|
|
1457
|
+
try {
|
|
1458
|
+
const { comments } = await api.get(
|
|
1459
|
+
`/api/tasks/${encodeURIComponent(taskId)}/comments`
|
|
1460
|
+
);
|
|
1461
|
+
if (opts.json) {
|
|
1462
|
+
console.log(JSON.stringify({ comments }, null, 2));
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
if (!comments.length) {
|
|
1466
|
+
console.log(chalk6.dim("No comments."));
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
for (const c of comments) printRow(c);
|
|
1470
|
+
} catch (err) {
|
|
1471
|
+
fail(err);
|
|
1472
|
+
}
|
|
1473
|
+
});
|
|
1474
|
+
comment.command("add <taskId>").description("Add a comment to a task").option("--body <text>", "Comment body (markdown)").option("--file <path>", "Read body from file (- for stdin)").option("--author <name>", "Author label (default: user)", "user").option("--json", "Output JSON").action(async (taskId, opts) => {
|
|
1475
|
+
try {
|
|
1476
|
+
const body = readBodyFromOpts(opts);
|
|
1477
|
+
const { comment: created } = await api.post(
|
|
1478
|
+
`/api/tasks/${encodeURIComponent(taskId)}/comments`,
|
|
1479
|
+
{ body, author: opts.author }
|
|
1480
|
+
);
|
|
1481
|
+
if (opts.json) {
|
|
1482
|
+
console.log(JSON.stringify({ comment: created }, null, 2));
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
console.log(chalk6.green(`Created ${created.id}`));
|
|
1486
|
+
printDetail(created);
|
|
1487
|
+
} catch (err) {
|
|
1488
|
+
fail(err);
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
comment.command("edit <cmtId>").description("Edit a comment by its cmt_ id").option("--body <text>", "New body (markdown)").option("--file <path>", "Read body from file (- for stdin)").option("--json", "Output JSON").action(async (cmtId, opts) => {
|
|
1492
|
+
try {
|
|
1493
|
+
const body = readBodyFromOpts(opts);
|
|
1494
|
+
const { comment: updated } = await api.patch(
|
|
1495
|
+
`/api/comments/${encodeURIComponent(cmtId)}`,
|
|
1496
|
+
{ body }
|
|
1497
|
+
);
|
|
1498
|
+
if (opts.json) {
|
|
1499
|
+
console.log(JSON.stringify({ comment: updated }, null, 2));
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
console.log(chalk6.green(`Updated ${updated.id}`));
|
|
1503
|
+
printDetail(updated);
|
|
1504
|
+
} catch (err) {
|
|
1505
|
+
fail(err);
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1508
|
+
comment.command("delete <cmtId>").description("Delete a comment by its cmt_ id").action(async (cmtId) => {
|
|
1509
|
+
try {
|
|
1510
|
+
const { taskObjectId } = await resolveCommentByObjectId(cmtId);
|
|
1511
|
+
await api.del(`/api/comments/${encodeURIComponent(cmtId)}`);
|
|
1512
|
+
console.log(chalk6.green(`Deleted ${cmtId}`) + chalk6.dim(` (task ${taskObjectId})`));
|
|
1513
|
+
} catch (err) {
|
|
1514
|
+
fail(err);
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
// src/commands/task/description.ts
|
|
1519
|
+
import fs13 from "fs";
|
|
1520
|
+
import { Command as Command7 } from "commander";
|
|
1521
|
+
import chalk7 from "chalk";
|
|
1522
|
+
var description = new Command7("description").description("Show or update the task description");
|
|
1523
|
+
function fail2(err) {
|
|
1524
|
+
console.error(chalk7.red(err.message));
|
|
1525
|
+
process.exit(1);
|
|
1526
|
+
}
|
|
1527
|
+
function readBodyFromOpts2(opts) {
|
|
1528
|
+
if (opts.body !== void 0) return opts.body;
|
|
1529
|
+
if (opts.file === "-") return fs13.readFileSync(0, "utf-8");
|
|
1530
|
+
if (opts.file) return fs13.readFileSync(opts.file, "utf-8");
|
|
1531
|
+
throw new Error("Provide --body or --file (use --file - to read stdin)");
|
|
1532
|
+
}
|
|
1533
|
+
description.command("show <taskId>").description("Print the current description (taskId is short id or tsk_)").option("--json", "Output JSON").action(async (taskId, opts) => {
|
|
1534
|
+
try {
|
|
1535
|
+
const { type, resource } = await api.get(
|
|
1536
|
+
`/api/resolve/${encodeURIComponent(taskId)}`
|
|
1537
|
+
);
|
|
1538
|
+
if (type !== "task") throw new Error(`object ${taskId} is not a task (got ${type})`);
|
|
1539
|
+
const desc = resource.description ?? "";
|
|
1540
|
+
if (opts.json) {
|
|
1541
|
+
console.log(JSON.stringify({ description: desc }, null, 2));
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
if (!desc) {
|
|
1545
|
+
console.log(chalk7.dim("(no description)"));
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
console.log(desc);
|
|
1549
|
+
} catch (err) {
|
|
1550
|
+
fail2(err);
|
|
1551
|
+
}
|
|
1552
|
+
});
|
|
1553
|
+
description.command("set <taskId>").description("Replace the task description (markdown)").option("--body <text>", "New description body").option("--file <path>", "Read description from file (- for stdin)").option("--json", "Output JSON").action(async (taskId, opts) => {
|
|
1554
|
+
try {
|
|
1555
|
+
const body = readBodyFromOpts2(opts);
|
|
1556
|
+
const resp = await api.patch(
|
|
1557
|
+
`/api/tasks/${encodeURIComponent(taskId)}/description`,
|
|
1558
|
+
{ description: body }
|
|
1559
|
+
);
|
|
1560
|
+
if (opts.json) {
|
|
1561
|
+
console.log(JSON.stringify(resp, null, 2));
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
console.log(chalk7.green("Updated description"));
|
|
1565
|
+
} catch (err) {
|
|
1566
|
+
fail2(err);
|
|
1567
|
+
}
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
// src/commands/task.ts
|
|
1571
|
+
var task = new Command8("task").description("Manage tasks");
|
|
1572
|
+
task.addCommand(triage);
|
|
1573
|
+
task.addCommand(exec);
|
|
1574
|
+
task.addCommand(summarize);
|
|
1575
|
+
task.addCommand(comment);
|
|
1576
|
+
task.addCommand(description);
|
|
1577
|
+
task.command("init <input>").description("Create a task from a description or Linear/GitHub issue URL").action(async (input) => {
|
|
1578
|
+
const cwd = process.cwd();
|
|
1579
|
+
const projectYml = path12.join(cwd, "task0.yml");
|
|
1580
|
+
if (!fs14.existsSync(projectYml)) {
|
|
1581
|
+
console.error(chalk8.red("Not a task0 project (task0.yml not found). Run `task0 project init` first."));
|
|
1582
|
+
process.exit(1);
|
|
1583
|
+
}
|
|
1584
|
+
const projectConfig = yaml5.load(fs14.readFileSync(projectYml, "utf-8"));
|
|
1585
|
+
if (projectConfig.kind !== "project") {
|
|
1586
|
+
console.error(chalk8.red('Invalid task0.yml: kind is not "project"'));
|
|
1587
|
+
process.exit(1);
|
|
1588
|
+
}
|
|
1589
|
+
try {
|
|
1590
|
+
const { task: task2 } = await api.post("/api/tasks/init", { projectPath: cwd, input });
|
|
1591
|
+
console.log(chalk8.green(`Created task "${task2.id}"`));
|
|
1592
|
+
console.log(` ${task2.path}`);
|
|
1593
|
+
} catch (err) {
|
|
1594
|
+
console.error(chalk8.red(err.message));
|
|
1595
|
+
process.exit(1);
|
|
1596
|
+
}
|
|
1597
|
+
});
|
|
1598
|
+
task.command("migrate").description("Add task0.yml to legacy task directories that lack one").option("--dry-run", "Show what would be created without writing").action((opts) => {
|
|
1599
|
+
const cwd = process.cwd();
|
|
1600
|
+
const projectYml = path12.join(cwd, "task0.yml");
|
|
1601
|
+
if (!fs14.existsSync(projectYml)) {
|
|
1602
|
+
console.error(chalk8.red("Not a task0 project (task0.yml not found). Run `task0 project init` first."));
|
|
1603
|
+
process.exit(1);
|
|
1604
|
+
}
|
|
1605
|
+
const projectConfig = yaml5.load(fs14.readFileSync(projectYml, "utf-8"));
|
|
1606
|
+
if (projectConfig.kind !== "project") {
|
|
1607
|
+
console.error(chalk8.red('Invalid task0.yml: kind is not "project"'));
|
|
1608
|
+
process.exit(1);
|
|
1609
|
+
}
|
|
1610
|
+
const tasksDir = path12.join(cwd, projectConfig.tasks_dir);
|
|
1611
|
+
if (!fs14.existsSync(tasksDir)) {
|
|
1612
|
+
console.error(chalk8.red(`Tasks directory not found: ${tasksDir}`));
|
|
1613
|
+
process.exit(1);
|
|
1614
|
+
}
|
|
1615
|
+
const entries = fs14.readdirSync(tasksDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1616
|
+
let migrated = 0;
|
|
1617
|
+
let skipped = 0;
|
|
1618
|
+
let seededObjectIds = 0;
|
|
1619
|
+
for (const name of entries) {
|
|
1620
|
+
const taskDir = path12.join(tasksDir, name);
|
|
1621
|
+
const taskYml = path12.join(taskDir, "task0.yml");
|
|
1622
|
+
if (fs14.existsSync(taskYml)) {
|
|
1623
|
+
skipped++;
|
|
1624
|
+
try {
|
|
1625
|
+
const raw = yaml5.load(fs14.readFileSync(taskYml, "utf-8"));
|
|
1626
|
+
if (raw && raw.kind === "task" && !raw.object_id) {
|
|
1627
|
+
raw.object_id = generateObjectId("task");
|
|
1628
|
+
if (opts.dryRun) {
|
|
1629
|
+
console.log(chalk8.dim(`[dry-run] seed object_id: ${taskYml}`));
|
|
1630
|
+
} else {
|
|
1631
|
+
fs14.writeFileSync(taskYml, yaml5.dump(raw, { lineWidth: 120 }), "utf-8");
|
|
1632
|
+
console.log(chalk8.green(` seed object_id: ${taskYml}`));
|
|
1633
|
+
}
|
|
1634
|
+
seededObjectIds++;
|
|
1635
|
+
}
|
|
1636
|
+
} catch {
|
|
1637
|
+
}
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1640
|
+
const match = name.match(/^(\d{8})_(.+)$/);
|
|
1641
|
+
const title = match ? match[2].replace(/[-_]/g, " ") : name;
|
|
1642
|
+
const createdAt = match ? `${match[1].slice(0, 4)}-${match[1].slice(4, 6)}-${match[1].slice(6, 8)}T00:00:00Z` : (/* @__PURE__ */ new Date()).toISOString();
|
|
1643
|
+
const taskConfig = {
|
|
1644
|
+
id: name,
|
|
1645
|
+
object_id: generateObjectId("task"),
|
|
1646
|
+
kind: "task",
|
|
1647
|
+
title,
|
|
1648
|
+
status: "todo",
|
|
1649
|
+
current_step: "",
|
|
1650
|
+
tags: [],
|
|
1651
|
+
created_at: createdAt
|
|
1652
|
+
};
|
|
1653
|
+
if (opts.dryRun) {
|
|
1654
|
+
console.log(chalk8.dim(`[dry-run] ${taskYml}`));
|
|
1655
|
+
} else {
|
|
1656
|
+
fs14.writeFileSync(taskYml, yaml5.dump(taskConfig), "utf-8");
|
|
1657
|
+
console.log(chalk8.green(` ${taskYml}`));
|
|
1658
|
+
}
|
|
1659
|
+
migrated++;
|
|
1660
|
+
}
|
|
1661
|
+
console.log(`
|
|
1662
|
+
Migrated ${migrated} tasks, skipped ${skipped} (already initialized), seeded object_id on ${seededObjectIds}`);
|
|
1663
|
+
});
|
|
1664
|
+
task.command("list").description("List tasks").option("-s, --status <status>", `Filter by status (${TASK_STATUS_VALUES.join("|")})`).option("--source <name>", "Filter by source name").option("--tag <tag>", "Filter by tag").option("--project <name>", "Filter by project name").option("--all", "Include done and archived tasks").option("--json", "Output JSON").action(async (opts) => {
|
|
1665
|
+
try {
|
|
1666
|
+
const query = opts.source ? `?source=${encodeURIComponent(opts.source)}` : "";
|
|
1667
|
+
const { tasks, errors } = await api.get(`/api/tasks${query}`);
|
|
1668
|
+
let filtered = tasks;
|
|
1669
|
+
if (opts.status) filtered = filtered.filter((t) => t.status === opts.status);
|
|
1670
|
+
if (opts.tag) filtered = filtered.filter((t) => (t.display_tags ?? t.tags ?? []).includes(opts.tag));
|
|
1671
|
+
if (opts.project) filtered = filtered.filter((t) => t.project === opts.project);
|
|
1672
|
+
if (!opts.all && !opts.status) {
|
|
1673
|
+
filtered = filtered.filter((t) => t.status !== "done" && t.status !== "archived");
|
|
1674
|
+
}
|
|
1675
|
+
filtered.sort((a, b) => (b.last_update || b.created_at || "").localeCompare(a.last_update || a.created_at || ""));
|
|
1676
|
+
if (opts.json) {
|
|
1677
|
+
console.log(JSON.stringify(filtered, null, 2));
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
for (const err of errors) console.error(chalk8.red(err));
|
|
1681
|
+
if (filtered.length === 0) {
|
|
1682
|
+
console.log(chalk8.dim("No tasks."));
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
const projectWidth = Math.min(20, Math.max(...filtered.map((t) => t.project.length), 7));
|
|
1686
|
+
const idWidth = Math.min(40, Math.max(...filtered.map((t) => t.id.length), 2));
|
|
1687
|
+
const objectIdWidth = Math.max(...filtered.map((t) => (t.object_id || "").length), 9);
|
|
1688
|
+
for (const t of filtered) {
|
|
1689
|
+
const status = statusColor(t.status)(t.status.padEnd(8));
|
|
1690
|
+
const project2 = chalk8.dim(t.project.padEnd(projectWidth));
|
|
1691
|
+
const objectId = chalk8.cyan((t.object_id || "-").padEnd(objectIdWidth));
|
|
1692
|
+
const id = chalk8.dim(t.id.padEnd(idWidth));
|
|
1693
|
+
const displayTags = t.display_tags ?? t.tags ?? [];
|
|
1694
|
+
const tags = displayTags.length > 0 ? chalk8.dim(` [${displayTags.join(",")}]`) : "";
|
|
1695
|
+
const displayTitle = t.display_title || t.title;
|
|
1696
|
+
console.log(`${status} ${objectId} ${project2} ${id} ${displayTitle}${tags}`);
|
|
1697
|
+
}
|
|
1698
|
+
console.log(chalk8.dim(`
|
|
1699
|
+
${filtered.length} tasks${!opts.all && !opts.status ? " (done/archived hidden, --all to show)" : ""}`));
|
|
1700
|
+
} catch (err) {
|
|
1701
|
+
console.error(chalk8.red(err.message));
|
|
1702
|
+
process.exit(1);
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1705
|
+
function statusColor(status) {
|
|
1706
|
+
if (status === "blocked") return chalk8.yellow;
|
|
1707
|
+
if (status === "done") return chalk8.green;
|
|
1708
|
+
if (status === "archived") return chalk8.dim;
|
|
1709
|
+
if (isActiveTaskStatus(status)) return chalk8.cyan;
|
|
1710
|
+
return chalk8.white;
|
|
1711
|
+
}
|
|
1712
|
+
task.command("done <id>").description("Write task lifecycle state to task0.yml \u2014 defaults to `status: done`; pass `--phase <phase>` to only update workflow.phase").option("--phase <phase>", "Set workflow.phase to this value instead of marking the task done").action(async (id, opts) => {
|
|
1713
|
+
try {
|
|
1714
|
+
const { resolveTaskByObjectId: resolveTaskByObjectId2 } = await Promise.resolve().then(() => (init_task_state2(), task_state_exports));
|
|
1715
|
+
const loc = resolveTaskByObjectId2(id);
|
|
1716
|
+
const raw = yaml5.load(fs14.readFileSync(loc.taskYml, "utf-8"));
|
|
1717
|
+
if (opts.phase) {
|
|
1718
|
+
const phase = opts.phase.trim();
|
|
1719
|
+
if (!phase) {
|
|
1720
|
+
console.error(chalk8.red("--phase value cannot be empty"));
|
|
1721
|
+
process.exit(1);
|
|
1722
|
+
}
|
|
1723
|
+
const workflow2 = raw.workflow ?? {};
|
|
1724
|
+
if (workflow2.phase === phase) {
|
|
1725
|
+
console.log(chalk8.dim(`${id} already at phase ${phase}`));
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
raw.workflow = { ...workflow2, phase };
|
|
1729
|
+
fs14.writeFileSync(loc.taskYml, yaml5.dump(raw, { lineWidth: 120 }), "utf-8");
|
|
1730
|
+
console.log(chalk8.green(`${id} phase: ${phase}`));
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
if (raw.status === "done") {
|
|
1734
|
+
console.log(chalk8.dim(`${id} already done`));
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
raw.status = "done";
|
|
1738
|
+
const workflow = raw.workflow ?? {};
|
|
1739
|
+
raw.workflow = { ...workflow, phase: "completed" };
|
|
1740
|
+
fs14.writeFileSync(loc.taskYml, yaml5.dump(raw, { lineWidth: 120 }), "utf-8");
|
|
1741
|
+
console.log(chalk8.green(`${id} marked as done`));
|
|
1742
|
+
} catch (err) {
|
|
1743
|
+
console.error(chalk8.red(err.message));
|
|
1744
|
+
process.exit(1);
|
|
1745
|
+
}
|
|
1746
|
+
});
|
|
1747
|
+
task.command("archive <id>").description("Archive a task (append to tasks.tar, remove from tasks/)").action((id) => {
|
|
1748
|
+
const cwd = process.cwd();
|
|
1749
|
+
const projectYml = path12.join(cwd, "task0.yml");
|
|
1750
|
+
if (!fs14.existsSync(projectYml)) {
|
|
1751
|
+
console.error(chalk8.red("Not a task0 project (task0.yml not found)."));
|
|
1752
|
+
process.exit(1);
|
|
1753
|
+
}
|
|
1754
|
+
const projectConfig = yaml5.load(fs14.readFileSync(projectYml, "utf-8"));
|
|
1755
|
+
if (projectConfig.kind !== "project") {
|
|
1756
|
+
console.error(chalk8.red('Invalid task0.yml: kind is not "project"'));
|
|
1757
|
+
process.exit(1);
|
|
1758
|
+
}
|
|
1759
|
+
const tasksDir = path12.join(cwd, projectConfig.tasks_dir);
|
|
1760
|
+
const taskDir = path12.join(tasksDir, id);
|
|
1761
|
+
if (!fs14.existsSync(taskDir)) {
|
|
1762
|
+
console.error(chalk8.red(`Task "${id}" not found at ${taskDir}`));
|
|
1763
|
+
process.exit(1);
|
|
1764
|
+
}
|
|
1765
|
+
const tarFile = tasksDir + ".tar";
|
|
1766
|
+
archiveTaskToTar(tasksDir, id, tarFile);
|
|
1767
|
+
fs14.rmSync(taskDir, { recursive: true });
|
|
1768
|
+
console.log(chalk8.green(`Archived "${id}" \u2192 ${tarFile}`));
|
|
1769
|
+
});
|
|
1770
|
+
function archiveTaskToTar(tasksDir, taskId, tarFile) {
|
|
1771
|
+
if (fs14.existsSync(tarFile) && !fs14.statSync(tarFile).isFile()) {
|
|
1772
|
+
console.error(chalk8.red(`${tarFile} exists but is not a file (likely a leftover directory). Remove it manually first.`));
|
|
1773
|
+
process.exit(1);
|
|
1774
|
+
}
|
|
1775
|
+
if (fs14.existsSync(tarFile)) {
|
|
1776
|
+
execSync(`tar -rf ${JSON.stringify(tarFile)} -C ${JSON.stringify(tasksDir)} ${JSON.stringify(taskId)}`);
|
|
1777
|
+
} else {
|
|
1778
|
+
execSync(`tar -cf ${JSON.stringify(tarFile)} -C ${JSON.stringify(tasksDir)} ${JSON.stringify(taskId)}`);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
// src/commands/ui.ts
|
|
1783
|
+
import { Command as Command9 } from "commander";
|
|
1784
|
+
import { spawn } from "child_process";
|
|
1785
|
+
import path13 from "path";
|
|
1786
|
+
import chalk9 from "chalk";
|
|
1787
|
+
var DASHBOARD_DIR = path13.resolve(
|
|
1788
|
+
import.meta.dirname,
|
|
1789
|
+
"..",
|
|
1790
|
+
"..",
|
|
1791
|
+
"..",
|
|
1792
|
+
"dashboard"
|
|
1793
|
+
);
|
|
1794
|
+
var ui = new Command9("ui").description("Launch the dashboard").option("-p, --port <port>", "Port number", "5173").option("-H, --host <address>", "Bind address (use 0.0.0.0 for LAN access)", "localhost").option("--no-open", "Do not open browser").action((opts) => {
|
|
1795
|
+
console.log(chalk9.green("Starting dashboard..."));
|
|
1796
|
+
const args = ["dev", "--port", opts.port, "--host", opts.host];
|
|
1797
|
+
if (opts.open) args.push("--open");
|
|
1798
|
+
const child = spawn("pnpm", args, {
|
|
1799
|
+
cwd: DASHBOARD_DIR,
|
|
1800
|
+
stdio: "inherit",
|
|
1801
|
+
shell: true
|
|
1802
|
+
});
|
|
1803
|
+
child.on("error", (err) => {
|
|
1804
|
+
console.error(chalk9.red(`Failed to start dashboard: ${err.message}`));
|
|
1805
|
+
process.exit(1);
|
|
1806
|
+
});
|
|
1807
|
+
child.on("exit", (code) => {
|
|
1808
|
+
process.exit(code ?? 0);
|
|
1809
|
+
});
|
|
1810
|
+
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
1811
|
+
process.on("SIGTERM", () => child.kill("SIGTERM"));
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
// src/commands/serve.ts
|
|
1815
|
+
import { Command as Command10 } from "commander";
|
|
1816
|
+
import { spawn as spawn2 } from "child_process";
|
|
1817
|
+
import fs15 from "fs";
|
|
1818
|
+
import path14 from "path";
|
|
1819
|
+
import chalk10 from "chalk";
|
|
1820
|
+
var SERVER_DIR = path14.resolve(
|
|
1821
|
+
import.meta.dirname,
|
|
1822
|
+
"..",
|
|
1823
|
+
"..",
|
|
1824
|
+
"..",
|
|
1825
|
+
"server"
|
|
1826
|
+
);
|
|
1827
|
+
var HTTP_ENTRY = path14.join(SERVER_DIR, "src", "main.ts");
|
|
1828
|
+
var CLI_TSX = path14.resolve(
|
|
1829
|
+
import.meta.dirname,
|
|
1830
|
+
"..",
|
|
1831
|
+
"..",
|
|
1832
|
+
"node_modules",
|
|
1833
|
+
".bin",
|
|
1834
|
+
"tsx"
|
|
1835
|
+
);
|
|
1836
|
+
var serve = new Command10("serve").description("Run the headless task0 API server").option("-p, --port <port>", "Port number", "4318").option("-H, --host <address>", "Bind address", "127.0.0.1").action((opts) => {
|
|
1837
|
+
if (!fs15.existsSync(HTTP_ENTRY)) {
|
|
1838
|
+
console.error(chalk10.red(`Server entry not found: ${HTTP_ENTRY}`));
|
|
1839
|
+
process.exit(1);
|
|
1840
|
+
}
|
|
1841
|
+
if (!fs15.existsSync(CLI_TSX)) {
|
|
1842
|
+
console.error(chalk10.red(`tsx not found at ${CLI_TSX}. Run \`pnpm install\` in the CLI package.`));
|
|
1843
|
+
process.exit(1);
|
|
1844
|
+
}
|
|
1845
|
+
console.log(chalk10.green(`Starting task0 API on http://${opts.host}:${opts.port}`));
|
|
1846
|
+
const child = spawn2(CLI_TSX, [HTTP_ENTRY], {
|
|
1847
|
+
cwd: SERVER_DIR,
|
|
1848
|
+
stdio: "inherit",
|
|
1849
|
+
env: {
|
|
1850
|
+
...process.env,
|
|
1851
|
+
TASK0_API_PORT: opts.port,
|
|
1852
|
+
TASK0_API_HOST: opts.host
|
|
1853
|
+
}
|
|
1854
|
+
});
|
|
1855
|
+
child.on("error", (err) => {
|
|
1856
|
+
console.error(chalk10.red(`Failed to start server: ${err.message}`));
|
|
1857
|
+
process.exit(1);
|
|
1858
|
+
});
|
|
1859
|
+
child.on("exit", (code) => {
|
|
1860
|
+
process.exit(code ?? 0);
|
|
1861
|
+
});
|
|
1862
|
+
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
1863
|
+
process.on("SIGTERM", () => child.kill("SIGTERM"));
|
|
1864
|
+
});
|
|
1865
|
+
|
|
1866
|
+
// src/commands/models.ts
|
|
1867
|
+
import { Command as Command11 } from "commander";
|
|
1868
|
+
import chalk11 from "chalk";
|
|
1869
|
+
|
|
1870
|
+
// ../../packages/shared/dist/types/agent.js
|
|
1871
|
+
var AGENT_KINDS = ["coding", "llm_api", "workflow"];
|
|
1872
|
+
var AGENT_KIND_SET = new Set(AGENT_KINDS);
|
|
1873
|
+
var CODING_RUNTIME_AGENTS = ["claude-code", "codex", "cursor"];
|
|
1874
|
+
var CODING_RUNTIME_AGENT_SET = new Set(CODING_RUNTIME_AGENTS);
|
|
1875
|
+
function isCodingRuntimeAgent(value) {
|
|
1876
|
+
return typeof value === "string" && CODING_RUNTIME_AGENT_SET.has(value);
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// ../../packages/shared/dist/types/runtime.js
|
|
1880
|
+
var RUNTIME_STATUS_VALUES = ["starting", "running", "done", "error"];
|
|
1881
|
+
var RUNTIME_STATUS_SET = new Set(RUNTIME_STATUS_VALUES);
|
|
1882
|
+
var RUNTIME_AGENTS = CODING_RUNTIME_AGENTS;
|
|
1883
|
+
var isRuntimeAgent = isCodingRuntimeAgent;
|
|
1884
|
+
var AGENT_MODEL_DEFAULTS = {
|
|
1885
|
+
"claude-code": [
|
|
1886
|
+
{ id: "opus", label: "Opus" },
|
|
1887
|
+
{ id: "sonnet", label: "Sonnet" },
|
|
1888
|
+
{ id: "haiku", label: "Haiku" }
|
|
1889
|
+
],
|
|
1890
|
+
codex: [
|
|
1891
|
+
{ id: "gpt-5.4", label: "GPT-5.4" },
|
|
1892
|
+
{ id: "o3", label: "o3" },
|
|
1893
|
+
{ id: "o4-mini", label: "o4-mini" }
|
|
1894
|
+
],
|
|
1895
|
+
cursor: [
|
|
1896
|
+
{ id: "", label: "Default" },
|
|
1897
|
+
{ id: "gpt-5", label: "GPT-5" },
|
|
1898
|
+
{ id: "sonnet-4", label: "Sonnet 4" },
|
|
1899
|
+
{ id: "sonnet-4-thinking", label: "Sonnet 4 Thinking" }
|
|
1900
|
+
]
|
|
1901
|
+
};
|
|
1902
|
+
function defaultAgentModelFetchCommand(agent2) {
|
|
1903
|
+
if (agent2 === "cursor")
|
|
1904
|
+
return "cursor-agent models";
|
|
1905
|
+
return void 0;
|
|
1906
|
+
}
|
|
1907
|
+
function defaultAgentModelOutputFormat(agent2) {
|
|
1908
|
+
if (agent2 === "cursor")
|
|
1909
|
+
return "lines";
|
|
1910
|
+
return void 0;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// src/core/agent-models.ts
|
|
1914
|
+
import { execSync as execSync2 } from "child_process";
|
|
1915
|
+
var ANSI_ESCAPE_RE = new RegExp(String.raw`\u001B\[[0-9;]*[A-Za-z]`, "g");
|
|
1916
|
+
var MODEL_ID_RE = /^[A-Za-z0-9._:+/-]+$/;
|
|
1917
|
+
function builtinModelsFor(agent2) {
|
|
1918
|
+
return AGENT_MODEL_DEFAULTS[agent2].map((model) => ({ ...model }));
|
|
1919
|
+
}
|
|
1920
|
+
function stripAnsi(value) {
|
|
1921
|
+
return value.replace(ANSI_ESCAPE_RE, "");
|
|
1922
|
+
}
|
|
1923
|
+
function ensureAgentModels(config) {
|
|
1924
|
+
if (!config.agentModels) config.agentModels = {};
|
|
1925
|
+
for (const agent2 of RUNTIME_AGENTS) {
|
|
1926
|
+
const current = config.agentModels[agent2];
|
|
1927
|
+
if (current) continue;
|
|
1928
|
+
const fetchCommand = defaultAgentModelFetchCommand(agent2);
|
|
1929
|
+
const outputFormat = defaultAgentModelOutputFormat(agent2);
|
|
1930
|
+
config.agentModels[agent2] = {
|
|
1931
|
+
models: builtinModelsFor(agent2),
|
|
1932
|
+
...fetchCommand ? { fetchCommand } : {},
|
|
1933
|
+
...outputFormat ? { outputFormat } : {}
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
return config.agentModels;
|
|
1937
|
+
}
|
|
1938
|
+
function cacheEntryForAgent(config, agent2) {
|
|
1939
|
+
return ensureAgentModels(config)[agent2] ?? { models: builtinModelsFor(agent2) };
|
|
1940
|
+
}
|
|
1941
|
+
function normalizeLineModel(line) {
|
|
1942
|
+
const cleaned = stripAnsi(line).trim().replace(/^[*-]\s+/, "");
|
|
1943
|
+
if (!cleaned) return null;
|
|
1944
|
+
if (/^loading\b/i.test(cleaned)) return null;
|
|
1945
|
+
if (/^available models\b/i.test(cleaned)) return null;
|
|
1946
|
+
if (/^(tip|note|usage)\b[:\s-]/i.test(cleaned)) return null;
|
|
1947
|
+
const idLabelMatch = cleaned.match(/^([A-Za-z0-9._:-]+)\s+-\s+(.+)$/);
|
|
1948
|
+
if (idLabelMatch) return { id: idLabelMatch[1], label: idLabelMatch[2].trim() };
|
|
1949
|
+
const labelIdMatch = cleaned.match(/^(.+?)\s+\(([A-Za-z0-9._:-]+)\)$/);
|
|
1950
|
+
if (labelIdMatch) return { id: labelIdMatch[2], label: labelIdMatch[1].trim() };
|
|
1951
|
+
if (!MODEL_ID_RE.test(cleaned)) return null;
|
|
1952
|
+
return { id: cleaned, label: cleaned };
|
|
1953
|
+
}
|
|
1954
|
+
function parseJsonModels(raw) {
|
|
1955
|
+
const parsed = JSON.parse(raw);
|
|
1956
|
+
const source2 = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && Array.isArray(parsed.models) ? parsed.models : null;
|
|
1957
|
+
if (!source2) return [];
|
|
1958
|
+
return source2.flatMap((item) => {
|
|
1959
|
+
if (typeof item === "string" && item.trim()) {
|
|
1960
|
+
return [{ id: item.trim(), label: item.trim() }];
|
|
1961
|
+
}
|
|
1962
|
+
if (!item || typeof item !== "object") return [];
|
|
1963
|
+
const record = item;
|
|
1964
|
+
const id = typeof record.id === "string" && record.id.trim() ? record.id.trim() : typeof record.value === "string" && record.value.trim() ? record.value.trim() : "";
|
|
1965
|
+
if (!id) return [];
|
|
1966
|
+
const label = typeof record.label === "string" && record.label.trim() ? record.label.trim() : typeof record.name === "string" && record.name.trim() ? record.name.trim() : id;
|
|
1967
|
+
return [{
|
|
1968
|
+
id,
|
|
1969
|
+
label,
|
|
1970
|
+
...typeof record.alias === "string" && record.alias.trim() ? { alias: record.alias.trim() } : {}
|
|
1971
|
+
}];
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
function dedupeModels(models2) {
|
|
1975
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
1976
|
+
for (const model of models2) {
|
|
1977
|
+
const id = model.id.trim();
|
|
1978
|
+
if (!id && model.label !== "Default") continue;
|
|
1979
|
+
deduped.set(id, {
|
|
1980
|
+
id,
|
|
1981
|
+
...model.label ? { label: model.label.trim() } : {},
|
|
1982
|
+
...model.alias ? { alias: model.alias.trim() } : {}
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
return [...deduped.values()];
|
|
1986
|
+
}
|
|
1987
|
+
function parseCommandOutput(output, format) {
|
|
1988
|
+
const normalized = output.trim();
|
|
1989
|
+
if (!normalized) return [];
|
|
1990
|
+
if (format === "json") {
|
|
1991
|
+
return dedupeModels(parseJsonModels(normalized));
|
|
1992
|
+
}
|
|
1993
|
+
return dedupeModels(
|
|
1994
|
+
normalized.split(/\r?\n/).flatMap((line) => {
|
|
1995
|
+
const parsed = normalizeLineModel(line);
|
|
1996
|
+
return parsed ? [parsed] : [];
|
|
1997
|
+
})
|
|
1998
|
+
);
|
|
1999
|
+
}
|
|
2000
|
+
function refreshAgentModelsInConfig(config, agent2) {
|
|
2001
|
+
const entry = cacheEntryForAgent(config, agent2);
|
|
2002
|
+
const aliases = new Map((entry.models ?? []).map((model) => [model.id, model.alias]));
|
|
2003
|
+
let models2 = builtinModelsFor(agent2);
|
|
2004
|
+
let source2 = "builtin";
|
|
2005
|
+
if (entry.fetchCommand?.trim()) {
|
|
2006
|
+
try {
|
|
2007
|
+
const output = execSync2(entry.fetchCommand, {
|
|
2008
|
+
encoding: "utf-8",
|
|
2009
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2010
|
+
});
|
|
2011
|
+
const parsed = parseCommandOutput(output, entry.outputFormat ?? "lines");
|
|
2012
|
+
if (parsed.length === 0) {
|
|
2013
|
+
throw new Error("no models detected in command output");
|
|
2014
|
+
}
|
|
2015
|
+
models2 = parsed;
|
|
2016
|
+
source2 = "command";
|
|
2017
|
+
} catch (error2) {
|
|
2018
|
+
const stdout = error2 && typeof error2 === "object" && "stdout" in error2 && typeof error2.stdout === "string" ? error2.stdout : "";
|
|
2019
|
+
const stderr = error2 && typeof error2 === "object" && "stderr" in error2 && typeof error2.stderr === "string" ? error2.stderr : "";
|
|
2020
|
+
const details = stripAnsi(`${stdout}
|
|
2021
|
+
${stderr}`).trim();
|
|
2022
|
+
throw new Error(details || `failed to refresh ${agent2} models`);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
const mergedModels = models2.map((model) => ({
|
|
2026
|
+
...model,
|
|
2027
|
+
...aliases.get(model.id)?.trim() ? { alias: aliases.get(model.id).trim() } : {}
|
|
2028
|
+
}));
|
|
2029
|
+
ensureAgentModels(config)[agent2] = {
|
|
2030
|
+
...entry,
|
|
2031
|
+
models: mergedModels,
|
|
2032
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2033
|
+
};
|
|
2034
|
+
return {
|
|
2035
|
+
agent: agent2,
|
|
2036
|
+
source: source2,
|
|
2037
|
+
modelCount: mergedModels.length,
|
|
2038
|
+
...entry.fetchCommand?.trim() ? { fetchCommand: entry.fetchCommand.trim() } : {}
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
function refreshAgentModels(agent2) {
|
|
2042
|
+
const config = loadConfig();
|
|
2043
|
+
const targets = agent2 ? [agent2] : RUNTIME_AGENTS;
|
|
2044
|
+
const results = [];
|
|
2045
|
+
const failures = [];
|
|
2046
|
+
for (const target of targets) {
|
|
2047
|
+
try {
|
|
2048
|
+
results.push(refreshAgentModelsInConfig(config, target));
|
|
2049
|
+
} catch (error2) {
|
|
2050
|
+
failures.push({
|
|
2051
|
+
agent: target,
|
|
2052
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
if (results.length > 0) {
|
|
2057
|
+
saveConfig(config);
|
|
2058
|
+
}
|
|
2059
|
+
return { results, failures };
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
// src/commands/models.ts
|
|
2063
|
+
function parseAgent(raw) {
|
|
2064
|
+
const trimmed = raw.trim();
|
|
2065
|
+
if (!isRuntimeAgent(trimmed)) {
|
|
2066
|
+
console.error(chalk11.red(`Invalid agent "${raw}" (expected one of ${RUNTIME_AGENTS.join(", ")})`));
|
|
2067
|
+
process.exit(1);
|
|
2068
|
+
}
|
|
2069
|
+
return trimmed;
|
|
2070
|
+
}
|
|
2071
|
+
var models = new Command11("models").description("Manage cached agent model lists");
|
|
2072
|
+
models.command("refresh").description("Refresh cached agent model lists into local config").option("-a, --agent <agent>", "Refresh a single agent only (claude-code, codex, cursor)").action((opts) => {
|
|
2073
|
+
const agent2 = opts.agent ? parseAgent(opts.agent) : void 0;
|
|
2074
|
+
try {
|
|
2075
|
+
const summary = refreshAgentModels(agent2);
|
|
2076
|
+
for (const result of summary.results) {
|
|
2077
|
+
const source2 = result.source === "command" ? chalk11.cyan("command") : chalk11.dim("builtin");
|
|
2078
|
+
console.log(chalk11.green(`Refreshed ${result.agent}`) + ` ${source2} ${result.modelCount} models`);
|
|
2079
|
+
if (result.fetchCommand) {
|
|
2080
|
+
console.log(chalk11.dim(` ${result.fetchCommand}`));
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
for (const failure of summary.failures) {
|
|
2084
|
+
console.error(chalk11.red(`Failed ${failure.agent}: ${failure.error}`));
|
|
2085
|
+
}
|
|
2086
|
+
if (summary.failures.length > 0) {
|
|
2087
|
+
process.exit(1);
|
|
2088
|
+
}
|
|
2089
|
+
} catch (error2) {
|
|
2090
|
+
console.error(chalk11.red(error2 instanceof Error ? error2.message : String(error2)));
|
|
2091
|
+
process.exit(1);
|
|
2092
|
+
}
|
|
2093
|
+
});
|
|
2094
|
+
models.command("default <agent>").description("Get or set default model / effort for an agent").option("--model <id>", 'Set default model id (pass "" to clear)').option("--effort <level>", 'Set default effort (pass "" to clear)').action((rawAgent, opts) => {
|
|
2095
|
+
const agent2 = parseAgent(rawAgent);
|
|
2096
|
+
const config = loadConfig();
|
|
2097
|
+
const agentModels = config.agentModels ?? (config.agentModels = {});
|
|
2098
|
+
const entry = agentModels[agent2] ?? (agentModels[agent2] = {});
|
|
2099
|
+
const wroteModel = opts.model !== void 0;
|
|
2100
|
+
const wroteEffort = opts.effort !== void 0;
|
|
2101
|
+
if (wroteModel) {
|
|
2102
|
+
const trimmed = opts.model.trim();
|
|
2103
|
+
if (trimmed) entry.defaultModel = trimmed;
|
|
2104
|
+
else delete entry.defaultModel;
|
|
2105
|
+
}
|
|
2106
|
+
if (wroteEffort) {
|
|
2107
|
+
const trimmed = opts.effort.trim();
|
|
2108
|
+
if (trimmed) entry.defaultEffort = trimmed;
|
|
2109
|
+
else delete entry.defaultEffort;
|
|
2110
|
+
}
|
|
2111
|
+
if (wroteModel || wroteEffort) {
|
|
2112
|
+
saveConfig(config);
|
|
2113
|
+
}
|
|
2114
|
+
console.log(chalk11.green(agent2));
|
|
2115
|
+
console.log(` model: ${entry.defaultModel ?? chalk11.dim("(unset)")}`);
|
|
2116
|
+
console.log(` effort: ${entry.defaultEffort ?? chalk11.dim("(unset)")}`);
|
|
2117
|
+
});
|
|
2118
|
+
|
|
2119
|
+
// src/commands/runtime.ts
|
|
2120
|
+
import { Command as Command12 } from "commander";
|
|
2121
|
+
import chalk12 from "chalk";
|
|
2122
|
+
import { spawn as spawn3 } from "child_process";
|
|
2123
|
+
var runtime = new Command12("runtime").description("Inspect and control runtimes");
|
|
2124
|
+
runtime.command("list").description("List active runtimes").option("--json", "Output JSON").option("--task <id>", "Filter by task id").action(async (opts) => {
|
|
2125
|
+
try {
|
|
2126
|
+
const { runtimes } = await api.get("/api/runtimes");
|
|
2127
|
+
const filtered = opts.task ? runtimes.filter((r) => r.taskId === opts.task) : runtimes;
|
|
2128
|
+
if (opts.json) {
|
|
2129
|
+
console.log(JSON.stringify(filtered, null, 2));
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2132
|
+
if (filtered.length === 0) {
|
|
2133
|
+
console.log(chalk12.dim("No runtimes."));
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
const objectIdWidth = Math.max(...filtered.map((r) => (r.objectId || "").length), 8);
|
|
2137
|
+
for (const r of filtered) {
|
|
2138
|
+
const statusColor2 = r.status === "done" ? chalk12.green : r.status === "error" ? chalk12.red : r.status === "running" ? chalk12.cyan : chalk12.dim;
|
|
2139
|
+
const objectId = chalk12.cyan((r.objectId || "-").padEnd(objectIdWidth));
|
|
2140
|
+
console.log(`${statusColor2(r.status.padEnd(8))} ${objectId} ${chalk12.dim(r.type.padEnd(12))} ${r.taskId.padEnd(30)} ${chalk12.dim(r.id)}`);
|
|
2141
|
+
}
|
|
2142
|
+
} catch (err) {
|
|
2143
|
+
console.error(chalk12.red(err.message));
|
|
2144
|
+
process.exit(1);
|
|
2145
|
+
}
|
|
2146
|
+
});
|
|
2147
|
+
runtime.command("show <id>").description("Show runtime details").option("--json", "Output JSON").action(async (id, opts) => {
|
|
2148
|
+
try {
|
|
2149
|
+
const r = await getRuntime(id);
|
|
2150
|
+
if (opts.json) {
|
|
2151
|
+
console.log(JSON.stringify(r, null, 2));
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
console.log(`${chalk12.bold("id:")} ${r.id}`);
|
|
2155
|
+
if (r.objectId) console.log(`${chalk12.bold("object_id:")} ${r.objectId}`);
|
|
2156
|
+
console.log(`${chalk12.bold("type:")} ${r.type}`);
|
|
2157
|
+
console.log(`${chalk12.bold("agent:")} ${r.agent}`);
|
|
2158
|
+
console.log(`${chalk12.bold("task:")} ${r.taskId}`);
|
|
2159
|
+
console.log(`${chalk12.bold("status:")} ${r.status}`);
|
|
2160
|
+
if (r.phase) console.log(`${chalk12.bold("phase:")} ${r.phase}`);
|
|
2161
|
+
if (r.error) console.log(`${chalk12.red("error:")} ${r.error}`);
|
|
2162
|
+
console.log(`${chalk12.bold("tmux:")} ${r.tmuxSession} (alive: ${r.sessionAlive ?? "unknown"})`);
|
|
2163
|
+
if (r.result) {
|
|
2164
|
+
console.log(chalk12.bold("result:"));
|
|
2165
|
+
console.log(" " + JSON.stringify(r.result, null, 2).split("\n").join("\n "));
|
|
2166
|
+
}
|
|
2167
|
+
} catch (err) {
|
|
2168
|
+
console.error(chalk12.red(err.message));
|
|
2169
|
+
process.exit(1);
|
|
2170
|
+
}
|
|
2171
|
+
});
|
|
2172
|
+
runtime.command("wait <id>").description("Block until runtime reaches done or error").option("--timeout <seconds>", "Max seconds to wait", "1800").option("--json", "Output final JSON").action(async (id, opts) => {
|
|
2173
|
+
try {
|
|
2174
|
+
const final = await waitForRuntime(id, {
|
|
2175
|
+
timeoutMs: Number(opts.timeout) * 1e3,
|
|
2176
|
+
onTick: (s) => {
|
|
2177
|
+
if (!opts.json) console.log(chalk12.dim(`[${s.status}] ${s.phase ?? ""}`));
|
|
2178
|
+
}
|
|
2179
|
+
});
|
|
2180
|
+
if (opts.json) {
|
|
2181
|
+
console.log(JSON.stringify(final, null, 2));
|
|
2182
|
+
}
|
|
2183
|
+
process.exit(final.status === "done" ? 0 : 1);
|
|
2184
|
+
} catch (err) {
|
|
2185
|
+
console.error(chalk12.red(err.message));
|
|
2186
|
+
process.exit(1);
|
|
2187
|
+
}
|
|
2188
|
+
});
|
|
2189
|
+
runtime.command("cancel <id>").description("Cancel a runtime").action(async (id) => {
|
|
2190
|
+
try {
|
|
2191
|
+
await api.post(`/api/runtimes/${encodeURIComponent(id)}/cancel`);
|
|
2192
|
+
console.log(chalk12.green(`Canceled ${id}`));
|
|
2193
|
+
} catch (err) {
|
|
2194
|
+
console.error(chalk12.red(err.message));
|
|
2195
|
+
process.exit(1);
|
|
2196
|
+
}
|
|
2197
|
+
});
|
|
2198
|
+
runtime.command("attach <id>").description("Attach to the tmux session backing a runtime").action(async (id) => {
|
|
2199
|
+
try {
|
|
2200
|
+
const r = await getRuntime(id);
|
|
2201
|
+
if (!r.tmuxSession) {
|
|
2202
|
+
console.error(chalk12.red("No tmux session recorded for this runtime."));
|
|
2203
|
+
process.exit(1);
|
|
2204
|
+
}
|
|
2205
|
+
const child = spawn3("tmux", ["attach", "-t", r.tmuxSession], { stdio: "inherit" });
|
|
2206
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
2207
|
+
child.on("error", (err) => {
|
|
2208
|
+
console.error(chalk12.red(`Failed to attach: ${err.message}`));
|
|
2209
|
+
process.exit(1);
|
|
2210
|
+
});
|
|
2211
|
+
} catch (err) {
|
|
2212
|
+
console.error(chalk12.red(err.message));
|
|
2213
|
+
process.exit(1);
|
|
2214
|
+
}
|
|
2215
|
+
});
|
|
2216
|
+
|
|
2217
|
+
// src/commands/plan.ts
|
|
2218
|
+
import { Command as Command13 } from "commander";
|
|
2219
|
+
import chalk13 from "chalk";
|
|
2220
|
+
import fs16 from "fs";
|
|
2221
|
+
import path15 from "path";
|
|
2222
|
+
init_task_state2();
|
|
2223
|
+
var PLAN_GENERATE_SKILL_NAME = "plan-generate";
|
|
2224
|
+
function resolveSkillFilePath3(projectRoot, skillName) {
|
|
2225
|
+
const p = path15.join(projectRoot, ".claude", "skills", skillName, "SKILL.md");
|
|
2226
|
+
return fs16.existsSync(p) ? p : null;
|
|
2227
|
+
}
|
|
2228
|
+
var plan = new Command13("plan").description("Generate and refine plans");
|
|
2229
|
+
plan.command("generate <objectId>").description("Generate plan(s) from an IDEA file \u2014 supports agent fan-out").option("-a, --agents <list>", "Comma-separated agents (claude-code,codex,cursor)", "codex,claude-code").option("--model <id>", "Model id or alias \u2014 only with a single agent; use `task0 models default` for fan-out").option("--effort <level>", "Reasoning effort \u2014 only with a single agent; use `task0 models default` for fan-out").option("--idea <file>", "IDEA file name (default: latest IDEA-NN.md)").option("--additional-prompt <text>", "Extra prompt content").option("--wait", "Wait for completion").option("--force", "Overwrite existing plan files").option("--json", "Output JSON").action(async (objectId, opts) => {
|
|
2230
|
+
try {
|
|
2231
|
+
const loc = resolveTaskByObjectId(objectId);
|
|
2232
|
+
const ideaFile = opts.idea || latestArtifact(loc.taskDir, /^IDEA-\d+\.md$/);
|
|
2233
|
+
if (!ideaFile) {
|
|
2234
|
+
console.error(chalk13.red(`No IDEA-NN.md found in ${loc.taskDir}. Pass --idea.`));
|
|
2235
|
+
process.exit(1);
|
|
2236
|
+
}
|
|
2237
|
+
const agents = opts.agents.split(",").map((a) => a.trim()).filter(Boolean);
|
|
2238
|
+
if (agents.length === 0) {
|
|
2239
|
+
console.error(chalk13.red("No agents specified."));
|
|
2240
|
+
process.exit(1);
|
|
2241
|
+
}
|
|
2242
|
+
if ((opts.model || opts.effort) && agents.length > 1) {
|
|
2243
|
+
console.error(chalk13.red(
|
|
2244
|
+
`--model/--effort only works with a single agent; got ${agents.length} (${agents.join(", ")}). Use \`task0 models default <agent>\` to set per-agent defaults for fan-out.`
|
|
2245
|
+
));
|
|
2246
|
+
process.exit(1);
|
|
2247
|
+
}
|
|
2248
|
+
const warn = opts.json ? void 0 : (m) => console.error(chalk13.dim(m));
|
|
2249
|
+
const skillFilePath = resolveSkillFilePath3(loc.projectRoot, PLAN_GENERATE_SKILL_NAME);
|
|
2250
|
+
if (opts.model || opts.effort) {
|
|
2251
|
+
resolveModelOptions(agents[0], { model: opts.model, effort: opts.effort }, warn);
|
|
2252
|
+
}
|
|
2253
|
+
const promptForAgent = (agent2) => {
|
|
2254
|
+
const lines = [
|
|
2255
|
+
`task_id: ${objectId}`,
|
|
2256
|
+
`Skill: ${PLAN_GENERATE_SKILL_NAME}`,
|
|
2257
|
+
`Idea file: ${ideaFile}`,
|
|
2258
|
+
`Target plan file: PLAN-${ideaFile.match(/^IDEA-(\d+)\.md$/)?.[1] ?? "01"}-${agent2}.md`,
|
|
2259
|
+
opts.force ? "Overwrite existing plan file." : "",
|
|
2260
|
+
opts.additionalPrompt ? "" : "",
|
|
2261
|
+
opts.additionalPrompt || ""
|
|
2262
|
+
].filter((line) => line !== "");
|
|
2263
|
+
return lines.join("\n");
|
|
2264
|
+
};
|
|
2265
|
+
const kicks = await Promise.all(agents.map(async (agent2) => {
|
|
2266
|
+
const body = {
|
|
2267
|
+
task_id: objectId,
|
|
2268
|
+
prompt: promptForAgent(agent2),
|
|
2269
|
+
...skillFilePath ? { skill_file_path: skillFilePath } : {}
|
|
2270
|
+
};
|
|
2271
|
+
try {
|
|
2272
|
+
const resp = await api.post(
|
|
2273
|
+
`/api/agents/${encodeURIComponent(agent2)}/run`,
|
|
2274
|
+
body
|
|
2275
|
+
);
|
|
2276
|
+
return {
|
|
2277
|
+
agent: agent2,
|
|
2278
|
+
runtimeId: resp.runtime.id,
|
|
2279
|
+
sessionName: "",
|
|
2280
|
+
error: null
|
|
2281
|
+
};
|
|
2282
|
+
} catch (err) {
|
|
2283
|
+
return { agent: agent2, runtimeId: "", sessionName: "", error: err.message };
|
|
2284
|
+
}
|
|
2285
|
+
}));
|
|
2286
|
+
const launched = kicks.filter((k) => !k.error);
|
|
2287
|
+
const failed = kicks.filter((k) => k.error);
|
|
2288
|
+
if (!opts.json) {
|
|
2289
|
+
for (const k of launched) {
|
|
2290
|
+
console.log(chalk13.green(`[${k.agent}]`) + ` runtime ${k.runtimeId}`);
|
|
2291
|
+
}
|
|
2292
|
+
for (const k of failed) {
|
|
2293
|
+
console.error(chalk13.red(`[${k.agent}] ${k.error}`));
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
const runtimeIds = {};
|
|
2297
|
+
for (const k of launched) runtimeIds[k.agent] = k.runtimeId;
|
|
2298
|
+
await updateWorkflow(loc.taskYml, {
|
|
2299
|
+
runtimes: { plan: runtimeIds }
|
|
2300
|
+
});
|
|
2301
|
+
const planFiles = [];
|
|
2302
|
+
if (opts.wait) {
|
|
2303
|
+
const results = await Promise.all(launched.map(async (k) => {
|
|
2304
|
+
try {
|
|
2305
|
+
const final = await waitForRuntime(k.runtimeId, {
|
|
2306
|
+
onTick: (s) => {
|
|
2307
|
+
if (!opts.json) console.log(chalk13.dim(`[${k.agent}] ${s.status}${s.phase ? " " + s.phase : ""}`));
|
|
2308
|
+
}
|
|
2309
|
+
});
|
|
2310
|
+
return { agent: k.agent, final, error: null };
|
|
2311
|
+
} catch (err) {
|
|
2312
|
+
return { agent: k.agent, final: null, error: err.message };
|
|
2313
|
+
}
|
|
2314
|
+
}));
|
|
2315
|
+
for (const r of results) {
|
|
2316
|
+
if (r.final?.status === "done") {
|
|
2317
|
+
const planFile = r.final.result?.planFile;
|
|
2318
|
+
if (planFile) {
|
|
2319
|
+
planFiles.push(planFile);
|
|
2320
|
+
if (!opts.json) console.log(chalk13.green(`[${r.agent}] ${planFile}`));
|
|
2321
|
+
}
|
|
2322
|
+
} else {
|
|
2323
|
+
if (!opts.json) console.error(chalk13.red(`[${r.agent}] failed: ${r.error || r.final?.error || "unknown"}`));
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
const existingPlanFiles = readWorkflow(loc.taskYml).plan_files || [];
|
|
2327
|
+
await updateWorkflow(loc.taskYml, {
|
|
2328
|
+
phase: "planned",
|
|
2329
|
+
plan_files: Array.from(/* @__PURE__ */ new Set([...planFiles, ...existingPlanFiles]))
|
|
2330
|
+
});
|
|
2331
|
+
}
|
|
2332
|
+
if (opts.json) {
|
|
2333
|
+
console.log(JSON.stringify({ ideaFile, launched, failed, planFiles }, null, 2));
|
|
2334
|
+
}
|
|
2335
|
+
if (failed.length > 0) process.exit(1);
|
|
2336
|
+
} catch (err) {
|
|
2337
|
+
console.error(chalk13.red(err.message));
|
|
2338
|
+
process.exit(1);
|
|
2339
|
+
}
|
|
2340
|
+
});
|
|
2341
|
+
var PLAN_REFINE_SKILL_NAME = "plan-refine";
|
|
2342
|
+
plan.command("refine <objectId>").description("Synthesize plan files + ISSUE files into a refined plan").option("--agent <name>", "Agent to run refine (claude-code|codex)", "claude-code").option("--model <id>", "Model id or alias (see `task0 models refresh`)").option("--effort <level>", "Reasoning effort (e.g. low|medium|high)").option("--wait", "Wait for completion").option("--json", "Output JSON").action(async (objectId, opts) => {
|
|
2343
|
+
try {
|
|
2344
|
+
const loc = resolveTaskByObjectId(objectId);
|
|
2345
|
+
const files = fs16.readdirSync(loc.taskDir);
|
|
2346
|
+
const planFiles = files.filter((f) => /^PLAN-\d+-(codex|claude-code|cursor)\.md$/.test(f));
|
|
2347
|
+
if (planFiles.length === 0) {
|
|
2348
|
+
console.error(chalk13.red("No PLAN-NN-<agent>.md files found. Run `task0 plan generate` first."));
|
|
2349
|
+
process.exit(1);
|
|
2350
|
+
}
|
|
2351
|
+
const idx = planFiles[0].match(/^PLAN-(\d+)/)?.[1] || nextArtifactIndex(loc.taskDir, "PLAN");
|
|
2352
|
+
const refinedFile = `PLAN-${idx}-refined.md`;
|
|
2353
|
+
if (opts.model || opts.effort) {
|
|
2354
|
+
resolveModelOptions(
|
|
2355
|
+
opts.agent,
|
|
2356
|
+
{ model: opts.model, effort: opts.effort },
|
|
2357
|
+
opts.json ? void 0 : (m) => console.error(chalk13.dim(m))
|
|
2358
|
+
);
|
|
2359
|
+
}
|
|
2360
|
+
const skillFilePath = resolveSkillFilePath3(loc.projectRoot, PLAN_REFINE_SKILL_NAME);
|
|
2361
|
+
const prompt = [
|
|
2362
|
+
`task_id: ${objectId}`,
|
|
2363
|
+
`Skill: ${PLAN_REFINE_SKILL_NAME}`,
|
|
2364
|
+
`Target file: ${refinedFile}`,
|
|
2365
|
+
`Source plans: ${planFiles.join(", ")}`
|
|
2366
|
+
].join("\n");
|
|
2367
|
+
const body = {
|
|
2368
|
+
task_id: objectId,
|
|
2369
|
+
prompt,
|
|
2370
|
+
...skillFilePath ? { skill_file_path: skillFilePath } : {}
|
|
2371
|
+
};
|
|
2372
|
+
const resp = await api.post(
|
|
2373
|
+
`/api/agents/${encodeURIComponent(opts.agent)}/run`,
|
|
2374
|
+
body
|
|
2375
|
+
);
|
|
2376
|
+
const runtimeId = resp.runtime.id;
|
|
2377
|
+
if (!opts.json) console.log(chalk13.green(`refine runtime ${runtimeId}`));
|
|
2378
|
+
await updateWorkflow(loc.taskYml, {
|
|
2379
|
+
runtimes: { refine: runtimeId }
|
|
2380
|
+
});
|
|
2381
|
+
if (opts.wait) {
|
|
2382
|
+
const final = await waitForRuntime(runtimeId, {
|
|
2383
|
+
onTick: (s) => {
|
|
2384
|
+
if (!opts.json) console.log(chalk13.dim(`[refine] ${s.status}${s.phase ? " " + s.phase : ""}`));
|
|
2385
|
+
}
|
|
2386
|
+
});
|
|
2387
|
+
const refinedPath = path15.join(loc.taskDir, refinedFile);
|
|
2388
|
+
const wrote = fs16.existsSync(refinedPath);
|
|
2389
|
+
if (final.status === "done" && wrote) {
|
|
2390
|
+
await updateWorkflow(loc.taskYml, {
|
|
2391
|
+
phase: "refined",
|
|
2392
|
+
refined_plan_file: refinedFile
|
|
2393
|
+
});
|
|
2394
|
+
if (!opts.json) console.log(chalk13.green(refinedFile));
|
|
2395
|
+
if (opts.json) console.log(JSON.stringify({ runtimeId, refinedFile }, null, 2));
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
if (final.status !== "done") {
|
|
2399
|
+
console.error(chalk13.red(`refine failed: ${final.error || "unknown"}`));
|
|
2400
|
+
} else if (!wrote) {
|
|
2401
|
+
console.error(chalk13.red(`refine completed but ${refinedFile} was not written`));
|
|
2402
|
+
}
|
|
2403
|
+
process.exit(1);
|
|
2404
|
+
}
|
|
2405
|
+
if (opts.json) console.log(JSON.stringify({ runtimeId, refinedFile }, null, 2));
|
|
2406
|
+
} catch (err) {
|
|
2407
|
+
console.error(chalk13.red(err.message));
|
|
2408
|
+
process.exit(1);
|
|
2409
|
+
}
|
|
2410
|
+
});
|
|
2411
|
+
|
|
2412
|
+
// src/commands/run.ts
|
|
2413
|
+
init_task_state2();
|
|
2414
|
+
import { Command as Command14 } from "commander";
|
|
2415
|
+
import chalk14 from "chalk";
|
|
2416
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
2417
|
+
var PHASES = ["triage", "plan", "refine", "exec"];
|
|
2418
|
+
var PAUSE_AFTER = /* @__PURE__ */ new Set(["refine"]);
|
|
2419
|
+
var CHECKPOINT_EXIT = 78;
|
|
2420
|
+
function nextPhaseFromWorkflow(wf) {
|
|
2421
|
+
switch (wf.phase) {
|
|
2422
|
+
case void 0:
|
|
2423
|
+
return "triage";
|
|
2424
|
+
case "triaged":
|
|
2425
|
+
return "plan";
|
|
2426
|
+
case "planned":
|
|
2427
|
+
return "refine";
|
|
2428
|
+
case "refined":
|
|
2429
|
+
return "exec";
|
|
2430
|
+
case "executing":
|
|
2431
|
+
case "completed":
|
|
2432
|
+
return null;
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
var run = new Command14("run").description("Run the full task orchestration (triage \u2192 plan \u2192 refine \u2192 exec)").argument("<objectId>", "Task object_id (tsk_XXXXX)").option("--from <phase>", "Start from phase (triage|plan|refine|exec)").option("--to <phase>", "Stop after phase").option("--auto", "Skip pauses (no exit 78 at checkpoints)").option("--planners <list>", "Comma-separated plan agents", "codex,claude-code").option("--executor <agent>", "Executor agent", "claude-code").option("--triage-agent <agent>", "Triage agent", "claude-code").option("--refine-agent <agent>", "Refine agent", "claude-code").option("--json", "Output JSON summary at end").action(async (objectId, opts) => {
|
|
2436
|
+
try {
|
|
2437
|
+
const loc = resolveTaskByObjectId(objectId);
|
|
2438
|
+
const wf = readWorkflow(loc.taskYml);
|
|
2439
|
+
const fromCandidate = opts.from ?? nextPhaseFromWorkflow(wf);
|
|
2440
|
+
if (!fromCandidate) {
|
|
2441
|
+
if (!opts.json) console.log(chalk14.dim(`Nothing to do \u2014 workflow phase is "${wf.phase}".`));
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
2444
|
+
const from = fromCandidate;
|
|
2445
|
+
const to = opts.to || "exec";
|
|
2446
|
+
if (!PHASES.includes(from)) throw new Error(`Invalid --from: ${from}`);
|
|
2447
|
+
if (!PHASES.includes(to)) throw new Error(`Invalid --to: ${to}`);
|
|
2448
|
+
const fromIdx = PHASES.indexOf(from);
|
|
2449
|
+
const toIdx = PHASES.indexOf(to);
|
|
2450
|
+
if (fromIdx > toIdx) throw new Error(`--from (${from}) is past --to (${to})`);
|
|
2451
|
+
if (!opts.json) {
|
|
2452
|
+
console.log(chalk14.bold(`task0 run ${objectId}`) + chalk14.dim(` \u2014 ${from} \u2192 ${to}`));
|
|
2453
|
+
}
|
|
2454
|
+
for (let i = fromIdx; i <= toIdx; i++) {
|
|
2455
|
+
const phase = PHASES[i];
|
|
2456
|
+
if (!opts.json) console.log(chalk14.bold.cyan(`
|
|
2457
|
+
\u25B6 ${phase}`));
|
|
2458
|
+
const code = runPhase(phase, objectId, opts);
|
|
2459
|
+
if (code !== 0) {
|
|
2460
|
+
console.error(chalk14.red(`Phase "${phase}" failed (exit ${code}). Resume with: task0 run ${objectId} --from ${phase}`));
|
|
2461
|
+
process.exit(code);
|
|
2462
|
+
}
|
|
2463
|
+
if (!opts.auto && PAUSE_AFTER.has(phase) && i < toIdx) {
|
|
2464
|
+
const next = PHASES[i + 1];
|
|
2465
|
+
if (!opts.json) {
|
|
2466
|
+
console.log(chalk14.yellow(`
|
|
2467
|
+
\u23F8 checkpoint after ${phase}. Review and resume with:`));
|
|
2468
|
+
console.log(chalk14.yellow(` task0 run ${objectId} --from ${next}`));
|
|
2469
|
+
}
|
|
2470
|
+
process.exit(CHECKPOINT_EXIT);
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
const finalWf = readWorkflow(loc.taskYml);
|
|
2474
|
+
if (opts.json) {
|
|
2475
|
+
console.log(JSON.stringify({ objectId, from, to, workflow: finalWf }, null, 2));
|
|
2476
|
+
} else {
|
|
2477
|
+
console.log(chalk14.green(`
|
|
2478
|
+
\u2713 done (phase: ${finalWf.phase})`));
|
|
2479
|
+
}
|
|
2480
|
+
} catch (err) {
|
|
2481
|
+
console.error(chalk14.red(err.message));
|
|
2482
|
+
process.exit(1);
|
|
2483
|
+
}
|
|
2484
|
+
});
|
|
2485
|
+
function runPhase(phase, objectId, opts) {
|
|
2486
|
+
const cli = process.argv[1];
|
|
2487
|
+
const node = process.argv[0];
|
|
2488
|
+
let args = [];
|
|
2489
|
+
switch (phase) {
|
|
2490
|
+
case "triage":
|
|
2491
|
+
args = ["task", "triage", objectId, "--wait", "--agent", opts.triageAgent];
|
|
2492
|
+
break;
|
|
2493
|
+
case "plan":
|
|
2494
|
+
args = ["plan", "generate", objectId, "--wait", "--agents", opts.planners];
|
|
2495
|
+
break;
|
|
2496
|
+
case "refine":
|
|
2497
|
+
args = ["plan", "refine", objectId, "--wait", "--agent", opts.refineAgent];
|
|
2498
|
+
break;
|
|
2499
|
+
case "exec":
|
|
2500
|
+
args = ["task", "exec", objectId, "--wait", "--agent", opts.executor];
|
|
2501
|
+
break;
|
|
2502
|
+
}
|
|
2503
|
+
const r = spawnSync3(node, [cli, ...args], { stdio: "inherit" });
|
|
2504
|
+
return r.status ?? 1;
|
|
2505
|
+
}
|
|
2506
|
+
run.command("status <objectId>").description("Show current workflow state").option("--json", "Output JSON").action((objectId, opts) => {
|
|
2507
|
+
try {
|
|
2508
|
+
const loc = resolveTaskByObjectId(objectId);
|
|
2509
|
+
const wf = readWorkflow(loc.taskYml);
|
|
2510
|
+
if (opts.json) {
|
|
2511
|
+
console.log(JSON.stringify(wf, null, 2));
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
console.log(`${chalk14.bold("phase:")} ${wf.phase ?? chalk14.dim("(not started)")}`);
|
|
2515
|
+
if (wf.issue_overview) {
|
|
2516
|
+
const extra = wf.issue_files?.length ? ` (+${wf.issue_files.length} details)` : "";
|
|
2517
|
+
console.log(`${chalk14.bold("issues:")} ${wf.issue_overview}${extra}`);
|
|
2518
|
+
}
|
|
2519
|
+
if (wf.plan_files?.length) console.log(`${chalk14.bold("plans:")} ${wf.plan_files.join(", ")}`);
|
|
2520
|
+
if (wf.refined_plan_file) console.log(`${chalk14.bold("refined:")} ${wf.refined_plan_file}`);
|
|
2521
|
+
if (wf.runtimes) {
|
|
2522
|
+
console.log(chalk14.bold("runtimes:"));
|
|
2523
|
+
for (const [k, v] of Object.entries(wf.runtimes)) {
|
|
2524
|
+
console.log(` ${k.padEnd(8)} ${typeof v === "string" ? v : JSON.stringify(v)}`);
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
if (wf.updated_at) console.log(chalk14.dim(`updated_at: ${wf.updated_at}`));
|
|
2528
|
+
} catch (err) {
|
|
2529
|
+
console.error(chalk14.red(err.message));
|
|
2530
|
+
process.exit(1);
|
|
2531
|
+
}
|
|
2532
|
+
});
|
|
2533
|
+
|
|
2534
|
+
// src/commands/okr.ts
|
|
2535
|
+
import { Command as Command15 } from "commander";
|
|
2536
|
+
import chalk15 from "chalk";
|
|
2537
|
+
|
|
2538
|
+
// src/lib/project.ts
|
|
2539
|
+
import path16 from "path";
|
|
2540
|
+
import fs17 from "fs";
|
|
2541
|
+
function resolveProjectName(opts) {
|
|
2542
|
+
if (opts.project && opts.project.length > 0) return opts.project;
|
|
2543
|
+
const config = loadConfig();
|
|
2544
|
+
const projects = config.sources.filter((s) => s.type === "project" && s.enabled !== false);
|
|
2545
|
+
if (projects.length === 0) {
|
|
2546
|
+
throw new Error(
|
|
2547
|
+
"Cannot resolve project: no registered projects. Use `task0 source add <path>` first, or pass --project <name>."
|
|
2548
|
+
);
|
|
2549
|
+
}
|
|
2550
|
+
const cwd = fs17.realpathSync(process.cwd());
|
|
2551
|
+
for (const source2 of projects) {
|
|
2552
|
+
let sourceAbs;
|
|
2553
|
+
try {
|
|
2554
|
+
sourceAbs = fs17.realpathSync(path16.resolve(source2.path));
|
|
2555
|
+
} catch {
|
|
2556
|
+
sourceAbs = path16.resolve(source2.path);
|
|
2557
|
+
}
|
|
2558
|
+
if (cwd === sourceAbs || cwd.startsWith(sourceAbs + path16.sep)) {
|
|
2559
|
+
return source2.name;
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
throw new Error(
|
|
2563
|
+
"Cannot resolve project from cwd. Pass --project <name> or run from a registered project directory."
|
|
2564
|
+
);
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
// src/commands/okr.ts
|
|
2568
|
+
function fail3(message) {
|
|
2569
|
+
console.error(chalk15.red(message));
|
|
2570
|
+
process.exit(1);
|
|
2571
|
+
}
|
|
2572
|
+
function parseNumber(value) {
|
|
2573
|
+
const n = Number(value);
|
|
2574
|
+
if (!Number.isFinite(n)) throw new Error(`invalid number: ${value}`);
|
|
2575
|
+
return n;
|
|
2576
|
+
}
|
|
2577
|
+
async function withProject(opts, fn) {
|
|
2578
|
+
try {
|
|
2579
|
+
const name = resolveProjectName(opts);
|
|
2580
|
+
return await fn(name);
|
|
2581
|
+
} catch (err) {
|
|
2582
|
+
fail3(err instanceof Error ? err.message : String(err));
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
function maybeJson(view, opts) {
|
|
2586
|
+
if (opts.json) {
|
|
2587
|
+
console.log(JSON.stringify(view, null, 2));
|
|
2588
|
+
return true;
|
|
2589
|
+
}
|
|
2590
|
+
return false;
|
|
2591
|
+
}
|
|
2592
|
+
function dateShort(iso) {
|
|
2593
|
+
if (!iso) return "\u2014";
|
|
2594
|
+
return iso.slice(0, 10);
|
|
2595
|
+
}
|
|
2596
|
+
function planEncodedPath(projectName, planId) {
|
|
2597
|
+
return `/api/projects/${encodeURIComponent(projectName)}/okrs/${encodeURIComponent(planId)}`;
|
|
2598
|
+
}
|
|
2599
|
+
function planBasePath(projectName) {
|
|
2600
|
+
return `/api/projects/${encodeURIComponent(projectName)}/okrs`;
|
|
2601
|
+
}
|
|
2602
|
+
function countKRs(plan3) {
|
|
2603
|
+
return plan3.objectives.reduce((sum, o) => sum + o.key_results.length, 0);
|
|
2604
|
+
}
|
|
2605
|
+
function printPlanSummary(plan3) {
|
|
2606
|
+
const oid = chalk15.cyan((plan3.object_id || "-").padEnd(10));
|
|
2607
|
+
const id = plan3.id.padEnd(16);
|
|
2608
|
+
const status = chalk15.yellow(plan3.status.padEnd(8));
|
|
2609
|
+
const range = `${dateShort(plan3.start_at)} \u2192 ${dateShort(plan3.end_at)}`;
|
|
2610
|
+
const counts = chalk15.dim(
|
|
2611
|
+
`${plan3.objectives.length} obj \xB7 ${countKRs(plan3)} kr \xB7 ${plan3.milestones.length} ms`
|
|
2612
|
+
);
|
|
2613
|
+
console.log(`${oid} ${id} ${status} ${range} ${counts} ${plan3.title}`);
|
|
2614
|
+
}
|
|
2615
|
+
function krProgress(kr2) {
|
|
2616
|
+
const start = kr2.start_value ?? 0;
|
|
2617
|
+
const target = kr2.target_value;
|
|
2618
|
+
const current = kr2.current_value;
|
|
2619
|
+
if (target === null || current === null) return "\u2014";
|
|
2620
|
+
const span = target - start;
|
|
2621
|
+
if (span === 0) return "\u2014";
|
|
2622
|
+
const ratio = Math.max(0, Math.min(1, (current - start) / span));
|
|
2623
|
+
const filled = Math.round(ratio * 8);
|
|
2624
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(8 - filled);
|
|
2625
|
+
return `[${bar}] ${Math.round(ratio * 100)}%`;
|
|
2626
|
+
}
|
|
2627
|
+
function printPlanDetail(plan3) {
|
|
2628
|
+
console.log(
|
|
2629
|
+
`${chalk15.bold(plan3.id)} ${chalk15.dim(plan3.object_id || "")} ${chalk15.yellow(plan3.status)} ${dateShort(plan3.start_at)} \u2192 ${dateShort(plan3.end_at)}`
|
|
2630
|
+
);
|
|
2631
|
+
if (plan3.title) console.log(` ${plan3.title}`);
|
|
2632
|
+
for (const obj of plan3.objectives) {
|
|
2633
|
+
console.log(
|
|
2634
|
+
` ${chalk15.bold(obj.id)} ${obj.title} ${chalk15.dim(`(${obj.status})`)} ${chalk15.dim(obj.object_id || "")}`
|
|
2635
|
+
);
|
|
2636
|
+
for (const kr2 of obj.key_results) {
|
|
2637
|
+
const unit = kr2.unit ? kr2.unit : "";
|
|
2638
|
+
const vals = kr2.target_value !== null ? `${kr2.start_value ?? 0}${unit}\u2192${kr2.target_value}${unit} now ${kr2.current_value ?? 0}${unit}` : "\u2014";
|
|
2639
|
+
console.log(
|
|
2640
|
+
` ${kr2.id.padEnd(20)} ${vals} ${krProgress(kr2)} ${chalk15.dim(`(${kr2.status}/${kr2.confidence})`)} ${chalk15.dim(kr2.object_id || "")}`
|
|
2641
|
+
);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
for (const ms of plan3.milestones) {
|
|
2645
|
+
const links = ms.linked_kr_ids.length ? chalk15.dim(`\u2192 ${ms.linked_kr_ids.join(",")}`) : "";
|
|
2646
|
+
console.log(
|
|
2647
|
+
` ${chalk15.bold(ms.id)} ${dateShort(ms.due_at)} ${ms.title} ${chalk15.dim(`(${ms.status})`)} ${links} ${chalk15.dim(ms.object_id || "")}`
|
|
2648
|
+
);
|
|
2649
|
+
}
|
|
2650
|
+
if (plan3.task_kr_links.length) {
|
|
2651
|
+
console.log(chalk15.dim("links:"));
|
|
2652
|
+
for (const link of plan3.task_kr_links) {
|
|
2653
|
+
console.log(
|
|
2654
|
+
` ${link.task_id} \u2192 ${link.kr_id} ${chalk15.dim(`${link.contribution} w=${link.weight}`)}`
|
|
2655
|
+
);
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
var okr = new Command15("okr").description(
|
|
2660
|
+
"Manage OKR plans, objectives, key results, milestones"
|
|
2661
|
+
);
|
|
2662
|
+
var plan2 = okr.command("plan").description("OKR plans");
|
|
2663
|
+
plan2.command("list").description("List OKR plans for the project").option("-p, --project <name>", "Project name (defaults to cwd)").option("--json", "Output JSON").action(async (opts) => {
|
|
2664
|
+
await withProject(opts, async (name) => {
|
|
2665
|
+
const { plans } = await api.get(planBasePath(name));
|
|
2666
|
+
if (maybeJson(plans, opts)) return;
|
|
2667
|
+
if (plans.length === 0) {
|
|
2668
|
+
console.log("No OKR plans. Use `task0 okr plan create <id> --title ...` to create one.");
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
for (const p of plans) printPlanSummary(p);
|
|
2672
|
+
});
|
|
2673
|
+
});
|
|
2674
|
+
plan2.command("get <plan-id>").description("Show a plan with its objectives, KRs, milestones").option("-p, --project <name>", "Project name (defaults to cwd)").option("--json", "Output JSON").action(async (planId, opts) => {
|
|
2675
|
+
await withProject(opts, async (name) => {
|
|
2676
|
+
const { plan: view } = await api.get(planEncodedPath(name, planId));
|
|
2677
|
+
if (maybeJson(view, opts)) return;
|
|
2678
|
+
printPlanDetail(view);
|
|
2679
|
+
});
|
|
2680
|
+
});
|
|
2681
|
+
plan2.command("create <plan-id>").description("Create a new OKR plan").requiredOption("-t, --title <title>", "Plan title").option("-s, --status <status>", "draft|active|closed|archived").option("--start-at <iso>", "ISO 8601 UTC start").option("--end-at <iso>", "ISO 8601 UTC end").option("-p, --project <name>", "Project name (defaults to cwd)").option("--json", "Output JSON").action(
|
|
2682
|
+
async (planId, opts) => {
|
|
2683
|
+
await withProject(opts, async (name) => {
|
|
2684
|
+
const body = {
|
|
2685
|
+
id: planId,
|
|
2686
|
+
title: opts.title,
|
|
2687
|
+
status: opts.status,
|
|
2688
|
+
start_at: opts.startAt,
|
|
2689
|
+
end_at: opts.endAt
|
|
2690
|
+
};
|
|
2691
|
+
const { plan: view } = await api.post(planBasePath(name), body);
|
|
2692
|
+
if (maybeJson(view, opts)) return;
|
|
2693
|
+
console.log(chalk15.green("ok"), view.id, chalk15.dim(view.object_id || ""));
|
|
2694
|
+
printPlanSummary(view);
|
|
2695
|
+
});
|
|
2696
|
+
}
|
|
2697
|
+
);
|
|
2698
|
+
plan2.command("update <plan-id>").description("Update plan fields").option("-t, --title <title>", "Plan title").option("-s, --status <status>", "draft|active|closed|archived").option("--start-at <iso>", "ISO 8601 UTC start").option("--end-at <iso>", "ISO 8601 UTC end").option("-p, --project <name>", "Project name (defaults to cwd)").option("--json", "Output JSON").action(
|
|
2699
|
+
async (planId, opts) => {
|
|
2700
|
+
await withProject(opts, async (name) => {
|
|
2701
|
+
const body = {
|
|
2702
|
+
title: opts.title,
|
|
2703
|
+
status: opts.status,
|
|
2704
|
+
start_at: opts.startAt,
|
|
2705
|
+
end_at: opts.endAt
|
|
2706
|
+
};
|
|
2707
|
+
const { plan: view } = await api.patch(
|
|
2708
|
+
planEncodedPath(name, planId),
|
|
2709
|
+
body
|
|
2710
|
+
);
|
|
2711
|
+
if (maybeJson(view, opts)) return;
|
|
2712
|
+
console.log(chalk15.green("updated"), view.id, chalk15.dim(view.object_id || ""));
|
|
2713
|
+
printPlanSummary(view);
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
);
|
|
2717
|
+
var objective = okr.command("objective").description("Objectives inside a plan");
|
|
2718
|
+
objective.command("create <plan-id> <objective-id>").description("Create an objective under a plan").requiredOption("-t, --title <title>", "Objective title").option("-d, --description <text>", "Description").option("--order <n>", "Display order", (v) => parseNumber(v)).option("-s, --status <status>", "draft|active|at_risk|done|cancelled").option("-o, --owner <owner>", "Owner").option("-p, --project <name>", "Project name (defaults to cwd)").option("--json", "Output JSON").action(
|
|
2719
|
+
async (planId, objectiveId, opts) => {
|
|
2720
|
+
await withProject(opts, async (name) => {
|
|
2721
|
+
const body = {
|
|
2722
|
+
id: objectiveId,
|
|
2723
|
+
title: opts.title,
|
|
2724
|
+
description: opts.description,
|
|
2725
|
+
order: opts.order,
|
|
2726
|
+
status: opts.status,
|
|
2727
|
+
owner: opts.owner
|
|
2728
|
+
};
|
|
2729
|
+
const { objective: view } = await api.post(
|
|
2730
|
+
`${planEncodedPath(name, planId)}/objectives`,
|
|
2731
|
+
body
|
|
2732
|
+
);
|
|
2733
|
+
if (maybeJson(view, opts)) return;
|
|
2734
|
+
console.log(
|
|
2735
|
+
chalk15.green("ok"),
|
|
2736
|
+
view.id,
|
|
2737
|
+
chalk15.dim(view.object_id || ""),
|
|
2738
|
+
chalk15.dim(`(${view.status})`),
|
|
2739
|
+
view.title
|
|
2740
|
+
);
|
|
2741
|
+
});
|
|
2742
|
+
}
|
|
2743
|
+
);
|
|
2744
|
+
objective.command("update <plan-id> <objective-id>").description("Update objective fields").option("-t, --title <title>", "Title").option("-d, --description <text>", "Description").option("--order <n>", "Display order", (v) => parseNumber(v)).option("-s, --status <status>", "draft|active|at_risk|done|cancelled").option("-o, --owner <owner>", "Owner").option("-p, --project <name>", "Project name (defaults to cwd)").option("--json", "Output JSON").action(
|
|
2745
|
+
async (planId, objectiveId, opts) => {
|
|
2746
|
+
await withProject(opts, async (name) => {
|
|
2747
|
+
const body = {
|
|
2748
|
+
title: opts.title,
|
|
2749
|
+
description: opts.description,
|
|
2750
|
+
order: opts.order,
|
|
2751
|
+
status: opts.status,
|
|
2752
|
+
owner: opts.owner
|
|
2753
|
+
};
|
|
2754
|
+
const { objective: view } = await api.patch(
|
|
2755
|
+
`${planEncodedPath(name, planId)}/objectives/${encodeURIComponent(objectiveId)}`,
|
|
2756
|
+
body
|
|
2757
|
+
);
|
|
2758
|
+
if (maybeJson(view, opts)) return;
|
|
2759
|
+
console.log(chalk15.green("updated"), view.id, chalk15.dim(`(${view.status})`), view.title);
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
);
|
|
2763
|
+
var kr = okr.command("kr").description("Key results inside an objective");
|
|
2764
|
+
kr.command("create <plan-id> <objective-id> <kr-id>").description("Create a key result under an objective").requiredOption("-t, --title <title>", "Key result title").option("-d, --description <text>", "Description").option("--metric-type <type>", "binary|numeric|percentage|milestone_count").option("--unit <unit>", "Unit (e.g. ms, %, count)").option("--start-value <n>", "Baseline value", (v) => parseNumber(v)).option("--current-value <n>", "Current value", (v) => parseNumber(v)).option("--target-value <n>", "Target value", (v) => parseNumber(v)).option("-s, --status <status>", "draft|active|at_risk|done|cancelled").option("-c, --confidence <level>", "low|medium|high").option("--due-at <iso>", "ISO 8601 UTC due").option("-o, --owner <owner>", "Owner").option("-p, --project <name>", "Project name (defaults to cwd)").option("--json", "Output JSON").action(
|
|
2765
|
+
async (planId, objectiveId, krId, opts) => {
|
|
2766
|
+
await withProject(opts, async (name) => {
|
|
2767
|
+
const body = {
|
|
2768
|
+
id: krId,
|
|
2769
|
+
title: opts.title,
|
|
2770
|
+
description: opts.description,
|
|
2771
|
+
metric_type: opts.metricType,
|
|
2772
|
+
unit: opts.unit,
|
|
2773
|
+
start_value: opts.startValue,
|
|
2774
|
+
current_value: opts.currentValue,
|
|
2775
|
+
target_value: opts.targetValue,
|
|
2776
|
+
status: opts.status,
|
|
2777
|
+
confidence: opts.confidence,
|
|
2778
|
+
due_at: opts.dueAt,
|
|
2779
|
+
owner: opts.owner
|
|
2780
|
+
};
|
|
2781
|
+
const { key_result: view } = await api.post(
|
|
2782
|
+
`${planEncodedPath(name, planId)}/objectives/${encodeURIComponent(objectiveId)}/key-results`,
|
|
2783
|
+
body
|
|
2784
|
+
);
|
|
2785
|
+
if (maybeJson(view, opts)) return;
|
|
2786
|
+
console.log(
|
|
2787
|
+
chalk15.green("ok"),
|
|
2788
|
+
view.id,
|
|
2789
|
+
chalk15.dim(view.object_id || ""),
|
|
2790
|
+
chalk15.dim(`(${view.status}/${view.confidence})`),
|
|
2791
|
+
view.title
|
|
2792
|
+
);
|
|
2793
|
+
});
|
|
2794
|
+
}
|
|
2795
|
+
);
|
|
2796
|
+
kr.command("update <plan-id> <kr-id>").description("Update a key result").option("-t, --title <title>", "Title").option("-d, --description <text>", "Description").option("--metric-type <type>", "binary|numeric|percentage|milestone_count").option("--unit <unit>", "Unit").option("--start-value <n>", "Baseline value", (v) => parseNumber(v)).option("--current-value <n>", "Current value", (v) => parseNumber(v)).option("--target-value <n>", "Target value", (v) => parseNumber(v)).option("-s, --status <status>", "draft|active|at_risk|done|cancelled").option("-c, --confidence <level>", "low|medium|high").option("--due-at <iso>", "ISO 8601 UTC due").option("-o, --owner <owner>", "Owner").option("-p, --project <name>", "Project name (defaults to cwd)").option("--json", "Output JSON").action(
|
|
2797
|
+
async (planId, krId, opts) => {
|
|
2798
|
+
await withProject(opts, async (name) => {
|
|
2799
|
+
const body = {
|
|
2800
|
+
title: opts.title,
|
|
2801
|
+
description: opts.description,
|
|
2802
|
+
metric_type: opts.metricType,
|
|
2803
|
+
unit: opts.unit,
|
|
2804
|
+
start_value: opts.startValue,
|
|
2805
|
+
current_value: opts.currentValue,
|
|
2806
|
+
target_value: opts.targetValue,
|
|
2807
|
+
status: opts.status,
|
|
2808
|
+
confidence: opts.confidence,
|
|
2809
|
+
due_at: opts.dueAt,
|
|
2810
|
+
owner: opts.owner
|
|
2811
|
+
};
|
|
2812
|
+
const { key_result: view } = await api.patch(
|
|
2813
|
+
`${planEncodedPath(name, planId)}/key-results/${encodeURIComponent(krId)}`,
|
|
2814
|
+
body
|
|
2815
|
+
);
|
|
2816
|
+
if (maybeJson(view, opts)) return;
|
|
2817
|
+
console.log(
|
|
2818
|
+
chalk15.green("updated"),
|
|
2819
|
+
view.id,
|
|
2820
|
+
chalk15.dim(`(${view.status}/${view.confidence})`),
|
|
2821
|
+
view.title
|
|
2822
|
+
);
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2825
|
+
);
|
|
2826
|
+
kr.command("progress <plan-id> <kr-id>").description("Update key result progress (current/confidence/status)").requiredOption("--current <n>", "Current value", (v) => parseNumber(v)).option("-c, --confidence <level>", "low|medium|high").option("-s, --status <status>", "draft|active|at_risk|done|cancelled").option("-p, --project <name>", "Project name (defaults to cwd)").option("--json", "Output JSON").action(
|
|
2827
|
+
async (planId, krId, opts) => {
|
|
2828
|
+
await withProject(opts, async (name) => {
|
|
2829
|
+
const body = {
|
|
2830
|
+
current_value: opts.current,
|
|
2831
|
+
confidence: opts.confidence,
|
|
2832
|
+
status: opts.status
|
|
2833
|
+
};
|
|
2834
|
+
const { key_result: view } = await api.patch(
|
|
2835
|
+
`${planEncodedPath(name, planId)}/key-results/${encodeURIComponent(krId)}`,
|
|
2836
|
+
body
|
|
2837
|
+
);
|
|
2838
|
+
if (maybeJson(view, opts)) return;
|
|
2839
|
+
console.log(
|
|
2840
|
+
chalk15.green("progress"),
|
|
2841
|
+
view.id,
|
|
2842
|
+
krProgress(view),
|
|
2843
|
+
chalk15.dim(`(${view.status}/${view.confidence})`)
|
|
2844
|
+
);
|
|
2845
|
+
});
|
|
2846
|
+
}
|
|
2847
|
+
);
|
|
2848
|
+
var milestone = okr.command("milestone").description("Milestones inside a plan");
|
|
2849
|
+
milestone.command("create <plan-id> <milestone-id>").description("Create a milestone under a plan").requiredOption("-t, --title <title>", "Milestone title").option("-d, --description <text>", "Description").option("--due-at <iso>", "ISO 8601 UTC due").option("--order <n>", "Display order", (v) => parseNumber(v)).option("-s, --status <status>", "pending|in_progress|done|delayed|cancelled").option("--linked-kr <id...>", "Linked key result ids (repeatable)").option("-p, --project <name>", "Project name (defaults to cwd)").option("--json", "Output JSON").action(
|
|
2850
|
+
async (planId, milestoneId, opts) => {
|
|
2851
|
+
await withProject(opts, async (name) => {
|
|
2852
|
+
const body = {
|
|
2853
|
+
id: milestoneId,
|
|
2854
|
+
title: opts.title,
|
|
2855
|
+
description: opts.description,
|
|
2856
|
+
due_at: opts.dueAt,
|
|
2857
|
+
order: opts.order,
|
|
2858
|
+
status: opts.status,
|
|
2859
|
+
linked_kr_ids: opts.linkedKr
|
|
2860
|
+
};
|
|
2861
|
+
const { milestone: view } = await api.post(
|
|
2862
|
+
`${planEncodedPath(name, planId)}/milestones`,
|
|
2863
|
+
body
|
|
2864
|
+
);
|
|
2865
|
+
if (maybeJson(view, opts)) return;
|
|
2866
|
+
console.log(
|
|
2867
|
+
chalk15.green("ok"),
|
|
2868
|
+
view.id,
|
|
2869
|
+
chalk15.dim(view.object_id || ""),
|
|
2870
|
+
dateShort(view.due_at),
|
|
2871
|
+
chalk15.dim(`(${view.status})`),
|
|
2872
|
+
view.title
|
|
2873
|
+
);
|
|
2874
|
+
});
|
|
2875
|
+
}
|
|
2876
|
+
);
|
|
2877
|
+
milestone.command("update <plan-id> <milestone-id>").description("Update a milestone").option("-t, --title <title>", "Title").option("-d, --description <text>", "Description").option("--due-at <iso>", "ISO 8601 UTC due").option("--order <n>", "Display order", (v) => parseNumber(v)).option("-s, --status <status>", "pending|in_progress|done|delayed|cancelled").option("--linked-kr <id...>", "Linked key result ids (repeatable, replaces set)").option("-p, --project <name>", "Project name (defaults to cwd)").option("--json", "Output JSON").action(
|
|
2878
|
+
async (planId, milestoneId, opts) => {
|
|
2879
|
+
await withProject(opts, async (name) => {
|
|
2880
|
+
const body = {
|
|
2881
|
+
title: opts.title,
|
|
2882
|
+
description: opts.description,
|
|
2883
|
+
due_at: opts.dueAt,
|
|
2884
|
+
order: opts.order,
|
|
2885
|
+
status: opts.status,
|
|
2886
|
+
linked_kr_ids: opts.linkedKr
|
|
2887
|
+
};
|
|
2888
|
+
const { milestone: view } = await api.patch(
|
|
2889
|
+
`${planEncodedPath(name, planId)}/milestones/${encodeURIComponent(milestoneId)}`,
|
|
2890
|
+
body
|
|
2891
|
+
);
|
|
2892
|
+
if (maybeJson(view, opts)) return;
|
|
2893
|
+
console.log(
|
|
2894
|
+
chalk15.green("updated"),
|
|
2895
|
+
view.id,
|
|
2896
|
+
dateShort(view.due_at),
|
|
2897
|
+
chalk15.dim(`(${view.status})`),
|
|
2898
|
+
view.title
|
|
2899
|
+
);
|
|
2900
|
+
});
|
|
2901
|
+
}
|
|
2902
|
+
);
|
|
2903
|
+
okr.command("link <plan-id>").description("Link a task to a key result").requiredOption("--task <id>", "Task id (tsk_...)").requiredOption("--kr <id>", "Key result id").option("--contribution <type>", "direct|indirect|blocker|enabler").option("--weight <n>", "Weight (> 0)", (v) => parseNumber(v)).option("--note <text>", "Note").option("-p, --project <name>", "Project name (defaults to cwd)").option("--json", "Output JSON").action(
|
|
2904
|
+
async (planId, opts) => {
|
|
2905
|
+
await withProject(opts, async (name) => {
|
|
2906
|
+
const body = {
|
|
2907
|
+
task_id: opts.task,
|
|
2908
|
+
kr_id: opts.kr,
|
|
2909
|
+
contribution: opts.contribution,
|
|
2910
|
+
weight: opts.weight,
|
|
2911
|
+
note: opts.note
|
|
2912
|
+
};
|
|
2913
|
+
const { link } = await api.post(
|
|
2914
|
+
`${planEncodedPath(name, planId)}/task-kr-links`,
|
|
2915
|
+
body
|
|
2916
|
+
);
|
|
2917
|
+
if (maybeJson(link, opts)) return;
|
|
2918
|
+
console.log(
|
|
2919
|
+
chalk15.green("linked"),
|
|
2920
|
+
`${link.task_id} \u2192 ${link.kr_id}`,
|
|
2921
|
+
chalk15.dim(`${link.contribution} w=${link.weight}`)
|
|
2922
|
+
);
|
|
2923
|
+
});
|
|
2924
|
+
}
|
|
2925
|
+
);
|
|
2926
|
+
okr.command("unlink <plan-id>").description("Unlink a task from a key result").requiredOption("--task <id>", "Task id (tsk_...)").requiredOption("--kr <id>", "Key result id").option("-p, --project <name>", "Project name (defaults to cwd)").option("--json", "Output JSON").action(
|
|
2927
|
+
async (planId, opts) => {
|
|
2928
|
+
await withProject(opts, async (name) => {
|
|
2929
|
+
const result = await api.del(
|
|
2930
|
+
`${planEncodedPath(name, planId)}/task-kr-links/${encodeURIComponent(opts.task)}/${encodeURIComponent(opts.kr)}`
|
|
2931
|
+
);
|
|
2932
|
+
if (maybeJson(result, opts)) return;
|
|
2933
|
+
console.log(chalk15.green("unlinked"), `${opts.task} \u2715 ${opts.kr}`);
|
|
2934
|
+
});
|
|
2935
|
+
}
|
|
2936
|
+
);
|
|
2937
|
+
|
|
2938
|
+
// src/commands/note.ts
|
|
2939
|
+
import { Command as Command16 } from "commander";
|
|
2940
|
+
import chalk16 from "chalk";
|
|
2941
|
+
var note = new Command16("note").description("Manage inbox notes");
|
|
2942
|
+
async function fetchAllInboxNotes() {
|
|
2943
|
+
const { inboxes } = await api.get("/api/inboxes");
|
|
2944
|
+
const results = [];
|
|
2945
|
+
for (const inbox of inboxes) {
|
|
2946
|
+
const { notes } = await api.get(`/api/inboxes/${encodeURIComponent(inbox.id)}/notes`);
|
|
2947
|
+
results.push({ inbox, notes });
|
|
2948
|
+
}
|
|
2949
|
+
return results;
|
|
2950
|
+
}
|
|
2951
|
+
async function resolveByObjectId(objectId) {
|
|
2952
|
+
const { type, resource } = await api.get(
|
|
2953
|
+
`/api/resolve/${encodeURIComponent(objectId)}`
|
|
2954
|
+
);
|
|
2955
|
+
if (type !== "inbox_note" || !resource.inboxId) {
|
|
2956
|
+
throw new Error(`object ${objectId} is not an inbox_note (got ${type})`);
|
|
2957
|
+
}
|
|
2958
|
+
const { inboxId, ...rest } = resource;
|
|
2959
|
+
return { inboxId, note: rest };
|
|
2960
|
+
}
|
|
2961
|
+
async function findNoteById(noteId, inboxHint) {
|
|
2962
|
+
if (noteId.startsWith("note_")) return resolveByObjectId(noteId);
|
|
2963
|
+
if (inboxHint) {
|
|
2964
|
+
const { notes } = await api.get(
|
|
2965
|
+
`/api/inboxes/${encodeURIComponent(inboxHint)}/notes`
|
|
2966
|
+
);
|
|
2967
|
+
const found = notes.find((n) => n.id === noteId);
|
|
2968
|
+
if (!found) throw new Error(`note ${noteId} not found in inbox ${inboxHint}`);
|
|
2969
|
+
return { inboxId: inboxHint, note: found };
|
|
2970
|
+
}
|
|
2971
|
+
const all = await fetchAllInboxNotes();
|
|
2972
|
+
for (const { inbox, notes } of all) {
|
|
2973
|
+
const found = notes.find((n) => n.id === noteId);
|
|
2974
|
+
if (found) return { inboxId: inbox.id, note: found };
|
|
2975
|
+
}
|
|
2976
|
+
throw new Error(`note ${noteId} not found in any inbox`);
|
|
2977
|
+
}
|
|
2978
|
+
function preview2(body, width = 60) {
|
|
2979
|
+
const single = body.replace(/\s+/g, " ").trim();
|
|
2980
|
+
return single.length > width ? single.slice(0, width - 1) + "\u2026" : single;
|
|
2981
|
+
}
|
|
2982
|
+
function printNoteRow(n, inboxName) {
|
|
2983
|
+
const oid = chalk16.cyan((n.object_id || "-").padEnd(14));
|
|
2984
|
+
const id = chalk16.dim(n.id.padEnd(10));
|
|
2985
|
+
const tags = n.tags.length ? chalk16.yellow(n.tags.join(",")) : chalk16.dim("-");
|
|
2986
|
+
const linked = n.linked_task_id ? chalk16.magenta(n.linked_task_id) : chalk16.dim("-");
|
|
2987
|
+
const inboxPart = inboxName ? chalk16.dim(`[${inboxName}] `) : "";
|
|
2988
|
+
console.log(`${oid} ${id} ${inboxPart}${preview2(n.body)} ${tags} ${linked} ${chalk16.dim(n.updated_at)}`);
|
|
2989
|
+
}
|
|
2990
|
+
function printNoteDetail(n, inboxId) {
|
|
2991
|
+
if (n.object_id) console.log(`${chalk16.bold("object_id:")} ${n.object_id}`);
|
|
2992
|
+
console.log(`${chalk16.bold("id:")} ${n.id}`);
|
|
2993
|
+
if (inboxId) console.log(`${chalk16.bold("inbox:")} ${inboxId}`);
|
|
2994
|
+
console.log(`${chalk16.bold("tags:")} ${n.tags.length ? n.tags.join(", ") : "-"}`);
|
|
2995
|
+
if (n.linked_task_id) console.log(`${chalk16.bold("linked_task:")} ${n.linked_task_id}`);
|
|
2996
|
+
if (n.linked_project) console.log(`${chalk16.bold("linked_project:")} ${n.linked_project}`);
|
|
2997
|
+
console.log(`${chalk16.bold("created_at:")} ${n.created_at}`);
|
|
2998
|
+
console.log(`${chalk16.bold("updated_at:")} ${n.updated_at}`);
|
|
2999
|
+
if (n.published_to?.length) {
|
|
3000
|
+
console.log(chalk16.bold("published_to:"));
|
|
3001
|
+
for (const p of n.published_to) {
|
|
3002
|
+
console.log(` - ${p.source_type}:${p.source_name} ${p.identifier} ${chalk16.dim(p.url)}`);
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
console.log(chalk16.bold("body:"));
|
|
3006
|
+
console.log(n.body.split("\n").map((l) => " " + l).join("\n"));
|
|
3007
|
+
}
|
|
3008
|
+
function fail4(err) {
|
|
3009
|
+
console.error(chalk16.red(err.message));
|
|
3010
|
+
process.exit(1);
|
|
3011
|
+
}
|
|
3012
|
+
note.command("list").description("List inbox notes").option("--inbox <id>", "Restrict to a specific inbox").option("--json", "Output JSON").action(async (opts) => {
|
|
3013
|
+
try {
|
|
3014
|
+
if (opts.inbox) {
|
|
3015
|
+
const { notes } = await api.get(
|
|
3016
|
+
`/api/inboxes/${encodeURIComponent(opts.inbox)}/notes`
|
|
3017
|
+
);
|
|
3018
|
+
if (opts.json) {
|
|
3019
|
+
console.log(JSON.stringify({ notes }, null, 2));
|
|
3020
|
+
return;
|
|
3021
|
+
}
|
|
3022
|
+
if (!notes.length) {
|
|
3023
|
+
console.log(chalk16.dim("No notes."));
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
3026
|
+
for (const n of notes) printNoteRow(n);
|
|
3027
|
+
return;
|
|
3028
|
+
}
|
|
3029
|
+
const all = await fetchAllInboxNotes();
|
|
3030
|
+
if (opts.json) {
|
|
3031
|
+
console.log(JSON.stringify(all, null, 2));
|
|
3032
|
+
return;
|
|
3033
|
+
}
|
|
3034
|
+
const flat = all.flatMap(({ inbox, notes }) => notes.map((n) => ({ n, inboxName: inbox.name })));
|
|
3035
|
+
if (!flat.length) {
|
|
3036
|
+
console.log(chalk16.dim("No notes."));
|
|
3037
|
+
return;
|
|
3038
|
+
}
|
|
3039
|
+
for (const { n, inboxName } of flat) printNoteRow(n, inboxName);
|
|
3040
|
+
} catch (err) {
|
|
3041
|
+
fail4(err);
|
|
3042
|
+
}
|
|
3043
|
+
});
|
|
3044
|
+
note.command("get <id>").description("Show a note by id or object_id (note_\u2026)").option("--inbox <id>", "Inbox id (optional when <id> is a note_ object_id)").option("--json", "Output JSON").action(async (id, opts) => {
|
|
3045
|
+
try {
|
|
3046
|
+
const { inboxId, note: found } = await findNoteById(id, opts.inbox);
|
|
3047
|
+
if (opts.json) {
|
|
3048
|
+
console.log(JSON.stringify({ inboxId, note: found }, null, 2));
|
|
3049
|
+
return;
|
|
3050
|
+
}
|
|
3051
|
+
printNoteDetail(found, inboxId);
|
|
3052
|
+
} catch (err) {
|
|
3053
|
+
fail4(err);
|
|
3054
|
+
}
|
|
3055
|
+
});
|
|
3056
|
+
note.command("create").description("Create a note in an inbox").requiredOption("--inbox <id>", "Inbox id").requiredOption("--body <text>", "Note body (markdown)").option("--tag <value>", "Tag (repeatable)", (v, prev) => prev.concat([v]), []).option("--linked-task <tsk>", "Linked task id").option("--linked-project <name>", "Linked project name").option("--json", "Output JSON").action(async (opts) => {
|
|
3057
|
+
try {
|
|
3058
|
+
const payload = { body: opts.body, tags: opts.tag };
|
|
3059
|
+
if (opts.linkedTask) payload.linked_task_id = opts.linkedTask;
|
|
3060
|
+
if (opts.linkedProject) payload.linked_project = opts.linkedProject;
|
|
3061
|
+
const { note: created } = await api.post(
|
|
3062
|
+
`/api/inboxes/${encodeURIComponent(opts.inbox)}/notes`,
|
|
3063
|
+
payload
|
|
3064
|
+
);
|
|
3065
|
+
if (opts.json) {
|
|
3066
|
+
console.log(JSON.stringify({ note: created }, null, 2));
|
|
3067
|
+
return;
|
|
3068
|
+
}
|
|
3069
|
+
console.log(chalk16.green(`Created ${created.object_id || created.id}`));
|
|
3070
|
+
printNoteDetail(created, opts.inbox);
|
|
3071
|
+
} catch (err) {
|
|
3072
|
+
fail4(err);
|
|
3073
|
+
}
|
|
3074
|
+
});
|
|
3075
|
+
note.command("update <id>").description("Update a note (accepts short id or note_ object_id)").option("--inbox <id>", "Inbox id (optional when <id> is a note_ object_id)").option("--body <text>", "New body").option("--tag <value>", "Replace tags (repeatable)", (v, prev) => prev.concat([v]), []).option("--clear-tags", "Clear all tags").option("--linked-task <tsk>", "Linked task id (empty string clears)").option("--linked-project <name>", "Linked project name (empty string clears)").option("--json", "Output JSON").action(async (id, opts) => {
|
|
3076
|
+
try {
|
|
3077
|
+
const { inboxId, note: existing } = await findNoteById(id, opts.inbox);
|
|
3078
|
+
const payload = {};
|
|
3079
|
+
if (opts.body !== void 0) payload.body = opts.body;
|
|
3080
|
+
if (opts.clearTags) payload.tags = [];
|
|
3081
|
+
else if (opts.tag.length) payload.tags = opts.tag;
|
|
3082
|
+
if (opts.linkedTask !== void 0) payload.linked_task_id = opts.linkedTask;
|
|
3083
|
+
if (opts.linkedProject !== void 0) payload.linked_project = opts.linkedProject;
|
|
3084
|
+
if (!Object.keys(payload).length) {
|
|
3085
|
+
console.error(chalk16.yellow("Nothing to update. Pass --body, --tag, --clear-tags, --linked-task, or --linked-project."));
|
|
3086
|
+
process.exit(1);
|
|
3087
|
+
}
|
|
3088
|
+
const { note: updated } = await api.patch(
|
|
3089
|
+
`/api/inboxes/${encodeURIComponent(inboxId)}/notes/${encodeURIComponent(existing.id)}`,
|
|
3090
|
+
payload
|
|
3091
|
+
);
|
|
3092
|
+
if (opts.json) {
|
|
3093
|
+
console.log(JSON.stringify({ note: updated }, null, 2));
|
|
3094
|
+
return;
|
|
3095
|
+
}
|
|
3096
|
+
console.log(chalk16.green(`Updated ${updated.object_id || updated.id}`));
|
|
3097
|
+
printNoteDetail(updated, inboxId);
|
|
3098
|
+
} catch (err) {
|
|
3099
|
+
fail4(err);
|
|
3100
|
+
}
|
|
3101
|
+
});
|
|
3102
|
+
note.command("delete <id>").description("Delete a note (accepts short id or note_ object_id)").option("--inbox <id>", "Inbox id (optional when <id> is a note_ object_id)").action(async (id, opts) => {
|
|
3103
|
+
try {
|
|
3104
|
+
const { inboxId, note: existing } = await findNoteById(id, opts.inbox);
|
|
3105
|
+
await api.del(
|
|
3106
|
+
`/api/inboxes/${encodeURIComponent(inboxId)}/notes/${encodeURIComponent(existing.id)}`
|
|
3107
|
+
);
|
|
3108
|
+
console.log(chalk16.green(`Deleted ${existing.object_id || existing.id}`));
|
|
3109
|
+
} catch (err) {
|
|
3110
|
+
fail4(err);
|
|
3111
|
+
}
|
|
3112
|
+
});
|
|
3113
|
+
note.command("publish <id>").description("Publish a note to a github/linear source").requiredOption("--source <name>", "Source name (non-project)").option("--inbox <id>", "Inbox id (optional when <id> is a note_ object_id)").option("--project-id <linear>", "Linear project id (linear sources only)").option("--json", "Output JSON").action(async (id, opts) => {
|
|
3114
|
+
try {
|
|
3115
|
+
const { inboxId, note: existing } = await findNoteById(id, opts.inbox);
|
|
3116
|
+
const payload = { source_name: opts.source };
|
|
3117
|
+
if (opts.projectId) payload.project_id = opts.projectId;
|
|
3118
|
+
const { published } = await api.post(
|
|
3119
|
+
`/api/inboxes/${encodeURIComponent(inboxId)}/notes/${encodeURIComponent(existing.id)}/publish`,
|
|
3120
|
+
payload
|
|
3121
|
+
);
|
|
3122
|
+
if (opts.json) {
|
|
3123
|
+
console.log(JSON.stringify({ published }, null, 2));
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
console.log(chalk16.green(`Published to ${published.source_type}:${published.source_name} ${published.identifier}`));
|
|
3127
|
+
console.log(` ${chalk16.dim(published.url)}`);
|
|
3128
|
+
} catch (err) {
|
|
3129
|
+
fail4(err);
|
|
3130
|
+
}
|
|
3131
|
+
});
|
|
3132
|
+
note.command("convert <id>").description("Convert a note into a task under a project source").requiredOption("--project <name>", "Project source name").option("--inbox <id>", "Inbox id (optional when <id> is a note_ object_id)").option("--json", "Output JSON").action(async (id, opts) => {
|
|
3133
|
+
try {
|
|
3134
|
+
const { inboxId, note: existing } = await findNoteById(id, opts.inbox);
|
|
3135
|
+
const { task: task2 } = await api.post(
|
|
3136
|
+
`/api/inboxes/${encodeURIComponent(inboxId)}/notes/${encodeURIComponent(existing.id)}/convert`,
|
|
3137
|
+
{ project: opts.project }
|
|
3138
|
+
);
|
|
3139
|
+
if (opts.json) {
|
|
3140
|
+
console.log(JSON.stringify({ task: task2 }, null, 2));
|
|
3141
|
+
return;
|
|
3142
|
+
}
|
|
3143
|
+
console.log(chalk16.green(`Converted to task ${task2.object_id} (${task2.id}) in ${task2.project}`));
|
|
3144
|
+
} catch (err) {
|
|
3145
|
+
fail4(err);
|
|
3146
|
+
}
|
|
3147
|
+
});
|
|
3148
|
+
|
|
3149
|
+
// src/commands/object.ts
|
|
3150
|
+
import { Command as Command17 } from "commander";
|
|
3151
|
+
import chalk17 from "chalk";
|
|
3152
|
+
var object = new Command17("object").description("Resolve any object by its object_id");
|
|
3153
|
+
function pickFirst(resource, keys) {
|
|
3154
|
+
for (const k of keys) {
|
|
3155
|
+
const value = resource[k];
|
|
3156
|
+
if (typeof value === "string" && value.length) return value;
|
|
3157
|
+
}
|
|
3158
|
+
return void 0;
|
|
3159
|
+
}
|
|
3160
|
+
function printSummary(result) {
|
|
3161
|
+
const { type, resource } = result;
|
|
3162
|
+
console.log(`${chalk17.bold("type:")} ${type}`);
|
|
3163
|
+
const title = pickFirst(resource, ["title", "name"]);
|
|
3164
|
+
const id = pickFirst(resource, ["object_id", "objectId", "id"]);
|
|
3165
|
+
const status = pickFirst(resource, ["status"]);
|
|
3166
|
+
if (id) console.log(`${chalk17.bold("object_id:")} ${id}`);
|
|
3167
|
+
if (title) console.log(`${chalk17.bold("title:")} ${title}`);
|
|
3168
|
+
if (status) console.log(`${chalk17.bold("status:")} ${status}`);
|
|
3169
|
+
const updatedAt = pickFirst(resource, ["updated_at", "updatedAt"]);
|
|
3170
|
+
if (updatedAt) console.log(`${chalk17.bold("updated_at:")} ${updatedAt}`);
|
|
3171
|
+
console.log(chalk17.dim("(pass --json for full payload)"));
|
|
3172
|
+
}
|
|
3173
|
+
object.command("get <object-id>").description("Resolve an object by object_id (tsk_\u2026, note_\u2026, obj_\u2026, rt_\u2026, \u2026)").option("--json", "Output full JSON envelope").action(async (objectId, opts) => {
|
|
3174
|
+
try {
|
|
3175
|
+
const result = await api.get(`/api/resolve/${encodeURIComponent(objectId)}`);
|
|
3176
|
+
if (opts.json) {
|
|
3177
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3180
|
+
printSummary(result);
|
|
3181
|
+
} catch (err) {
|
|
3182
|
+
const apiErr = err;
|
|
3183
|
+
if (apiErr.status === 404) {
|
|
3184
|
+
console.error(chalk17.red(`not found: ${objectId}`));
|
|
3185
|
+
process.exit(1);
|
|
3186
|
+
}
|
|
3187
|
+
console.error(chalk17.red(err.message));
|
|
3188
|
+
process.exit(1);
|
|
3189
|
+
}
|
|
3190
|
+
});
|
|
3191
|
+
|
|
3192
|
+
// src/commands/issue.ts
|
|
3193
|
+
import { Command as Command18 } from "commander";
|
|
3194
|
+
import chalk18 from "chalk";
|
|
3195
|
+
|
|
3196
|
+
// src/core/issue/decision.ts
|
|
3197
|
+
init_node();
|
|
3198
|
+
import fs18 from "fs";
|
|
3199
|
+
import path17 from "path";
|
|
3200
|
+
init_task_state2();
|
|
3201
|
+
var DECISION_FILE_RE = /^DECISION-(\d+)-([a-z0-9-]+)\.md$/;
|
|
3202
|
+
function selectBlockingIssues(taskDir, explicitIssue) {
|
|
3203
|
+
const files = fs18.existsSync(taskDir) ? fs18.readdirSync(taskDir) : [];
|
|
3204
|
+
const issueFiles = files.filter((f) => /^ISSUE-\d+\.md$/.test(f)).sort();
|
|
3205
|
+
const all = readOpenQuestions(taskDir, issueFiles);
|
|
3206
|
+
if (!explicitIssue) return all;
|
|
3207
|
+
const normalized = normalizeIssueFile(explicitIssue);
|
|
3208
|
+
const match = all.find((iq) => iq.file === normalized);
|
|
3209
|
+
if (!match) {
|
|
3210
|
+
const hint = all.length === 0 ? "no ISSUE-NN.md files have Open Questions" : `blocking ISSUEs: ${all.map((iq) => iq.file).join(", ")}`;
|
|
3211
|
+
throw new Error(`${normalized} has no Open Questions (${hint})`);
|
|
3212
|
+
}
|
|
3213
|
+
return [match];
|
|
3214
|
+
}
|
|
3215
|
+
function normalizeIssueFile(input) {
|
|
3216
|
+
if (/^ISSUE-\d+\.md$/.test(input)) return input;
|
|
3217
|
+
if (/^\d+$/.test(input)) return `ISSUE-${input.padStart(2, "0")}.md`;
|
|
3218
|
+
if (/^ISSUE-\d+$/.test(input)) return `${input}.md`;
|
|
3219
|
+
throw new Error(`--issue expects NN, ISSUE-NN, or ISSUE-NN.md (got: ${input})`);
|
|
3220
|
+
}
|
|
3221
|
+
function decisionFileName(issue2, agent2) {
|
|
3222
|
+
return `DECISION-${issue2.index}-${agent2}.md`;
|
|
3223
|
+
}
|
|
3224
|
+
function consolidatedFileName(issue2) {
|
|
3225
|
+
return `DECISION-${issue2.index}-consolidated.md`;
|
|
3226
|
+
}
|
|
3227
|
+
function buildProposePrompt(issue2, decisionFile, extra) {
|
|
3228
|
+
const lines = [
|
|
3229
|
+
`You are proposing resolutions to "Open Questions" in ${issue2.file} for task planning.`,
|
|
3230
|
+
``,
|
|
3231
|
+
`Task context files:`,
|
|
3232
|
+
`- ${issue2.file} (contains the "## Open Questions" section)`,
|
|
3233
|
+
`- ISSUE.md (task overview, if present)`,
|
|
3234
|
+
`- IDEA-${issue2.index}.md (original idea, if present)`,
|
|
3235
|
+
``,
|
|
3236
|
+
`For EACH bullet under "## Open Questions" in ${issue2.file}, produce one answer section in your output file.`,
|
|
3237
|
+
``,
|
|
3238
|
+
`Output:`,
|
|
3239
|
+
`- Write to ${decisionFile} in the task directory.`,
|
|
3240
|
+
`- Structure the file as:`,
|
|
3241
|
+
``,
|
|
3242
|
+
` # Proposed Decisions for ${issue2.file}`,
|
|
3243
|
+
``,
|
|
3244
|
+
` ## Q1: <verbatim question text>`,
|
|
3245
|
+
``,
|
|
3246
|
+
` **Recommendation:** <single-sentence answer>`,
|
|
3247
|
+
``,
|
|
3248
|
+
` **Reasoning:** <1-3 sentences>`,
|
|
3249
|
+
``,
|
|
3250
|
+
` **Evidence:**`,
|
|
3251
|
+
` - \`path/to/file.ts:NN\` \u2014 <what this shows>`,
|
|
3252
|
+
``,
|
|
3253
|
+
` ## Q2: ...`,
|
|
3254
|
+
``,
|
|
3255
|
+
`Rules:`,
|
|
3256
|
+
`- One Q heading per bullet, in the same order they appear in ${issue2.file}.`,
|
|
3257
|
+
`- Cite concrete file:line evidence from the repo when available; if no evidence exists, write "Evidence: none \u2014 judgment call" and give reasoning.`,
|
|
3258
|
+
`- Do NOT modify ${issue2.file}.`,
|
|
3259
|
+
`- Do NOT write any files other than ${decisionFile}.`
|
|
3260
|
+
];
|
|
3261
|
+
if (extra) {
|
|
3262
|
+
lines.push(``, `Additional instructions:`, extra);
|
|
3263
|
+
}
|
|
3264
|
+
return lines.join("\n");
|
|
3265
|
+
}
|
|
3266
|
+
function buildConsolidatePrompt(issue2, proposalFiles, consolidatedFile) {
|
|
3267
|
+
return [
|
|
3268
|
+
`You are consolidating multiple "Proposed Decisions" drafts into a single consolidated decision file.`,
|
|
3269
|
+
``,
|
|
3270
|
+
`Inputs:`,
|
|
3271
|
+
`- ${issue2.file} (source of "## Open Questions")`,
|
|
3272
|
+
...proposalFiles.map((f) => `- ${f} (one agent's proposal)`),
|
|
3273
|
+
``,
|
|
3274
|
+
`Output:`,
|
|
3275
|
+
`- Write to ${consolidatedFile} in the task directory.`,
|
|
3276
|
+
`- For each "## Qn" heading that appears in the proposals, produce ONE consolidated answer.`,
|
|
3277
|
+
`- When agents agree, adopt the shared answer and keep reasoning terse.`,
|
|
3278
|
+
`- When agents disagree, pick the option better supported by cited evidence and add a short line: "Chose X over Y because <reason>".`,
|
|
3279
|
+
`- Preserve the Q heading ordering from ${issue2.file}.`,
|
|
3280
|
+
`- Match the structure used by the proposals (Recommendation / Reasoning / Evidence).`,
|
|
3281
|
+
``,
|
|
3282
|
+
`Rules:`,
|
|
3283
|
+
`- Do NOT modify ${issue2.file} or any DECISION-*-<agent>.md.`,
|
|
3284
|
+
`- Do NOT write any files other than ${consolidatedFile}.`
|
|
3285
|
+
].join("\n");
|
|
3286
|
+
}
|
|
3287
|
+
function parseConsolidatedAnswers(md) {
|
|
3288
|
+
const sections = [];
|
|
3289
|
+
const qRe = /^##\s+(Q\d+:?\s*.*)$/gm;
|
|
3290
|
+
const matches = [];
|
|
3291
|
+
let match;
|
|
3292
|
+
while (match = qRe.exec(md)) {
|
|
3293
|
+
matches.push({
|
|
3294
|
+
heading: match[1].trim(),
|
|
3295
|
+
start: match.index,
|
|
3296
|
+
contentStart: match.index + match[0].length
|
|
3297
|
+
});
|
|
3298
|
+
}
|
|
3299
|
+
for (let i = 0; i < matches.length; i++) {
|
|
3300
|
+
const end = i + 1 < matches.length ? matches[i + 1].start : md.length;
|
|
3301
|
+
const body = md.slice(matches[i].contentStart, end).trim();
|
|
3302
|
+
sections.push({ heading: matches[i].heading, body });
|
|
3303
|
+
}
|
|
3304
|
+
return sections;
|
|
3305
|
+
}
|
|
3306
|
+
function rewriteIssueWithDecisions(taskDir, issueFile, consolidatedFile) {
|
|
3307
|
+
const issuePath = path17.join(taskDir, issueFile);
|
|
3308
|
+
const consolidatedPath = path17.join(taskDir, consolidatedFile);
|
|
3309
|
+
if (!fs18.existsSync(issuePath)) throw new Error(`${issueFile} not found in ${taskDir}`);
|
|
3310
|
+
if (!fs18.existsSync(consolidatedPath)) throw new Error(`${consolidatedFile} not found in ${taskDir}`);
|
|
3311
|
+
const issueMd = fs18.readFileSync(issuePath, "utf-8");
|
|
3312
|
+
const consolidatedMd = fs18.readFileSync(consolidatedPath, "utf-8");
|
|
3313
|
+
const answers = parseConsolidatedAnswers(consolidatedMd);
|
|
3314
|
+
if (answers.length === 0) {
|
|
3315
|
+
throw new Error(`${consolidatedFile} has no "## Qn" sections; cannot derive Decisions`);
|
|
3316
|
+
}
|
|
3317
|
+
const decisionsBody = answers.map((a) => {
|
|
3318
|
+
const recMatch = a.body.match(/\*\*Recommendation:\*\*\s*(.+?)(?:\n|$)/);
|
|
3319
|
+
const recommendation = recMatch?.[1]?.trim() ?? a.body.split("\n")[0] ?? "";
|
|
3320
|
+
return `- **${a.heading}** \u2014 ${recommendation}`;
|
|
3321
|
+
}).join("\n");
|
|
3322
|
+
const decisionsSection = `## Decisions
|
|
3323
|
+
|
|
3324
|
+
${decisionsBody}
|
|
3325
|
+
|
|
3326
|
+
_Resolved from [${consolidatedFile}](${consolidatedFile}); see that file for reasoning and evidence._
|
|
3327
|
+
`;
|
|
3328
|
+
const openQRe = /## Open Questions\s*\n[\s\S]*?(?=\n## |\n*$)/i;
|
|
3329
|
+
const openQMatch = issueMd.match(openQRe);
|
|
3330
|
+
if (!openQMatch) {
|
|
3331
|
+
throw new Error(`${issueFile} has no "## Open Questions" section to replace`);
|
|
3332
|
+
}
|
|
3333
|
+
const next = issueMd.replace(openQRe, decisionsSection.trimEnd() + "\n");
|
|
3334
|
+
fs18.writeFileSync(issuePath, next, "utf-8");
|
|
3335
|
+
return { replaced: answers.length };
|
|
3336
|
+
}
|
|
3337
|
+
async function propose(opts) {
|
|
3338
|
+
const loc = resolveTaskByObjectId(opts.objectId);
|
|
3339
|
+
let issues;
|
|
3340
|
+
try {
|
|
3341
|
+
issues = selectBlockingIssues(loc.taskDir, opts.issue);
|
|
3342
|
+
} catch (err) {
|
|
3343
|
+
if (opts.ifNeeded) return { issues: [] };
|
|
3344
|
+
throw err;
|
|
3345
|
+
}
|
|
3346
|
+
if (issues.length === 0) {
|
|
3347
|
+
if (opts.ifNeeded) return { issues: [] };
|
|
3348
|
+
throw new Error(`No blocking "## Open Questions" found in ${loc.taskDir}`);
|
|
3349
|
+
}
|
|
3350
|
+
if (opts.agents.length === 0) throw new Error("No agents specified for propose");
|
|
3351
|
+
if ((opts.model || opts.effort) && opts.agents.length > 1) {
|
|
3352
|
+
throw new Error(
|
|
3353
|
+
`--model/--effort only works with a single agent; got ${opts.agents.length} (${opts.agents.join(", ")}).`
|
|
3354
|
+
);
|
|
3355
|
+
}
|
|
3356
|
+
const warn = opts.warn;
|
|
3357
|
+
const issuesOut = [];
|
|
3358
|
+
for (const issue2 of issues) {
|
|
3359
|
+
const kicks = [];
|
|
3360
|
+
for (const agent2 of opts.agents) {
|
|
3361
|
+
const decisionFile = decisionFileName(issue2, agent2);
|
|
3362
|
+
const decisionPath = path17.join(loc.taskDir, decisionFile);
|
|
3363
|
+
if (fs18.existsSync(decisionPath) && !opts.force) {
|
|
3364
|
+
if (opts.ifNeeded) {
|
|
3365
|
+
kicks.push({
|
|
3366
|
+
issue: issue2.file,
|
|
3367
|
+
agent: agent2,
|
|
3368
|
+
runtimeId: "",
|
|
3369
|
+
sessionName: "",
|
|
3370
|
+
decisionFile,
|
|
3371
|
+
error: null,
|
|
3372
|
+
skipped: "already-exists"
|
|
3373
|
+
});
|
|
3374
|
+
} else {
|
|
3375
|
+
kicks.push({
|
|
3376
|
+
issue: issue2.file,
|
|
3377
|
+
agent: agent2,
|
|
3378
|
+
runtimeId: "",
|
|
3379
|
+
sessionName: "",
|
|
3380
|
+
decisionFile,
|
|
3381
|
+
error: `${decisionFile} already exists (pass --force to overwrite)`
|
|
3382
|
+
});
|
|
3383
|
+
}
|
|
3384
|
+
continue;
|
|
3385
|
+
}
|
|
3386
|
+
if (opts.model || opts.effort) {
|
|
3387
|
+
resolveModelOptions(agent2, { model: opts.model, effort: opts.effort }, warn);
|
|
3388
|
+
}
|
|
3389
|
+
void buildReferenceFiles;
|
|
3390
|
+
const prompt = buildProposePrompt(issue2, decisionFile, opts.additionalPrompt);
|
|
3391
|
+
try {
|
|
3392
|
+
const resp = await api.post(
|
|
3393
|
+
`/api/agents/${encodeURIComponent(agent2)}/run`,
|
|
3394
|
+
{
|
|
3395
|
+
task_id: opts.objectId,
|
|
3396
|
+
prompt
|
|
3397
|
+
}
|
|
3398
|
+
);
|
|
3399
|
+
kicks.push({
|
|
3400
|
+
issue: issue2.file,
|
|
3401
|
+
agent: agent2,
|
|
3402
|
+
runtimeId: resp.runtime.id,
|
|
3403
|
+
sessionName: "",
|
|
3404
|
+
decisionFile,
|
|
3405
|
+
error: null
|
|
3406
|
+
});
|
|
3407
|
+
} catch (err) {
|
|
3408
|
+
kicks.push({
|
|
3409
|
+
issue: issue2.file,
|
|
3410
|
+
agent: agent2,
|
|
3411
|
+
runtimeId: "",
|
|
3412
|
+
sessionName: "",
|
|
3413
|
+
decisionFile,
|
|
3414
|
+
error: err.message
|
|
3415
|
+
});
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
const runtimesByAgent = {};
|
|
3419
|
+
for (const k of kicks) if (!k.error && k.runtimeId) runtimesByAgent[k.agent] = k.runtimeId;
|
|
3420
|
+
const proposalFiles = kicks.filter((k) => !k.error).map((k) => k.decisionFile);
|
|
3421
|
+
if (Object.keys(runtimesByAgent).length > 0 || proposalFiles.length > 0) {
|
|
3422
|
+
await updateWorkflow(loc.taskYml, {
|
|
3423
|
+
decisions: {
|
|
3424
|
+
[issue2.file]: {
|
|
3425
|
+
proposal_files: proposalFiles.length > 0 ? proposalFiles : void 0,
|
|
3426
|
+
proposal_runtimes: Object.keys(runtimesByAgent).length > 0 ? runtimesByAgent : void 0
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
});
|
|
3430
|
+
}
|
|
3431
|
+
if (opts.wait) {
|
|
3432
|
+
for (const k of kicks) {
|
|
3433
|
+
if (k.error || !k.runtimeId) continue;
|
|
3434
|
+
try {
|
|
3435
|
+
const final = await waitForRuntime(k.runtimeId);
|
|
3436
|
+
if (final.status !== "done") {
|
|
3437
|
+
k.error = final.error || `runtime ${k.runtimeId} ended with ${final.status}`;
|
|
3438
|
+
} else if (!fs18.existsSync(path17.join(loc.taskDir, k.decisionFile))) {
|
|
3439
|
+
k.error = `runtime completed but ${k.decisionFile} was not written`;
|
|
3440
|
+
}
|
|
3441
|
+
} catch (err) {
|
|
3442
|
+
k.error = err.message;
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
issuesOut.push({ issue: issue2.file, kicks });
|
|
3447
|
+
}
|
|
3448
|
+
return { issues: issuesOut };
|
|
3449
|
+
}
|
|
3450
|
+
async function consolidate(opts) {
|
|
3451
|
+
const loc = resolveTaskByObjectId(opts.objectId);
|
|
3452
|
+
const issues = selectBlockingIssues(loc.taskDir, opts.issue);
|
|
3453
|
+
if (issues.length === 0) {
|
|
3454
|
+
throw new Error(`No blocking "## Open Questions" found in ${loc.taskDir}`);
|
|
3455
|
+
}
|
|
3456
|
+
const kicks = [];
|
|
3457
|
+
const modelOpts = resolveModelOptions(opts.agent, { model: opts.model, effort: opts.effort }, opts.warn);
|
|
3458
|
+
for (const issue2 of issues) {
|
|
3459
|
+
const files = fs18.readdirSync(loc.taskDir);
|
|
3460
|
+
const proposalFiles = files.filter((f) => {
|
|
3461
|
+
const m = f.match(DECISION_FILE_RE);
|
|
3462
|
+
return !!m && m[1] === issue2.index && m[2] !== "consolidated";
|
|
3463
|
+
}).sort();
|
|
3464
|
+
if (proposalFiles.length === 0) {
|
|
3465
|
+
kicks.push({
|
|
3466
|
+
issue: issue2.file,
|
|
3467
|
+
runtimeId: "",
|
|
3468
|
+
sessionName: "",
|
|
3469
|
+
consolidatedFile: consolidatedFileName(issue2),
|
|
3470
|
+
error: `no DECISION-${issue2.index}-<agent>.md proposals found. Run \`task0 issue propose\` first.`
|
|
3471
|
+
});
|
|
3472
|
+
continue;
|
|
3473
|
+
}
|
|
3474
|
+
const consolidatedFile = consolidatedFileName(issue2);
|
|
3475
|
+
void proposalFiles;
|
|
3476
|
+
void modelOpts;
|
|
3477
|
+
const prompt = buildConsolidatePrompt(issue2, proposalFiles, consolidatedFile);
|
|
3478
|
+
try {
|
|
3479
|
+
const resp = await api.post(
|
|
3480
|
+
`/api/agents/${encodeURIComponent(opts.agent)}/run`,
|
|
3481
|
+
{
|
|
3482
|
+
task_id: opts.objectId,
|
|
3483
|
+
prompt
|
|
3484
|
+
}
|
|
3485
|
+
);
|
|
3486
|
+
const runtimeId = resp.runtime.id;
|
|
3487
|
+
await updateWorkflow(loc.taskYml, {
|
|
3488
|
+
decisions: {
|
|
3489
|
+
[issue2.file]: {
|
|
3490
|
+
consolidated_file: consolidatedFile,
|
|
3491
|
+
consolidate_runtime: runtimeId
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
});
|
|
3495
|
+
const kick = {
|
|
3496
|
+
issue: issue2.file,
|
|
3497
|
+
runtimeId,
|
|
3498
|
+
sessionName: "",
|
|
3499
|
+
consolidatedFile,
|
|
3500
|
+
error: null
|
|
3501
|
+
};
|
|
3502
|
+
if (opts.wait) {
|
|
3503
|
+
try {
|
|
3504
|
+
const final = await waitForRuntime(runtimeId);
|
|
3505
|
+
const wrote = fs18.existsSync(path17.join(loc.taskDir, consolidatedFile));
|
|
3506
|
+
if (final.status !== "done") {
|
|
3507
|
+
kick.error = final.error || `runtime ${runtimeId} ended with ${final.status}`;
|
|
3508
|
+
} else if (!wrote) {
|
|
3509
|
+
kick.error = `runtime completed but ${consolidatedFile} was not written`;
|
|
3510
|
+
}
|
|
3511
|
+
kick.wrote = wrote;
|
|
3512
|
+
} catch (err) {
|
|
3513
|
+
kick.error = err.message;
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
kicks.push(kick);
|
|
3517
|
+
} catch (err) {
|
|
3518
|
+
kicks.push({
|
|
3519
|
+
issue: issue2.file,
|
|
3520
|
+
runtimeId: "",
|
|
3521
|
+
sessionName: "",
|
|
3522
|
+
consolidatedFile,
|
|
3523
|
+
error: err.message
|
|
3524
|
+
});
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
return { kicks };
|
|
3528
|
+
}
|
|
3529
|
+
async function approve(opts) {
|
|
3530
|
+
const loc = resolveTaskByObjectId(opts.objectId);
|
|
3531
|
+
const workflow = readWorkflow(loc.taskYml);
|
|
3532
|
+
const decisions = workflow.decisions || {};
|
|
3533
|
+
const candidates = [];
|
|
3534
|
+
if (opts.issue) {
|
|
3535
|
+
const normalized = normalizeIssueFile(opts.issue);
|
|
3536
|
+
const record = decisions[normalized];
|
|
3537
|
+
if (!record?.consolidated_file) {
|
|
3538
|
+
throw new Error(`${normalized} has no consolidated decision. Run \`task0 issue consolidate-propose\` first.`);
|
|
3539
|
+
}
|
|
3540
|
+
candidates.push({ issueFile: normalized, record });
|
|
3541
|
+
} else {
|
|
3542
|
+
for (const [issueFile, record] of Object.entries(decisions)) {
|
|
3543
|
+
if (record?.consolidated_file && !record.approved_at) {
|
|
3544
|
+
candidates.push({ issueFile, record });
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
if (candidates.length === 0) {
|
|
3548
|
+
throw new Error("No consolidated decisions pending approval. Run `task0 issue consolidate-propose` first.");
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
const updated = [];
|
|
3552
|
+
const approvedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3553
|
+
for (const c of candidates) {
|
|
3554
|
+
const consolidatedFile = c.record.consolidated_file;
|
|
3555
|
+
const { replaced } = rewriteIssueWithDecisions(loc.taskDir, c.issueFile, consolidatedFile);
|
|
3556
|
+
await updateWorkflow(loc.taskYml, {
|
|
3557
|
+
decisions: {
|
|
3558
|
+
[c.issueFile]: { approved_at: approvedAt }
|
|
3559
|
+
}
|
|
3560
|
+
});
|
|
3561
|
+
updated.push({
|
|
3562
|
+
issue: c.issueFile.replace(/\.md$/, ""),
|
|
3563
|
+
issueFile: c.issueFile,
|
|
3564
|
+
questionsReplaced: replaced,
|
|
3565
|
+
consolidatedFile
|
|
3566
|
+
});
|
|
3567
|
+
}
|
|
3568
|
+
return { updated };
|
|
3569
|
+
}
|
|
3570
|
+
function buildReferenceFiles(taskDir, issue2) {
|
|
3571
|
+
const names = fs18.readdirSync(taskDir);
|
|
3572
|
+
const refs = [issue2.file];
|
|
3573
|
+
if (names.includes("ISSUE.md")) refs.push("ISSUE.md");
|
|
3574
|
+
const ideaFile = `IDEA-${issue2.index}.md`;
|
|
3575
|
+
if (names.includes(ideaFile)) refs.push(ideaFile);
|
|
3576
|
+
return refs;
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3579
|
+
// src/commands/issue.ts
|
|
3580
|
+
var issue = new Command18("issue").description(
|
|
3581
|
+
"Resolve blocking ISSUE Open Questions via multi-agent decision proposals"
|
|
3582
|
+
);
|
|
3583
|
+
issue.command("propose <taskId>").description(`Fan out agents to propose decisions for the task's blocking "## Open Questions"`).option("--issue <file>", "Specific ISSUE file (ISSUE-NN, NN, or ISSUE-NN.md); defaults to all blocking ISSUEs").option("-a, --agents <list>", "Comma-separated agents", "codex,claude-code").option("--additional-prompt <text>", "Extra prompt content appended to the default propose prompt").option("--model <id>", "Model id or alias (only valid with a single agent)").option("--effort <level>", "Reasoning effort (only valid with a single agent)").option("--wait", "Wait for each runtime to finish").option("--force", "Overwrite existing DECISION files").option("--if-needed", "Idempotent: skip issues with no Open Questions and agents whose DECISION file already exists (exit 0)").option("--json", "Output JSON").action(async (taskId, opts) => {
|
|
3584
|
+
try {
|
|
3585
|
+
const agents = opts.agents.split(",").map((a) => a.trim()).filter(Boolean);
|
|
3586
|
+
const result = await propose({
|
|
3587
|
+
objectId: taskId,
|
|
3588
|
+
issue: opts.issue,
|
|
3589
|
+
agents,
|
|
3590
|
+
additionalPrompt: opts.additionalPrompt,
|
|
3591
|
+
model: opts.model,
|
|
3592
|
+
effort: opts.effort,
|
|
3593
|
+
wait: opts.wait,
|
|
3594
|
+
force: opts.force,
|
|
3595
|
+
ifNeeded: opts.ifNeeded,
|
|
3596
|
+
warn: opts.json ? void 0 : (m) => console.error(chalk18.dim(m))
|
|
3597
|
+
});
|
|
3598
|
+
if (opts.json) {
|
|
3599
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3600
|
+
const anyFailed2 = result.issues.some((i) => i.kicks.some((k) => k.error));
|
|
3601
|
+
if (anyFailed2) process.exit(1);
|
|
3602
|
+
return;
|
|
3603
|
+
}
|
|
3604
|
+
if (opts.ifNeeded && result.issues.length === 0) {
|
|
3605
|
+
console.log(chalk18.dim("no blocking Open Questions; nothing to propose"));
|
|
3606
|
+
return;
|
|
3607
|
+
}
|
|
3608
|
+
let anyFailed = false;
|
|
3609
|
+
for (const item of result.issues) {
|
|
3610
|
+
console.log(chalk18.bold(item.issue));
|
|
3611
|
+
for (const k of item.kicks) {
|
|
3612
|
+
if (k.error) {
|
|
3613
|
+
anyFailed = true;
|
|
3614
|
+
console.error(chalk18.red(` [${k.agent}] ${k.error}`));
|
|
3615
|
+
} else if (k.skipped === "already-exists") {
|
|
3616
|
+
console.log(chalk18.dim(` [${k.agent}] skipped (${k.decisionFile} exists)`));
|
|
3617
|
+
} else {
|
|
3618
|
+
console.log(chalk18.green(` [${k.agent}]`) + ` runtime ${k.runtimeId} \u2192 ${k.decisionFile}`);
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
if (anyFailed) process.exit(1);
|
|
3623
|
+
} catch (err) {
|
|
3624
|
+
console.error(chalk18.red(err.message));
|
|
3625
|
+
process.exit(1);
|
|
3626
|
+
}
|
|
3627
|
+
});
|
|
3628
|
+
issue.command("consolidate-propose <taskId>").description("Synthesize DECISION-NN-<agent>.md proposals into DECISION-NN-consolidated.md").option("--issue <file>", "Specific ISSUE file; defaults to all blocking ISSUEs with proposals").option("--agent <name>", "Agent to run consolidation", "claude-code").option("--model <id>", "Model id or alias").option("--effort <level>", "Reasoning effort").option("--wait", "Wait for the consolidation runtime to finish").option("--json", "Output JSON").action(async (taskId, opts) => {
|
|
3629
|
+
try {
|
|
3630
|
+
const result = await consolidate({
|
|
3631
|
+
objectId: taskId,
|
|
3632
|
+
issue: opts.issue,
|
|
3633
|
+
agent: opts.agent,
|
|
3634
|
+
model: opts.model,
|
|
3635
|
+
effort: opts.effort,
|
|
3636
|
+
wait: opts.wait,
|
|
3637
|
+
warn: opts.json ? void 0 : (m) => console.error(chalk18.dim(m))
|
|
3638
|
+
});
|
|
3639
|
+
if (opts.json) {
|
|
3640
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3641
|
+
const anyFailed2 = result.kicks.some((k) => k.error);
|
|
3642
|
+
if (anyFailed2) process.exit(1);
|
|
3643
|
+
return;
|
|
3644
|
+
}
|
|
3645
|
+
let anyFailed = false;
|
|
3646
|
+
for (const k of result.kicks) {
|
|
3647
|
+
if (k.error) {
|
|
3648
|
+
anyFailed = true;
|
|
3649
|
+
console.error(chalk18.red(`[${k.issue}] ${k.error}`));
|
|
3650
|
+
} else {
|
|
3651
|
+
console.log(
|
|
3652
|
+
chalk18.green(`[${k.issue}]`) + ` runtime ${k.runtimeId} \u2192 ${k.consolidatedFile}` + (k.wrote === false ? chalk18.yellow(" (pending write)") : "")
|
|
3653
|
+
);
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
if (anyFailed) process.exit(1);
|
|
3657
|
+
} catch (err) {
|
|
3658
|
+
console.error(chalk18.red(err.message));
|
|
3659
|
+
process.exit(1);
|
|
3660
|
+
}
|
|
3661
|
+
});
|
|
3662
|
+
issue.command("approve <taskId>").description("Apply the consolidated decisions to ISSUE files").option("--issue <file>", "Specific ISSUE file; defaults to every un-approved consolidated decision").option("--json", "Output JSON").action(async (taskId, opts) => {
|
|
3663
|
+
try {
|
|
3664
|
+
const result = await approve({ objectId: taskId, issue: opts.issue });
|
|
3665
|
+
if (opts.json) {
|
|
3666
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3667
|
+
return;
|
|
3668
|
+
}
|
|
3669
|
+
for (const u of result.updated) {
|
|
3670
|
+
console.log(chalk18.green(`approved ${u.issueFile}`) + ` (${u.questionsReplaced} decisions \u2190 ${u.consolidatedFile})`);
|
|
3671
|
+
}
|
|
3672
|
+
} catch (err) {
|
|
3673
|
+
console.error(chalk18.red(err.message));
|
|
3674
|
+
process.exit(1);
|
|
3675
|
+
}
|
|
3676
|
+
});
|
|
3677
|
+
|
|
3678
|
+
// src/commands/agent.ts
|
|
3679
|
+
import fs19 from "fs";
|
|
3680
|
+
import path18 from "path";
|
|
3681
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
3682
|
+
import { Command as Command19 } from "commander";
|
|
3683
|
+
import chalk19 from "chalk";
|
|
3684
|
+
import yaml6 from "js-yaml";
|
|
3685
|
+
function fail5(message, code = 1) {
|
|
3686
|
+
console.error(chalk19.red(message));
|
|
3687
|
+
process.exit(code);
|
|
3688
|
+
}
|
|
3689
|
+
function formatAgent(a, withDetails = false) {
|
|
3690
|
+
const tag = a.system ? chalk19.dim("(system)") : a.scope ? chalk19.dim(`(${a.scope})`) : "";
|
|
3691
|
+
const head = `${chalk19.bold(a.slug.padEnd(24))} ${chalk19.dim(a.object_id.padEnd(16))} ${a.kind.padEnd(10)} ${tag}`;
|
|
3692
|
+
if (!withDetails) return head;
|
|
3693
|
+
const lines = [head];
|
|
3694
|
+
if (a.description) lines.push(chalk19.dim(" " + a.description));
|
|
3695
|
+
return lines.join("\n");
|
|
3696
|
+
}
|
|
3697
|
+
var agent = new Command19("agent").description("Manage agents and run them against tasks");
|
|
3698
|
+
agent.command("list").description("List visible agents (project + user + built-in cascade)").option("--kind <kind>", "Filter by kind: coding | llm-api | workflow").option("--include-system", "Include built-in system agents (default true)").option("--json", "Output JSON").action(async (opts) => {
|
|
3699
|
+
try {
|
|
3700
|
+
const params = new URLSearchParams();
|
|
3701
|
+
if (opts.kind) params.set("kind", opts.kind.replace("-", "_"));
|
|
3702
|
+
if (opts.includeSystem === false) params.set("include_system", "false");
|
|
3703
|
+
const qs = params.toString() ? `?${params}` : "";
|
|
3704
|
+
const result = await api.get(`/api/agents${qs}`);
|
|
3705
|
+
if (opts.json) return console.log(JSON.stringify(result.agents, null, 2));
|
|
3706
|
+
if (!result.agents.length) {
|
|
3707
|
+
console.log(chalk19.dim("(no agents)"));
|
|
3708
|
+
return;
|
|
3709
|
+
}
|
|
3710
|
+
for (const a of result.agents) console.log(formatAgent(a, true));
|
|
3711
|
+
} catch (err) {
|
|
3712
|
+
fail5(err.message);
|
|
3713
|
+
}
|
|
3714
|
+
});
|
|
3715
|
+
agent.command("get <ref>").description("Show one agent by object_id or slug").option("--json", "Output JSON").action(async (ref, opts) => {
|
|
3716
|
+
try {
|
|
3717
|
+
const result = await api.get(`/api/agents/${encodeURIComponent(ref)}`);
|
|
3718
|
+
if (opts.json) return console.log(JSON.stringify(result.agent, null, 2));
|
|
3719
|
+
console.log(formatAgent(result.agent, true));
|
|
3720
|
+
console.log();
|
|
3721
|
+
console.log(chalk19.dim("--- spec ---"));
|
|
3722
|
+
console.log(yaml6.dump(result.agent.spec, { lineWidth: 100 }));
|
|
3723
|
+
} catch (err) {
|
|
3724
|
+
const apiErr = err;
|
|
3725
|
+
if (apiErr.status === 404) fail5(`not found: ${ref}`);
|
|
3726
|
+
fail5(err.message);
|
|
3727
|
+
}
|
|
3728
|
+
});
|
|
3729
|
+
agent.command("create").description("Create an agent from a YAML spec file").requiredOption("--from-file <file>", "YAML spec file with at least kind, slug, spec").option("--scope <scope>", "Storage scope: user (default) or project", "user").option("--project-root <path>", "Project root (required for --scope project)").action(async (opts) => {
|
|
3730
|
+
let parsed;
|
|
3731
|
+
try {
|
|
3732
|
+
const raw = fs19.readFileSync(path18.resolve(opts.fromFile), "utf-8");
|
|
3733
|
+
parsed = yaml6.load(raw);
|
|
3734
|
+
} catch (err) {
|
|
3735
|
+
fail5(`cannot read ${opts.fromFile}: ${err.message}`);
|
|
3736
|
+
}
|
|
3737
|
+
try {
|
|
3738
|
+
const body = { ...parsed, scope: opts.scope, project_root: opts.projectRoot };
|
|
3739
|
+
const result = await api.post("/api/agents", body);
|
|
3740
|
+
console.log(chalk19.green(`created ${result.agent.slug} (${result.agent.object_id})`));
|
|
3741
|
+
} catch (err) {
|
|
3742
|
+
fail5(err.message);
|
|
3743
|
+
}
|
|
3744
|
+
});
|
|
3745
|
+
agent.command("edit <ref>").description("Open the agent YAML in $EDITOR and save changes").action(async (ref) => {
|
|
3746
|
+
try {
|
|
3747
|
+
const result = await api.get(`/api/agents/${encodeURIComponent(ref)}`);
|
|
3748
|
+
if (result.agent.system) fail5("cannot edit a system agent");
|
|
3749
|
+
const tmp = path18.join(
|
|
3750
|
+
process.env.TMPDIR || "/tmp",
|
|
3751
|
+
`task0-agent-${result.agent.slug}-${Date.now()}.yml`
|
|
3752
|
+
);
|
|
3753
|
+
fs19.writeFileSync(tmp, yaml6.dump(result.agent, { lineWidth: 100 }), "utf-8");
|
|
3754
|
+
const editor = process.env.EDITOR || "vi";
|
|
3755
|
+
const r = spawnSync4(editor, [tmp], { stdio: "inherit" });
|
|
3756
|
+
if (r.status !== 0) fail5(`editor exited with status ${r.status}`);
|
|
3757
|
+
const updated = yaml6.load(fs19.readFileSync(tmp, "utf-8"));
|
|
3758
|
+
await api.put(`/api/agents/${encodeURIComponent(ref)}`, updated);
|
|
3759
|
+
fs19.unlinkSync(tmp);
|
|
3760
|
+
console.log(chalk19.green(`updated ${ref}`));
|
|
3761
|
+
} catch (err) {
|
|
3762
|
+
fail5(err.message);
|
|
3763
|
+
}
|
|
3764
|
+
});
|
|
3765
|
+
agent.command("delete <ref>").description("Delete an agent").action(async (ref) => {
|
|
3766
|
+
try {
|
|
3767
|
+
const result = await api.del(`/api/agents/${encodeURIComponent(ref)}`);
|
|
3768
|
+
if (!result.deleted) fail5(`not deleted: ${ref}`);
|
|
3769
|
+
console.log(chalk19.green(`deleted ${ref}`));
|
|
3770
|
+
} catch (err) {
|
|
3771
|
+
fail5(err.message);
|
|
3772
|
+
}
|
|
3773
|
+
});
|
|
3774
|
+
agent.command("run <ref>").description("Run an agent against a task with a user prompt").requiredOption("--task <task-id>", "Task id (tsk_\u2026 or directory name)").requiredOption("--prompt <text>", "User prompt to send the agent").option("--stream", "Stream output.jsonl to stdout as it arrives").option("--attach", "Open tmux session (coding agents only)").action(async (ref, opts) => {
|
|
3775
|
+
try {
|
|
3776
|
+
const result = await api.post(
|
|
3777
|
+
`/api/agents/${encodeURIComponent(ref)}/run`,
|
|
3778
|
+
{ task_id: opts.task, prompt: opts.prompt }
|
|
3779
|
+
);
|
|
3780
|
+
console.log(chalk19.green(`runtime ${result.runtime.id} started`));
|
|
3781
|
+
if (result.runtime.objectId) console.log(chalk19.dim(`object_id: ${result.runtime.objectId}`));
|
|
3782
|
+
if (opts.stream) {
|
|
3783
|
+
await streamOutput(ref, result.runtime.id);
|
|
3784
|
+
}
|
|
3785
|
+
if (opts.attach) {
|
|
3786
|
+
console.log(chalk19.dim("--attach not yet wired; runtime is visible in dashboard"));
|
|
3787
|
+
}
|
|
3788
|
+
} catch (err) {
|
|
3789
|
+
fail5(err.message);
|
|
3790
|
+
}
|
|
3791
|
+
});
|
|
3792
|
+
async function streamOutput(ref, runtimeId) {
|
|
3793
|
+
let lastCount = 0;
|
|
3794
|
+
for (let i = 0; i < 600; i++) {
|
|
3795
|
+
const result = await api.get(
|
|
3796
|
+
`/api/agents/${encodeURIComponent(ref)}/runs/${encodeURIComponent(runtimeId)}/output`
|
|
3797
|
+
);
|
|
3798
|
+
for (const line of result.lines.slice(lastCount)) {
|
|
3799
|
+
if (line.error?.message) {
|
|
3800
|
+
console.error(chalk19.red(`[error] ${line.error.message}`));
|
|
3801
|
+
return;
|
|
3802
|
+
}
|
|
3803
|
+
const text = line.message?.content?.map((c) => c.text ?? "").join("") ?? "";
|
|
3804
|
+
if (text) process.stdout.write(text);
|
|
3805
|
+
}
|
|
3806
|
+
lastCount = result.lines.length;
|
|
3807
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3811
|
+
// src/commands/daemon.ts
|
|
3812
|
+
import os5 from "os";
|
|
3813
|
+
import { Command as Command20 } from "commander";
|
|
3814
|
+
import chalk20 from "chalk";
|
|
3815
|
+
import WebSocket from "ws";
|
|
3816
|
+
|
|
3817
|
+
// src/core/daemon-config.ts
|
|
3818
|
+
import fs20 from "fs";
|
|
3819
|
+
import os4 from "os";
|
|
3820
|
+
import path19 from "path";
|
|
3821
|
+
var CONFIG_DIR2 = path19.join(os4.homedir(), ".config", "task0");
|
|
3822
|
+
var CONFIG_FILE2 = path19.join(CONFIG_DIR2, "daemon.json");
|
|
3823
|
+
function daemonConfigPath() {
|
|
3824
|
+
return CONFIG_FILE2;
|
|
3825
|
+
}
|
|
3826
|
+
function readDaemonIdentity() {
|
|
3827
|
+
if (!fs20.existsSync(CONFIG_FILE2)) return null;
|
|
3828
|
+
try {
|
|
3829
|
+
const raw = fs20.readFileSync(CONFIG_FILE2, "utf-8");
|
|
3830
|
+
return JSON.parse(raw);
|
|
3831
|
+
} catch {
|
|
3832
|
+
return null;
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
function writeDaemonIdentity(identity) {
|
|
3836
|
+
fs20.mkdirSync(CONFIG_DIR2, { recursive: true });
|
|
3837
|
+
fs20.writeFileSync(CONFIG_FILE2, JSON.stringify(identity, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
|
|
3838
|
+
try {
|
|
3839
|
+
fs20.chmodSync(CONFIG_FILE2, 384);
|
|
3840
|
+
} catch {
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
function clearDaemonIdentity() {
|
|
3844
|
+
if (!fs20.existsSync(CONFIG_FILE2)) return false;
|
|
3845
|
+
fs20.unlinkSync(CONFIG_FILE2);
|
|
3846
|
+
return true;
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3849
|
+
// src/core/daemon-rpc-handlers.ts
|
|
3850
|
+
init_node();
|
|
3851
|
+
import fs21 from "fs";
|
|
3852
|
+
import path20 from "path";
|
|
3853
|
+
var MAX_FILE_BYTES = 1 * 1024 * 1024;
|
|
3854
|
+
function ensureString(value, name) {
|
|
3855
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
3856
|
+
throw Object.assign(new Error(`${name} is required (string)`), { code: "invalid_params" });
|
|
3857
|
+
}
|
|
3858
|
+
return value;
|
|
3859
|
+
}
|
|
3860
|
+
var rpcHandlers = {
|
|
3861
|
+
// Scan a local project for its task manifest. Returns the same shape the
|
|
3862
|
+
// in-process server's /api/tasks scanProject() returns.
|
|
3863
|
+
async scan_project(params) {
|
|
3864
|
+
const rootPath = ensureString(params.rootPath, "rootPath");
|
|
3865
|
+
const name = typeof params.name === "string" && params.name ? params.name : path20.basename(rootPath);
|
|
3866
|
+
return scanProject(rootPath, name);
|
|
3867
|
+
},
|
|
3868
|
+
// Read a file from disk on the daemon's host. The dashboard uses this to
|
|
3869
|
+
// peek into archived task content, runtime logs, etc. Cap is 1 MiB to
|
|
3870
|
+
// avoid streaming huge files over a single RPC envelope.
|
|
3871
|
+
async read_file(params) {
|
|
3872
|
+
const filePath = ensureString(params.path, "path");
|
|
3873
|
+
let stat;
|
|
3874
|
+
try {
|
|
3875
|
+
stat = fs21.statSync(filePath);
|
|
3876
|
+
} catch {
|
|
3877
|
+
throw Object.assign(new Error("file not found"), { code: "not_found" });
|
|
3878
|
+
}
|
|
3879
|
+
if (stat.isDirectory()) {
|
|
3880
|
+
throw Object.assign(new Error("path is a directory"), { code: "invalid_target" });
|
|
3881
|
+
}
|
|
3882
|
+
if (stat.size > MAX_FILE_BYTES) {
|
|
3883
|
+
throw Object.assign(new Error(`file too large (${stat.size} bytes > ${MAX_FILE_BYTES})`), { code: "too_large" });
|
|
3884
|
+
}
|
|
3885
|
+
const content = fs21.readFileSync(filePath, "utf-8");
|
|
3886
|
+
return { content, size: stat.size, modifiedAt: stat.mtime.toISOString() };
|
|
3887
|
+
}
|
|
3888
|
+
};
|
|
3889
|
+
|
|
3890
|
+
// src/commands/daemon.ts
|
|
3891
|
+
var DAEMON_VERSION = "0.1.0";
|
|
3892
|
+
async function dispatchRpc(ws, id, method, params) {
|
|
3893
|
+
const handler = rpcHandlers[method];
|
|
3894
|
+
if (!handler) {
|
|
3895
|
+
sendRpc(ws, { type: "rpc_error", id, error: { code: "unknown_method", message: `unknown method: ${method}` } });
|
|
3896
|
+
return;
|
|
3897
|
+
}
|
|
3898
|
+
try {
|
|
3899
|
+
const result = await handler(params ?? {});
|
|
3900
|
+
sendRpc(ws, { type: "rpc_response", id, result });
|
|
3901
|
+
} catch (error2) {
|
|
3902
|
+
const code = error2 && typeof error2 === "object" && "code" in error2 && typeof error2.code === "string" ? error2.code : "handler_error";
|
|
3903
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
3904
|
+
sendRpc(ws, { type: "rpc_error", id, error: { code, message } });
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
function sendRpc(ws, payload) {
|
|
3908
|
+
if (ws.readyState === ws.OPEN) {
|
|
3909
|
+
ws.send(JSON.stringify(payload));
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
function fail6(message, code = 1) {
|
|
3913
|
+
console.error(chalk20.red(message));
|
|
3914
|
+
process.exit(code);
|
|
3915
|
+
}
|
|
3916
|
+
function loadRequiredIdentity() {
|
|
3917
|
+
const identity = readDaemonIdentity();
|
|
3918
|
+
if (!identity) {
|
|
3919
|
+
fail6(`No daemon identity at ${daemonConfigPath()}. Run \`task0 daemon register --server <url>\` first.`);
|
|
3920
|
+
}
|
|
3921
|
+
return identity;
|
|
3922
|
+
}
|
|
3923
|
+
function serverBase(identity) {
|
|
3924
|
+
if (identity) return identity.server_url.replace(/\/$/, "");
|
|
3925
|
+
return (process.env.TASK0_API_URL || "http://127.0.0.1:4318").replace(/\/$/, "");
|
|
3926
|
+
}
|
|
3927
|
+
async function jsonGet(url) {
|
|
3928
|
+
let res;
|
|
3929
|
+
try {
|
|
3930
|
+
res = await fetch(url);
|
|
3931
|
+
} catch (error2) {
|
|
3932
|
+
fail6(`Cannot reach ${url}: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
3933
|
+
}
|
|
3934
|
+
if (!res.ok) {
|
|
3935
|
+
fail6(`Server returned ${res.status}: ${await res.text()}`);
|
|
3936
|
+
}
|
|
3937
|
+
return res.json();
|
|
3938
|
+
}
|
|
3939
|
+
var daemonCmd = new Command20("daemon").description("Manage this host as a task0 daemon registered with a central server");
|
|
3940
|
+
daemonCmd.command("register").description("Register this host with a central server and save the identity locally").requiredOption("-s, --server <url>", "Central server URL (e.g. https://central.example.com:4318)").option("-n, --name <name>", "Display name for this daemon (defaults to hostname)").option("--force", "Overwrite existing identity if present").action(async (opts) => {
|
|
3941
|
+
const existing = readDaemonIdentity();
|
|
3942
|
+
if (existing && !opts.force) {
|
|
3943
|
+
fail6(`Already registered as ${existing.daemon_id}. Pass --force to re-register.`);
|
|
3944
|
+
}
|
|
3945
|
+
const base = opts.server.replace(/\/$/, "");
|
|
3946
|
+
const body = {
|
|
3947
|
+
hostname: os5.hostname(),
|
|
3948
|
+
platform: process.platform,
|
|
3949
|
+
name: opts.name
|
|
3950
|
+
};
|
|
3951
|
+
let res;
|
|
3952
|
+
try {
|
|
3953
|
+
res = await fetch(`${base}/api/daemons/register`, {
|
|
3954
|
+
method: "POST",
|
|
3955
|
+
headers: { "content-type": "application/json" },
|
|
3956
|
+
body: JSON.stringify(body)
|
|
3957
|
+
});
|
|
3958
|
+
} catch (error2) {
|
|
3959
|
+
fail6(`Cannot reach ${base}: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
3960
|
+
}
|
|
3961
|
+
if (!res.ok) {
|
|
3962
|
+
fail6(`Server rejected registration (${res.status}): ${await res.text()}`);
|
|
3963
|
+
}
|
|
3964
|
+
const data = await res.json();
|
|
3965
|
+
const identity = {
|
|
3966
|
+
daemon_id: data.daemon.object_id,
|
|
3967
|
+
name: data.daemon.name,
|
|
3968
|
+
hostname: data.daemon.hostname,
|
|
3969
|
+
platform: data.daemon.platform,
|
|
3970
|
+
server_url: base,
|
|
3971
|
+
token: data.token,
|
|
3972
|
+
registered_at: data.daemon.registered_at
|
|
3973
|
+
};
|
|
3974
|
+
writeDaemonIdentity(identity);
|
|
3975
|
+
console.log(chalk20.green(`Registered as ${data.daemon.object_id} (${data.daemon.name})`));
|
|
3976
|
+
console.log(`Identity saved to ${daemonConfigPath()}`);
|
|
3977
|
+
});
|
|
3978
|
+
daemonCmd.command("start").description("Start the local daemon and connect to the registered server (long-running)").action(async () => {
|
|
3979
|
+
const identity = loadRequiredIdentity();
|
|
3980
|
+
const wsUrl = identity.server_url.replace(/^http/, "ws").replace(/\/$/, "") + "/ws/daemon";
|
|
3981
|
+
console.log(chalk20.green(`Starting daemon ${identity.daemon_id} \u2192 ${wsUrl}`));
|
|
3982
|
+
let reconnectDelay = 1e3;
|
|
3983
|
+
let shouldRun = true;
|
|
3984
|
+
let activeWs = null;
|
|
3985
|
+
function connect() {
|
|
3986
|
+
const ws = new WebSocket(wsUrl, {
|
|
3987
|
+
headers: { authorization: `Bearer ${identity.token}` }
|
|
3988
|
+
});
|
|
3989
|
+
activeWs = ws;
|
|
3990
|
+
ws.on("open", () => {
|
|
3991
|
+
reconnectDelay = 1e3;
|
|
3992
|
+
console.log(chalk20.green(`[${(/* @__PURE__ */ new Date()).toISOString()}] connected`));
|
|
3993
|
+
const hello = {
|
|
3994
|
+
type: "hello",
|
|
3995
|
+
daemon_id: identity.daemon_id,
|
|
3996
|
+
version: DAEMON_VERSION,
|
|
3997
|
+
hostname: identity.hostname,
|
|
3998
|
+
platform: identity.platform
|
|
3999
|
+
};
|
|
4000
|
+
ws.send(JSON.stringify(hello));
|
|
4001
|
+
const projects = loadConfig().sources.filter((source2) => source2.type === "project").map((source2) => ({ name: source2.name, path: source2.path, enabled: source2.enabled }));
|
|
4002
|
+
const manifest = { type: "manifest", projects };
|
|
4003
|
+
ws.send(JSON.stringify(manifest));
|
|
4004
|
+
console.log(chalk20.dim(`pushed manifest: ${projects.length} project(s)`));
|
|
4005
|
+
});
|
|
4006
|
+
ws.on("message", (raw) => {
|
|
4007
|
+
let msg;
|
|
4008
|
+
try {
|
|
4009
|
+
msg = JSON.parse(raw.toString("utf-8"));
|
|
4010
|
+
} catch {
|
|
4011
|
+
return;
|
|
4012
|
+
}
|
|
4013
|
+
if (msg.type === "welcome") {
|
|
4014
|
+
console.log(chalk20.green(`welcomed ${msg.server_time}`));
|
|
4015
|
+
} else if (msg.type === "ping") {
|
|
4016
|
+
const beat = { type: "heartbeat", ts: (/* @__PURE__ */ new Date()).toISOString() };
|
|
4017
|
+
if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(beat));
|
|
4018
|
+
} else if (msg.type === "error") {
|
|
4019
|
+
console.error(chalk20.yellow(`server error: ${msg.message}`));
|
|
4020
|
+
} else if (msg.type === "rpc_request") {
|
|
4021
|
+
void dispatchRpc(ws, msg.id, msg.method, msg.params);
|
|
4022
|
+
}
|
|
4023
|
+
});
|
|
4024
|
+
ws.on("close", (code, reason) => {
|
|
4025
|
+
activeWs = null;
|
|
4026
|
+
const reasonText = reason.toString("utf-8") || "no reason";
|
|
4027
|
+
console.log(chalk20.yellow(`[${(/* @__PURE__ */ new Date()).toISOString()}] disconnected (code=${code}, ${reasonText})`));
|
|
4028
|
+
if (code === 4001) {
|
|
4029
|
+
console.error(chalk20.red("superseded by another daemon connection; exiting"));
|
|
4030
|
+
process.exit(0);
|
|
4031
|
+
}
|
|
4032
|
+
if (shouldRun) {
|
|
4033
|
+
setTimeout(connect, reconnectDelay);
|
|
4034
|
+
reconnectDelay = Math.min(reconnectDelay * 2, 3e4);
|
|
4035
|
+
}
|
|
4036
|
+
});
|
|
4037
|
+
ws.on("error", (err) => {
|
|
4038
|
+
console.error(chalk20.red(`ws error: ${err.message}`));
|
|
4039
|
+
});
|
|
4040
|
+
}
|
|
4041
|
+
const shutdown = () => {
|
|
4042
|
+
shouldRun = false;
|
|
4043
|
+
try {
|
|
4044
|
+
activeWs?.close(1e3, "shutdown");
|
|
4045
|
+
} catch {
|
|
4046
|
+
}
|
|
4047
|
+
setTimeout(() => process.exit(0), 200).unref();
|
|
4048
|
+
};
|
|
4049
|
+
process.on("SIGINT", shutdown);
|
|
4050
|
+
process.on("SIGTERM", shutdown);
|
|
4051
|
+
connect();
|
|
4052
|
+
await new Promise(() => {
|
|
4053
|
+
});
|
|
4054
|
+
});
|
|
4055
|
+
daemonCmd.command("list").description("List daemons registered on the configured server").action(async () => {
|
|
4056
|
+
const base = serverBase(readDaemonIdentity());
|
|
4057
|
+
const data = await jsonGet(`${base}/api/daemons`);
|
|
4058
|
+
if (data.daemons.length === 0) {
|
|
4059
|
+
console.log("No daemons registered.");
|
|
4060
|
+
return;
|
|
4061
|
+
}
|
|
4062
|
+
for (const d of data.daemons) {
|
|
4063
|
+
const status = d.status === "online" ? chalk20.green(d.status) : chalk20.dim(d.status);
|
|
4064
|
+
console.log(
|
|
4065
|
+
`${chalk20.bold(d.object_id)} ${d.name.padEnd(20)} ${d.hostname.padEnd(24)} ${d.platform.padEnd(8)} ${status}`
|
|
4066
|
+
);
|
|
4067
|
+
}
|
|
4068
|
+
});
|
|
4069
|
+
daemonCmd.command("show [daemonId]").description("Show local daemon identity (no arg) or remote daemon by id").action(async (daemonId) => {
|
|
4070
|
+
if (!daemonId) {
|
|
4071
|
+
const identity = loadRequiredIdentity();
|
|
4072
|
+
console.log(JSON.stringify({ ...identity, token: "***" }, null, 2));
|
|
4073
|
+
return;
|
|
4074
|
+
}
|
|
4075
|
+
const base = serverBase(readDaemonIdentity());
|
|
4076
|
+
const data = await jsonGet(`${base}/api/daemons/${encodeURIComponent(daemonId)}`);
|
|
4077
|
+
console.log(JSON.stringify(data.daemon, null, 2));
|
|
4078
|
+
});
|
|
4079
|
+
daemonCmd.command("logout").description("Forget the locally stored daemon identity").action(() => {
|
|
4080
|
+
if (clearDaemonIdentity()) {
|
|
4081
|
+
console.log(chalk20.green("Daemon identity cleared."));
|
|
4082
|
+
} else {
|
|
4083
|
+
console.log("No daemon identity to clear.");
|
|
4084
|
+
}
|
|
4085
|
+
});
|
|
4086
|
+
|
|
4087
|
+
// src/commands/error.ts
|
|
4088
|
+
init_node();
|
|
4089
|
+
import { Command as Command21 } from "commander";
|
|
4090
|
+
import chalk21 from "chalk";
|
|
4091
|
+
import readline from "readline";
|
|
4092
|
+
var error = new Command21("error").description("Browse and manage captured CLI error reports (local, ~/.task0/errors)");
|
|
4093
|
+
function truncate(s, n) {
|
|
4094
|
+
if (s.length <= n) return s;
|
|
4095
|
+
return s.slice(0, Math.max(0, n - 1)) + "\u2026";
|
|
4096
|
+
}
|
|
4097
|
+
function formatSize(bytes) {
|
|
4098
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
4099
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}K`;
|
|
4100
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
|
|
4101
|
+
}
|
|
4102
|
+
function printListTable(reports) {
|
|
4103
|
+
if (reports.length === 0) {
|
|
4104
|
+
console.log(chalk21.dim("No error reports."));
|
|
4105
|
+
return;
|
|
4106
|
+
}
|
|
4107
|
+
const idW = Math.max(...reports.map((r) => r.id.length), 12);
|
|
4108
|
+
for (const r of reports) {
|
|
4109
|
+
const id = r.id.padEnd(idW);
|
|
4110
|
+
const ts = r.captured_at.padEnd(24);
|
|
4111
|
+
const name = truncate(r.error_name || "-", 20).padEnd(20);
|
|
4112
|
+
const cmd = truncate(r.command || "-", 50).padEnd(50);
|
|
4113
|
+
const size = formatSize(r.size_bytes).padStart(6);
|
|
4114
|
+
console.log(`${chalk21.cyan(id)} ${chalk21.dim(ts)} ${name} ${cmd} ${chalk21.dim(size)}`);
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
error.command("ls").description("List captured error reports (newest first)").option("--json", "Output JSON envelope").option("--limit <n>", "Max rows to display", "50").action((opts) => {
|
|
4118
|
+
const result = listErrorReports();
|
|
4119
|
+
const limit = Math.max(0, Number(opts.limit) || 50);
|
|
4120
|
+
const reports = result.reports.slice(0, limit);
|
|
4121
|
+
if (opts.json) {
|
|
4122
|
+
console.log(JSON.stringify({ reports, skipped: result.skipped }, null, 2));
|
|
4123
|
+
return;
|
|
4124
|
+
}
|
|
4125
|
+
printListTable(reports);
|
|
4126
|
+
if (result.skipped.unreadable > 0 || result.skipped.unsupported_schema > 0) {
|
|
4127
|
+
console.log(
|
|
4128
|
+
chalk21.dim(
|
|
4129
|
+
` (skipped: unreadable=${result.skipped.unreadable}, unsupported_schema=${result.skipped.unsupported_schema})`
|
|
4130
|
+
)
|
|
4131
|
+
);
|
|
4132
|
+
}
|
|
4133
|
+
});
|
|
4134
|
+
function resolveOrFail(query) {
|
|
4135
|
+
const result = resolveErrorReport(query);
|
|
4136
|
+
if (result.kind === "hit") return result.summary;
|
|
4137
|
+
if (result.kind === "ambiguous") {
|
|
4138
|
+
const ids = result.candidates.map((c) => c.id).join(", ");
|
|
4139
|
+
console.error(chalk21.red(`ambiguous id prefix "${query}"; candidates: ${ids}`));
|
|
4140
|
+
process.exit(1);
|
|
4141
|
+
}
|
|
4142
|
+
console.error(chalk21.red(`no error report found matching "${query}"`));
|
|
4143
|
+
process.exit(1);
|
|
4144
|
+
}
|
|
4145
|
+
function printReportPretty(report) {
|
|
4146
|
+
console.log(`${chalk21.bold("id:")} ${report.id}`);
|
|
4147
|
+
console.log(`${chalk21.bold("captured_at:")} ${report.captured_at}`);
|
|
4148
|
+
console.log(`${chalk21.bold("origin:")} ${report.origin}`);
|
|
4149
|
+
console.log(`${chalk21.bold("task0:")} ${report.task0_version}`);
|
|
4150
|
+
console.log(`${chalk21.bold("node:")} ${report.node_version} ${report.platform}/${report.arch}`);
|
|
4151
|
+
console.log(`${chalk21.bold("pid/ppid:")} ${report.pid}/${report.ppid}`);
|
|
4152
|
+
console.log("");
|
|
4153
|
+
console.log(chalk21.bold("command:"));
|
|
4154
|
+
console.log(` ${report.command}`);
|
|
4155
|
+
console.log(` ${chalk21.dim("argv:")} ${JSON.stringify(report.argv)}`);
|
|
4156
|
+
console.log(` ${chalk21.dim("cwd:")} ${report.cwd}`);
|
|
4157
|
+
console.log("");
|
|
4158
|
+
console.log(chalk21.bold("error:"));
|
|
4159
|
+
console.log(` ${chalk21.red(report.error.name)}: ${report.error.message}`);
|
|
4160
|
+
if (report.error.exit_code !== null) {
|
|
4161
|
+
console.log(` ${chalk21.dim("exit_code:")} ${report.error.exit_code}`);
|
|
4162
|
+
}
|
|
4163
|
+
if (report.error.stack) {
|
|
4164
|
+
console.log(chalk21.dim("--- stack ---"));
|
|
4165
|
+
console.log(report.error.stack);
|
|
4166
|
+
}
|
|
4167
|
+
console.log("");
|
|
4168
|
+
if (report.git) {
|
|
4169
|
+
console.log(chalk21.bold("git:"));
|
|
4170
|
+
console.log(` root: ${report.git.root ?? "-"}`);
|
|
4171
|
+
console.log(` head: ${report.git.head ?? "-"}`);
|
|
4172
|
+
console.log(` branch: ${report.git.branch ?? "-"}`);
|
|
4173
|
+
console.log("");
|
|
4174
|
+
}
|
|
4175
|
+
const envKeys = Object.keys(report.env).sort();
|
|
4176
|
+
if (envKeys.length > 0) {
|
|
4177
|
+
console.log(chalk21.bold("env:"));
|
|
4178
|
+
for (const k of envKeys) {
|
|
4179
|
+
console.log(` ${k}=${report.env[k]}`);
|
|
4180
|
+
}
|
|
4181
|
+
console.log("");
|
|
4182
|
+
}
|
|
4183
|
+
console.log(chalk21.bold("redaction:"));
|
|
4184
|
+
console.log(` argv_redacted: ${report.redaction.argv_redacted}`);
|
|
4185
|
+
console.log(` message_redacted: ${report.redaction.message_redacted}`);
|
|
4186
|
+
console.log(` stack_redacted: ${report.redaction.stack_redacted}`);
|
|
4187
|
+
console.log(` env_strategy: ${report.redaction.env_strategy}`);
|
|
4188
|
+
}
|
|
4189
|
+
error.command("show <id>").description("Print a captured error report (full id or unique prefix)").option("--json", "Output the full JSON report").action((id, opts) => {
|
|
4190
|
+
const summary = resolveOrFail(id);
|
|
4191
|
+
const report = readErrorReport(summary);
|
|
4192
|
+
if (opts.json) {
|
|
4193
|
+
console.log(JSON.stringify(report, null, 2));
|
|
4194
|
+
return;
|
|
4195
|
+
}
|
|
4196
|
+
printReportPretty(report);
|
|
4197
|
+
});
|
|
4198
|
+
error.command("path <id>").description("Print the absolute path to a captured error report").action((id) => {
|
|
4199
|
+
const summary = resolveOrFail(id);
|
|
4200
|
+
console.log(summary.path);
|
|
4201
|
+
});
|
|
4202
|
+
var DURATION_RE = /^(\d+)(s|m|h|d)$/i;
|
|
4203
|
+
function parseDuration(input) {
|
|
4204
|
+
const m = DURATION_RE.exec(input.trim());
|
|
4205
|
+
if (!m) throw new Error(`invalid duration "${input}" (expected e.g. 7d, 12h, 30m, 120s)`);
|
|
4206
|
+
const n = Number(m[1]);
|
|
4207
|
+
const unit = m[2].toLowerCase();
|
|
4208
|
+
const mult = unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
|
|
4209
|
+
return n * mult;
|
|
4210
|
+
}
|
|
4211
|
+
async function confirmTty(message) {
|
|
4212
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
4213
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
4214
|
+
const answer = await new Promise((resolve) => {
|
|
4215
|
+
rl.question(message, resolve);
|
|
4216
|
+
});
|
|
4217
|
+
rl.close();
|
|
4218
|
+
return /^(y|yes)$/i.test(answer.trim());
|
|
4219
|
+
}
|
|
4220
|
+
error.command("prune").description("Remove captured error reports by age, count, or all").option("--older-than <duration>", "Remove reports older than duration (e.g. 7d, 12h, 30m, 120s)").option("--keep <n>", "Keep the newest N reports and remove the rest").option("--all", "Remove every captured report").option("--yes", "Skip confirmation prompt (required for --all on non-TTY)").option("--json", "Output JSON summary").action(async (opts) => {
|
|
4221
|
+
const hasAny = Boolean(opts.olderThan) || opts.keep !== void 0 || opts.all === true;
|
|
4222
|
+
if (!hasAny) {
|
|
4223
|
+
console.error(chalk21.red("prune: require at least one of --older-than, --keep, or --all"));
|
|
4224
|
+
process.exit(1);
|
|
4225
|
+
}
|
|
4226
|
+
if (opts.all && (opts.olderThan || opts.keep !== void 0)) {
|
|
4227
|
+
console.error(chalk21.red("prune: --all is mutually exclusive with --older-than / --keep"));
|
|
4228
|
+
process.exit(1);
|
|
4229
|
+
}
|
|
4230
|
+
if (opts.all && !opts.yes) {
|
|
4231
|
+
const ok = await confirmTty("Remove ALL captured error reports? [y/N] ");
|
|
4232
|
+
if (!ok) {
|
|
4233
|
+
console.error(chalk21.red("prune --all aborted (pass --yes to confirm non-interactively)."));
|
|
4234
|
+
process.exit(1);
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
4237
|
+
const olderThanMs = opts.olderThan ? parseDuration(opts.olderThan) : void 0;
|
|
4238
|
+
const keep = opts.keep !== void 0 ? Math.max(0, Number(opts.keep)) : void 0;
|
|
4239
|
+
const res = pruneErrorReports({
|
|
4240
|
+
olderThanMs,
|
|
4241
|
+
keep,
|
|
4242
|
+
all: opts.all === true
|
|
4243
|
+
});
|
|
4244
|
+
if (opts.json) {
|
|
4245
|
+
console.log(JSON.stringify({ removed: res.removed.length, kept: res.kept, paths: res.removed }, null, 2));
|
|
4246
|
+
return;
|
|
4247
|
+
}
|
|
4248
|
+
console.log(`Removed ${res.removed.length} report(s); ${res.kept} remaining.`);
|
|
4249
|
+
});
|
|
4250
|
+
|
|
4251
|
+
// src/core/error-capture.ts
|
|
4252
|
+
init_node();
|
|
4253
|
+
var DEFAULT_KEEP = 50;
|
|
4254
|
+
var DEFAULT_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
4255
|
+
var state = {
|
|
4256
|
+
captured: false,
|
|
4257
|
+
seen: /* @__PURE__ */ new WeakSet(),
|
|
4258
|
+
lastReportPath: null
|
|
4259
|
+
};
|
|
4260
|
+
function isCaptureDisabled() {
|
|
4261
|
+
return process.env.TASK0_DISABLE_ERROR_CAPTURE === "1";
|
|
4262
|
+
}
|
|
4263
|
+
var COMMANDER_BENIGN_CODES = /* @__PURE__ */ new Set([
|
|
4264
|
+
"commander.helpDisplayed",
|
|
4265
|
+
"commander.help",
|
|
4266
|
+
"commander.version",
|
|
4267
|
+
"commander.missingArgument",
|
|
4268
|
+
"commander.unknownOption",
|
|
4269
|
+
"commander.unknownCommand",
|
|
4270
|
+
"commander.missingMandatoryOptionValue",
|
|
4271
|
+
"commander.invalidArgument",
|
|
4272
|
+
"commander.excessArguments",
|
|
4273
|
+
"commander.optionMissingArgument",
|
|
4274
|
+
"commander.error"
|
|
4275
|
+
]);
|
|
4276
|
+
function isCommanderBenignError(err) {
|
|
4277
|
+
if (!err || typeof err !== "object") return false;
|
|
4278
|
+
const e = err;
|
|
4279
|
+
if (typeof e.code === "string" && COMMANDER_BENIGN_CODES.has(e.code)) return true;
|
|
4280
|
+
return false;
|
|
4281
|
+
}
|
|
4282
|
+
function captureAndWrite(ctx) {
|
|
4283
|
+
if (isCaptureDisabled()) return null;
|
|
4284
|
+
if (state.captured) return state.lastReportPath;
|
|
4285
|
+
if (ctx.error instanceof Error) {
|
|
4286
|
+
if (state.seen.has(ctx.error)) return state.lastReportPath;
|
|
4287
|
+
state.seen.add(ctx.error);
|
|
4288
|
+
}
|
|
4289
|
+
state.captured = true;
|
|
4290
|
+
try {
|
|
4291
|
+
const argv = ctx.options.argv ?? process.argv.slice(2);
|
|
4292
|
+
const cwd = ctx.options.cwd ?? process.cwd();
|
|
4293
|
+
const report = buildErrorReport({
|
|
4294
|
+
origin: ctx.origin,
|
|
4295
|
+
error: ctx.error,
|
|
4296
|
+
argv,
|
|
4297
|
+
cwd,
|
|
4298
|
+
task0Version: ctx.options.version,
|
|
4299
|
+
exitCode: ctx.exitCode ?? null
|
|
4300
|
+
});
|
|
4301
|
+
const { path: reportPath } = writeErrorReportSync(report);
|
|
4302
|
+
state.lastReportPath = reportPath;
|
|
4303
|
+
try {
|
|
4304
|
+
pruneErrorReports({ keep: DEFAULT_KEEP, olderThanMs: DEFAULT_MAX_AGE_MS });
|
|
4305
|
+
} catch {
|
|
4306
|
+
}
|
|
4307
|
+
return reportPath;
|
|
4308
|
+
} catch {
|
|
4309
|
+
return null;
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
4312
|
+
function normalizeReason(reason) {
|
|
4313
|
+
if (reason instanceof Error) return reason;
|
|
4314
|
+
const err = new Error(typeof reason === "string" ? reason : safeStringify2(reason));
|
|
4315
|
+
err.name = "UnhandledRejection";
|
|
4316
|
+
return err;
|
|
4317
|
+
}
|
|
4318
|
+
function safeStringify2(value) {
|
|
4319
|
+
try {
|
|
4320
|
+
return JSON.stringify(value);
|
|
4321
|
+
} catch {
|
|
4322
|
+
return String(value);
|
|
4323
|
+
}
|
|
4324
|
+
}
|
|
4325
|
+
var installed = false;
|
|
4326
|
+
function installErrorCapture(options) {
|
|
4327
|
+
if (installed) return;
|
|
4328
|
+
installed = true;
|
|
4329
|
+
if (isCaptureDisabled()) return;
|
|
4330
|
+
process.on("uncaughtExceptionMonitor", (err) => {
|
|
4331
|
+
captureAndWrite({ origin: "uncaught_exception", error: err, options });
|
|
4332
|
+
});
|
|
4333
|
+
process.on("unhandledRejection", (reason) => {
|
|
4334
|
+
const err = normalizeReason(reason);
|
|
4335
|
+
captureAndWrite({ origin: "unhandled_rejection", error: err, options });
|
|
4336
|
+
});
|
|
4337
|
+
}
|
|
4338
|
+
function captureTopLevel(err, options) {
|
|
4339
|
+
return captureAndWrite({
|
|
4340
|
+
origin: "top_level_catch",
|
|
4341
|
+
error: err,
|
|
4342
|
+
options,
|
|
4343
|
+
exitCode: 1
|
|
4344
|
+
});
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
// src/main.ts
|
|
4348
|
+
var TASK0_VERSION = "0.1.0";
|
|
4349
|
+
var program = new Command22().name("task0").description("Task-centric control layer for agent workflow").version(TASK0_VERSION);
|
|
4350
|
+
program.addCommand(source);
|
|
4351
|
+
program.addCommand(project);
|
|
4352
|
+
program.addCommand(task);
|
|
4353
|
+
program.addCommand(runtime);
|
|
4354
|
+
program.addCommand(plan);
|
|
4355
|
+
program.addCommand(okr);
|
|
4356
|
+
program.addCommand(note);
|
|
4357
|
+
program.addCommand(run);
|
|
4358
|
+
program.addCommand(ui);
|
|
4359
|
+
program.addCommand(serve);
|
|
4360
|
+
program.addCommand(models);
|
|
4361
|
+
program.addCommand(object);
|
|
4362
|
+
program.addCommand(issue);
|
|
4363
|
+
program.addCommand(agent);
|
|
4364
|
+
program.addCommand(daemonCmd);
|
|
4365
|
+
program.addCommand(error);
|
|
4366
|
+
async function main() {
|
|
4367
|
+
installErrorCapture({ version: TASK0_VERSION });
|
|
4368
|
+
try {
|
|
4369
|
+
await program.parseAsync(process.argv);
|
|
4370
|
+
} catch (err) {
|
|
4371
|
+
if (!isCommanderBenignError(err)) {
|
|
4372
|
+
captureTopLevel(err, { version: TASK0_VERSION });
|
|
4373
|
+
}
|
|
4374
|
+
throw err;
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
main().catch((err) => {
|
|
4378
|
+
const exitErr = err;
|
|
4379
|
+
if (exitErr && typeof exitErr.exitCode === "number") {
|
|
4380
|
+
process.exit(exitErr.exitCode);
|
|
4381
|
+
}
|
|
4382
|
+
const msg = err instanceof Error ? err.stack ?? err.message : String(err);
|
|
4383
|
+
console.error(msg);
|
|
4384
|
+
process.exit(1);
|
|
4385
|
+
});
|