@task0/cli 0.9.0 → 0.11.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/dist/main.js +793 -35
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -207,6 +207,8 @@ function scanProject(projectPath, sourceName) {
|
|
|
207
207
|
const summary = raw.summary || void 0;
|
|
208
208
|
const displayTitle = summary?.title || title;
|
|
209
209
|
const displayTags = summary?.tags ?? tags;
|
|
210
|
+
const agentRuns = Array.isArray(raw.agent_runs) ? raw.agent_runs.filter((v) => typeof v === "string") : void 0;
|
|
211
|
+
const runtimes = Array.isArray(raw.runtimes) ? raw.runtimes.filter((v) => typeof v === "string") : void 0;
|
|
210
212
|
tasks.push({
|
|
211
213
|
id,
|
|
212
214
|
object_id: objectId,
|
|
@@ -227,6 +229,8 @@ function scanProject(projectPath, sourceName) {
|
|
|
227
229
|
workflow: raw.workflow || void 0,
|
|
228
230
|
summary,
|
|
229
231
|
comments: raw.comments || void 0,
|
|
232
|
+
agent_runs: agentRuns,
|
|
233
|
+
runtimes,
|
|
230
234
|
display_title: displayTitle,
|
|
231
235
|
display_tags: displayTags
|
|
232
236
|
});
|
|
@@ -1905,11 +1909,11 @@ async function request(method, pathname, body) {
|
|
|
1905
1909
|
}
|
|
1906
1910
|
}
|
|
1907
1911
|
var api = {
|
|
1908
|
-
get: (
|
|
1909
|
-
post: (
|
|
1910
|
-
put: (
|
|
1911
|
-
patch: (
|
|
1912
|
-
del: (
|
|
1912
|
+
get: (path33) => request("GET", path33),
|
|
1913
|
+
post: (path33, body) => request("POST", path33, body ?? {}),
|
|
1914
|
+
put: (path33, body) => request("PUT", path33, body ?? {}),
|
|
1915
|
+
patch: (path33, body) => request("PATCH", path33, body ?? {}),
|
|
1916
|
+
del: (path33) => request("DELETE", path33)
|
|
1913
1917
|
};
|
|
1914
1918
|
|
|
1915
1919
|
// src/commands/task/triage.ts
|
|
@@ -4817,8 +4821,10 @@ function pickRegisterAuth(flagToken) {
|
|
|
4817
4821
|
|
|
4818
4822
|
// src/core/daemon-rpc-handlers.ts
|
|
4819
4823
|
init_node();
|
|
4824
|
+
import { execSync as execSync3 } from "child_process";
|
|
4820
4825
|
import fs28 from "fs";
|
|
4821
4826
|
import path26 from "path";
|
|
4827
|
+
import yaml10 from "js-yaml";
|
|
4822
4828
|
|
|
4823
4829
|
// src/core/daemon-agent-run-runner.ts
|
|
4824
4830
|
import { execFileSync } from "child_process";
|
|
@@ -5040,6 +5046,44 @@ function optionalBoolean(value) {
|
|
|
5040
5046
|
function listProjects() {
|
|
5041
5047
|
return loadConfig().sources.filter((source2) => source2.type === "project");
|
|
5042
5048
|
}
|
|
5049
|
+
function resolveTaskDir(params, opts = {}) {
|
|
5050
|
+
const projectName = ensureString(params.projectName, "projectName");
|
|
5051
|
+
const taskId = ensureSafeTaskId(params.taskId);
|
|
5052
|
+
const project2 = listProjects().find((p) => p.name === projectName);
|
|
5053
|
+
if (!project2) {
|
|
5054
|
+
throw Object.assign(new Error(`project not found: ${projectName}`), { code: "not_found" });
|
|
5055
|
+
}
|
|
5056
|
+
const projectAbs = path26.resolve(project2.path);
|
|
5057
|
+
const projectConfig = readYaml(path26.join(projectAbs, "task0.yml"));
|
|
5058
|
+
if (!projectConfig) {
|
|
5059
|
+
throw Object.assign(
|
|
5060
|
+
new Error(`invalid project: missing task0.yml at ${projectAbs}`),
|
|
5061
|
+
{ code: "invalid_project" }
|
|
5062
|
+
);
|
|
5063
|
+
}
|
|
5064
|
+
const tasksRoot = path26.resolve(projectAbs, projectConfig.tasks_dir);
|
|
5065
|
+
const taskDir = path26.resolve(tasksRoot, taskId);
|
|
5066
|
+
assertContained(tasksRoot, taskDir);
|
|
5067
|
+
const taskYml = path26.join(taskDir, "task0.yml");
|
|
5068
|
+
if (opts.requireExists !== false && !fs28.existsSync(taskDir)) {
|
|
5069
|
+
throw Object.assign(new Error(`task not found: ${taskId}`), { code: "not_found" });
|
|
5070
|
+
}
|
|
5071
|
+
return { projectName, taskId, projectAbs, tasksRoot, taskDir, taskYml };
|
|
5072
|
+
}
|
|
5073
|
+
async function withTaskYaml(ctx, resolved, mutate) {
|
|
5074
|
+
const outcome = await withTaskYamlLock(resolved.taskDir, async () => {
|
|
5075
|
+
const raw = readYaml(resolved.taskYml);
|
|
5076
|
+
if (!raw) {
|
|
5077
|
+
throw Object.assign(new Error(`task yaml missing or unreadable: ${resolved.taskYml}`), { code: "not_found" });
|
|
5078
|
+
}
|
|
5079
|
+
const { yaml: yaml12, result } = await mutate(raw);
|
|
5080
|
+
if (yaml12) writeYaml(resolved.taskYml, yaml12);
|
|
5081
|
+
const objectId = typeof (yaml12 ?? raw).object_id === "string" ? (yaml12 ?? raw).object_id : void 0;
|
|
5082
|
+
return { result, object_id: objectId, mutated: yaml12 !== void 0 };
|
|
5083
|
+
});
|
|
5084
|
+
if (outcome.mutated) ctx.notifyManifestChanged();
|
|
5085
|
+
return { result: outcome.result, object_id: outcome.object_id };
|
|
5086
|
+
}
|
|
5043
5087
|
function applyRepair(r) {
|
|
5044
5088
|
const raw = readYaml(r.taskYml);
|
|
5045
5089
|
if (!raw) return;
|
|
@@ -5166,12 +5210,12 @@ var rpcHandlers = {
|
|
|
5166
5210
|
throw Object.assign(new Error(`task not found: ${taskId}`), { code: "not_found" });
|
|
5167
5211
|
}
|
|
5168
5212
|
const taskYml = path26.join(taskDir, "task0.yml");
|
|
5169
|
-
const
|
|
5170
|
-
if (!
|
|
5213
|
+
const yaml12 = readYaml(taskYml);
|
|
5214
|
+
if (!yaml12) {
|
|
5171
5215
|
throw Object.assign(new Error(`task yaml missing or unreadable: ${taskYml}`), { code: "not_found" });
|
|
5172
5216
|
}
|
|
5173
5217
|
const files = fs28.readdirSync(taskDir).filter((name) => name !== "task0.yml");
|
|
5174
|
-
return { task_dir: taskDir, yaml:
|
|
5218
|
+
return { task_dir: taskDir, yaml: yaml12, files };
|
|
5175
5219
|
},
|
|
5176
5220
|
// Create a task directory + write task0.yml + write any additional named
|
|
5177
5221
|
// files. Hub decides the taskId and yaml content (slug/object_id are
|
|
@@ -5179,8 +5223,8 @@ var rpcHandlers = {
|
|
|
5179
5223
|
async task_create(params, ctx) {
|
|
5180
5224
|
const projectName = ensureString(params.projectName, "projectName");
|
|
5181
5225
|
const taskId = ensureSafeTaskId(params.taskId);
|
|
5182
|
-
const
|
|
5183
|
-
if (!
|
|
5226
|
+
const yaml12 = params.yaml;
|
|
5227
|
+
if (!yaml12 || typeof yaml12 !== "object") {
|
|
5184
5228
|
throw Object.assign(new Error("yaml (object) is required"), { code: "invalid_params" });
|
|
5185
5229
|
}
|
|
5186
5230
|
const files = params.files ?? {};
|
|
@@ -5200,7 +5244,7 @@ var rpcHandlers = {
|
|
|
5200
5244
|
throw Object.assign(new Error(`task already exists: ${taskId}`), { code: "already_exists" });
|
|
5201
5245
|
}
|
|
5202
5246
|
fs28.mkdirSync(taskDir, { recursive: true });
|
|
5203
|
-
writeYaml(path26.join(taskDir, "task0.yml"),
|
|
5247
|
+
writeYaml(path26.join(taskDir, "task0.yml"), yaml12);
|
|
5204
5248
|
for (const [name, content] of Object.entries(files)) {
|
|
5205
5249
|
if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
|
|
5206
5250
|
throw Object.assign(new Error(`invalid file name: ${name}`), { code: "invalid_params" });
|
|
@@ -5217,8 +5261,8 @@ var rpcHandlers = {
|
|
|
5217
5261
|
async task_write_yaml(params, ctx) {
|
|
5218
5262
|
const projectName = ensureString(params.projectName, "projectName");
|
|
5219
5263
|
const taskId = ensureSafeTaskId(params.taskId);
|
|
5220
|
-
const
|
|
5221
|
-
if (!
|
|
5264
|
+
const yaml12 = params.yaml;
|
|
5265
|
+
if (!yaml12 || typeof yaml12 !== "object") {
|
|
5222
5266
|
throw Object.assign(new Error("yaml (object) is required"), { code: "invalid_params" });
|
|
5223
5267
|
}
|
|
5224
5268
|
const project2 = listProjects().find((p) => p.name === projectName);
|
|
@@ -5237,7 +5281,7 @@ var rpcHandlers = {
|
|
|
5237
5281
|
if (!fs28.existsSync(taskYml)) {
|
|
5238
5282
|
throw Object.assign(new Error(`task yaml not found: ${taskYml}`), { code: "not_found" });
|
|
5239
5283
|
}
|
|
5240
|
-
writeYaml(taskYml,
|
|
5284
|
+
writeYaml(taskYml, yaml12);
|
|
5241
5285
|
ctx.notifyManifestChanged();
|
|
5242
5286
|
return { task_dir: taskDir };
|
|
5243
5287
|
},
|
|
@@ -5265,6 +5309,596 @@ var rpcHandlers = {
|
|
|
5265
5309
|
ctx.notifyManifestChanged();
|
|
5266
5310
|
return { task_dir: taskDir, deleted: true };
|
|
5267
5311
|
},
|
|
5312
|
+
// Archive a task directory: append it to `<tasks_dir>.tar`, then remove
|
|
5313
|
+
// the active directory. Returns the task's object_id so the hub can emit
|
|
5314
|
+
// a `task.archived` event. After the archive completes, notifyManifestChanged
|
|
5315
|
+
// re-pushes the daemon's manifest so the hub's daemon_tasks cache drops
|
|
5316
|
+
// the archived row.
|
|
5317
|
+
async task_archive(params, ctx) {
|
|
5318
|
+
const projectName = ensureString(params.projectName, "projectName");
|
|
5319
|
+
const taskId = ensureSafeTaskId(params.taskId);
|
|
5320
|
+
const project2 = listProjects().find((p) => p.name === projectName);
|
|
5321
|
+
if (!project2) {
|
|
5322
|
+
throw Object.assign(new Error(`project not found: ${projectName}`), { code: "not_found" });
|
|
5323
|
+
}
|
|
5324
|
+
const projectAbs = path26.resolve(project2.path);
|
|
5325
|
+
const projectConfig = readYaml(path26.join(projectAbs, "task0.yml"));
|
|
5326
|
+
if (!projectConfig) {
|
|
5327
|
+
throw Object.assign(new Error(`invalid project: missing task0.yml at ${projectAbs}`), { code: "invalid_project" });
|
|
5328
|
+
}
|
|
5329
|
+
const tasksRoot = path26.resolve(projectAbs, projectConfig.tasks_dir);
|
|
5330
|
+
const taskDir = path26.resolve(tasksRoot, taskId);
|
|
5331
|
+
assertContained(tasksRoot, taskDir);
|
|
5332
|
+
const tarFile = tasksRoot + ".tar";
|
|
5333
|
+
if (fs28.existsSync(tarFile) && !fs28.statSync(tarFile).isFile()) {
|
|
5334
|
+
throw Object.assign(
|
|
5335
|
+
new Error(`${tarFile} exists but is not a file. Remove it manually first.`),
|
|
5336
|
+
{ code: "invalid_target" }
|
|
5337
|
+
);
|
|
5338
|
+
}
|
|
5339
|
+
if (!fs28.existsSync(taskDir)) {
|
|
5340
|
+
if (fs28.existsSync(tarFile)) {
|
|
5341
|
+
const listing = execSync3(`tar -tf ${JSON.stringify(tarFile)}`).toString();
|
|
5342
|
+
const dirs = new Set(listing.split("\n").map((line) => line.split("/")[0]).filter(Boolean));
|
|
5343
|
+
if (dirs.has(taskId)) {
|
|
5344
|
+
return { task_dir: taskDir, already_archived: true };
|
|
5345
|
+
}
|
|
5346
|
+
}
|
|
5347
|
+
throw Object.assign(new Error(`task directory missing on disk: ${taskId}`), { code: "not_found" });
|
|
5348
|
+
}
|
|
5349
|
+
const taskYaml = readYaml(path26.join(taskDir, "task0.yml"));
|
|
5350
|
+
const objectId = typeof taskYaml?.object_id === "string" ? taskYaml.object_id : void 0;
|
|
5351
|
+
const tarFlag = fs28.existsSync(tarFile) ? "-rf" : "-cf";
|
|
5352
|
+
execSync3(`tar ${tarFlag} ${JSON.stringify(tarFile)} -C ${JSON.stringify(tasksRoot)} ${JSON.stringify(taskId)}`);
|
|
5353
|
+
fs28.rmSync(taskDir, { recursive: true });
|
|
5354
|
+
ctx.notifyManifestChanged();
|
|
5355
|
+
return { task_dir: taskDir, object_id: objectId, already_archived: false };
|
|
5356
|
+
},
|
|
5357
|
+
// Reverse of task_archive: extract `<taskId>` from `<tasks_dir>.tar`, then
|
|
5358
|
+
// rewrite the tar without it (or delete the tar if it becomes empty). Refuses
|
|
5359
|
+
// when an active directory already exists. Returns the extracted task's
|
|
5360
|
+
// object_id (read from the freshly-restored task0.yml) so the hub can emit
|
|
5361
|
+
// a `task.unarchived` event.
|
|
5362
|
+
async task_unarchive(params, ctx) {
|
|
5363
|
+
const resolved = resolveTaskDir(params, { requireExists: false });
|
|
5364
|
+
const { tasksRoot, taskDir, taskId, projectAbs } = resolved;
|
|
5365
|
+
const tarFile = tasksRoot + ".tar";
|
|
5366
|
+
if (!fs28.existsSync(tarFile)) {
|
|
5367
|
+
throw Object.assign(new Error("no archive found"), { code: "not_found" });
|
|
5368
|
+
}
|
|
5369
|
+
if (fs28.existsSync(taskDir)) {
|
|
5370
|
+
throw Object.assign(
|
|
5371
|
+
new Error(`task already exists in active tasks: ${taskId}`),
|
|
5372
|
+
{ code: "already_exists" }
|
|
5373
|
+
);
|
|
5374
|
+
}
|
|
5375
|
+
execSync3(`tar -xf ${JSON.stringify(tarFile)} -C ${JSON.stringify(tasksRoot)} ${JSON.stringify(taskId)}`);
|
|
5376
|
+
const listing = execSync3(`tar -tf ${JSON.stringify(tarFile)}`).toString();
|
|
5377
|
+
const remainingDirs = [
|
|
5378
|
+
...new Set(
|
|
5379
|
+
listing.split("\n").map((line) => line.split("/")[0]).filter((dir) => dir && dir !== taskId)
|
|
5380
|
+
)
|
|
5381
|
+
];
|
|
5382
|
+
if (remainingDirs.length === 0) {
|
|
5383
|
+
fs28.unlinkSync(tarFile);
|
|
5384
|
+
} else {
|
|
5385
|
+
const tmpTar = tarFile + ".tmp";
|
|
5386
|
+
const tmpDir = fs28.mkdtempSync(path26.join(projectAbs, ".task0", ".unarchive-"));
|
|
5387
|
+
try {
|
|
5388
|
+
execSync3(`tar -xf ${JSON.stringify(tarFile)} -C ${JSON.stringify(tmpDir)}`);
|
|
5389
|
+
fs28.rmSync(path26.join(tmpDir, taskId), { recursive: true, force: true });
|
|
5390
|
+
const dirs = fs28.readdirSync(tmpDir).filter((dir) => fs28.statSync(path26.join(tmpDir, dir)).isDirectory());
|
|
5391
|
+
if (dirs.length === 0) {
|
|
5392
|
+
fs28.unlinkSync(tarFile);
|
|
5393
|
+
} else {
|
|
5394
|
+
execSync3(
|
|
5395
|
+
`tar -cf ${JSON.stringify(tmpTar)} -C ${JSON.stringify(tmpDir)} ${dirs.map((dir) => JSON.stringify(dir)).join(" ")}`
|
|
5396
|
+
);
|
|
5397
|
+
fs28.renameSync(tmpTar, tarFile);
|
|
5398
|
+
}
|
|
5399
|
+
} finally {
|
|
5400
|
+
fs28.rmSync(tmpDir, { recursive: true, force: true });
|
|
5401
|
+
}
|
|
5402
|
+
}
|
|
5403
|
+
const taskYaml = readYaml(path26.join(taskDir, "task0.yml"));
|
|
5404
|
+
const objectId = typeof taskYaml?.object_id === "string" ? taskYaml.object_id : void 0;
|
|
5405
|
+
ctx.notifyManifestChanged();
|
|
5406
|
+
return { task_dir: taskDir, object_id: objectId };
|
|
5407
|
+
},
|
|
5408
|
+
// ---------------------------------------------------------------------
|
|
5409
|
+
// Task-yaml field mutations. Each handler is a single read-modify-write
|
|
5410
|
+
// transaction under the task's file lock, then re-pushes the manifest so
|
|
5411
|
+
// the hub's daemon_tasks cache catches up immediately. Hub controllers
|
|
5412
|
+
// call these instead of writing task0.yml directly.
|
|
5413
|
+
// ---------------------------------------------------------------------
|
|
5414
|
+
async task_link_issue(params, ctx) {
|
|
5415
|
+
const resolved = resolveTaskDir(params);
|
|
5416
|
+
const link = params.link;
|
|
5417
|
+
if (!link || typeof link !== "object") {
|
|
5418
|
+
throw Object.assign(new Error("link (object) is required"), { code: "invalid_params" });
|
|
5419
|
+
}
|
|
5420
|
+
const identifier = ensureString(link.identifier, "link.identifier");
|
|
5421
|
+
const { object_id } = await withTaskYaml(ctx, resolved, (raw) => {
|
|
5422
|
+
const linked = Array.isArray(raw.linked_issues) ? raw.linked_issues : [];
|
|
5423
|
+
if (linked.some((issue2) => issue2.identifier === identifier)) {
|
|
5424
|
+
return { result: { linked: false } };
|
|
5425
|
+
}
|
|
5426
|
+
linked.push({ ...link, linked_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
5427
|
+
raw.linked_issues = linked;
|
|
5428
|
+
return { yaml: raw, result: { linked: true } };
|
|
5429
|
+
});
|
|
5430
|
+
return { object_id, linked: true };
|
|
5431
|
+
},
|
|
5432
|
+
async task_unlink_issue(params, ctx) {
|
|
5433
|
+
const resolved = resolveTaskDir(params);
|
|
5434
|
+
const identifier = ensureString(params.identifier, "identifier");
|
|
5435
|
+
const { object_id } = await withTaskYaml(ctx, resolved, (raw) => {
|
|
5436
|
+
const linked = Array.isArray(raw.linked_issues) ? raw.linked_issues : [];
|
|
5437
|
+
const next = linked.filter((issue2) => issue2.identifier !== identifier);
|
|
5438
|
+
if (next.length === linked.length) {
|
|
5439
|
+
return { result: { removed: false } };
|
|
5440
|
+
}
|
|
5441
|
+
if (next.length === 0) delete raw.linked_issues;
|
|
5442
|
+
else raw.linked_issues = next;
|
|
5443
|
+
return { yaml: raw, result: { removed: true } };
|
|
5444
|
+
});
|
|
5445
|
+
return { object_id, removed: true };
|
|
5446
|
+
},
|
|
5447
|
+
async task_set_description(params, ctx) {
|
|
5448
|
+
const resolved = resolveTaskDir(params);
|
|
5449
|
+
const description2 = typeof params.description === "string" ? params.description : null;
|
|
5450
|
+
if (description2 === null) {
|
|
5451
|
+
throw Object.assign(new Error("description (string) is required"), { code: "invalid_params" });
|
|
5452
|
+
}
|
|
5453
|
+
const { object_id, result } = await withTaskYaml(ctx, resolved, (raw) => {
|
|
5454
|
+
const previous = typeof raw.description === "string" ? raw.description : void 0;
|
|
5455
|
+
if (description2.length === 0) delete raw.description;
|
|
5456
|
+
else raw.description = description2;
|
|
5457
|
+
return { yaml: raw, result: { previous } };
|
|
5458
|
+
});
|
|
5459
|
+
return { object_id, description: description2, previous: result.previous };
|
|
5460
|
+
},
|
|
5461
|
+
async task_set_summary(params, ctx) {
|
|
5462
|
+
const resolved = resolveTaskDir(params);
|
|
5463
|
+
const title = ensureString(params.title, "title");
|
|
5464
|
+
const tagsRaw = params.tags;
|
|
5465
|
+
if (!Array.isArray(tagsRaw) || tagsRaw.some((t) => typeof t !== "string")) {
|
|
5466
|
+
throw Object.assign(new Error("tags (string[]) is required"), { code: "invalid_params" });
|
|
5467
|
+
}
|
|
5468
|
+
const tags = tagsRaw;
|
|
5469
|
+
const { object_id } = await withTaskYaml(ctx, resolved, (raw) => {
|
|
5470
|
+
raw.title = title;
|
|
5471
|
+
raw.tags = [...tags];
|
|
5472
|
+
delete raw.summary;
|
|
5473
|
+
return { yaml: raw, result: null };
|
|
5474
|
+
});
|
|
5475
|
+
return { object_id, title, tags };
|
|
5476
|
+
},
|
|
5477
|
+
async task_set_metadata(params, ctx) {
|
|
5478
|
+
const resolved = resolveTaskDir(params);
|
|
5479
|
+
const patch = params.patch;
|
|
5480
|
+
if (!patch || typeof patch !== "object") {
|
|
5481
|
+
throw Object.assign(new Error("patch (object) is required"), { code: "invalid_params" });
|
|
5482
|
+
}
|
|
5483
|
+
const { object_id, result: metadata } = await withTaskYaml(
|
|
5484
|
+
ctx,
|
|
5485
|
+
resolved,
|
|
5486
|
+
(raw) => {
|
|
5487
|
+
const current = raw.metadata || {};
|
|
5488
|
+
const next = { ...current };
|
|
5489
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
5490
|
+
if (value === null) delete next[key];
|
|
5491
|
+
else if (typeof value === "string") next[key] = value;
|
|
5492
|
+
}
|
|
5493
|
+
if (Object.keys(next).length === 0) delete raw.metadata;
|
|
5494
|
+
else raw.metadata = next;
|
|
5495
|
+
return { yaml: raw, result: next };
|
|
5496
|
+
}
|
|
5497
|
+
);
|
|
5498
|
+
return { object_id, metadata };
|
|
5499
|
+
},
|
|
5500
|
+
async task_add_agent_run(params, ctx) {
|
|
5501
|
+
const resolved = resolveTaskDir(params);
|
|
5502
|
+
const agentRunObjectId = ensureString(params.agentRunObjectId, "agentRunObjectId");
|
|
5503
|
+
const { object_id } = await withTaskYaml(ctx, resolved, (raw) => {
|
|
5504
|
+
if (raw.kind !== "task") return { result: null };
|
|
5505
|
+
const fromCanonical = Array.isArray(raw.agent_runs) ? raw.agent_runs.filter((v) => typeof v === "string") : [];
|
|
5506
|
+
const fromLegacy = Array.isArray(raw.runtimes) ? raw.runtimes.filter((v) => typeof v === "string") : [];
|
|
5507
|
+
const merged = [.../* @__PURE__ */ new Set([...fromCanonical, ...fromLegacy])];
|
|
5508
|
+
if (!merged.includes(agentRunObjectId)) merged.push(agentRunObjectId);
|
|
5509
|
+
raw.agent_runs = merged;
|
|
5510
|
+
delete raw.runtimes;
|
|
5511
|
+
return { yaml: raw, result: null };
|
|
5512
|
+
});
|
|
5513
|
+
return { object_id };
|
|
5514
|
+
},
|
|
5515
|
+
async task_remove_agent_run(params, ctx) {
|
|
5516
|
+
const resolved = resolveTaskDir(params);
|
|
5517
|
+
const agentRunObjectId = ensureString(params.agentRunObjectId, "agentRunObjectId");
|
|
5518
|
+
const { object_id } = await withTaskYaml(ctx, resolved, (raw) => {
|
|
5519
|
+
if (raw.kind !== "task") return { result: null };
|
|
5520
|
+
const fromCanonical = Array.isArray(raw.agent_runs) ? raw.agent_runs.filter((v) => typeof v === "string") : [];
|
|
5521
|
+
const fromLegacy = Array.isArray(raw.runtimes) ? raw.runtimes.filter((v) => typeof v === "string") : [];
|
|
5522
|
+
const merged = [.../* @__PURE__ */ new Set([...fromCanonical, ...fromLegacy])].filter((id) => id !== agentRunObjectId);
|
|
5523
|
+
delete raw.runtimes;
|
|
5524
|
+
if (merged.length === 0) delete raw.agent_runs;
|
|
5525
|
+
else raw.agent_runs = merged;
|
|
5526
|
+
return { yaml: raw, result: null };
|
|
5527
|
+
});
|
|
5528
|
+
return { object_id };
|
|
5529
|
+
},
|
|
5530
|
+
// Append a comment (or a reply to parentCommentId). The hub provides the
|
|
5531
|
+
// pre-generated comment record (id, author, timestamps, body, metadata).
|
|
5532
|
+
// Daemon serializes it into task0.yml.comments under the file lock.
|
|
5533
|
+
async task_add_comment(params, ctx) {
|
|
5534
|
+
const resolved = resolveTaskDir(params);
|
|
5535
|
+
const comment2 = params.comment;
|
|
5536
|
+
if (!comment2 || typeof comment2 !== "object") {
|
|
5537
|
+
throw Object.assign(new Error("comment (object) is required"), { code: "invalid_params" });
|
|
5538
|
+
}
|
|
5539
|
+
const parentCommentId = typeof params.parentCommentId === "string" ? params.parentCommentId : void 0;
|
|
5540
|
+
const dedupeMetadata = params.dedupeMetadata;
|
|
5541
|
+
const { object_id, result } = await withTaskYaml(
|
|
5542
|
+
ctx,
|
|
5543
|
+
resolved,
|
|
5544
|
+
(raw) => {
|
|
5545
|
+
const comments = Array.isArray(raw.comments) ? raw.comments : [];
|
|
5546
|
+
if (dedupeMetadata && typeof dedupeMetadata === "object") {
|
|
5547
|
+
const dedupeEntries = Object.entries(dedupeMetadata);
|
|
5548
|
+
if (dedupeEntries.length > 0) {
|
|
5549
|
+
const matches = (m) => {
|
|
5550
|
+
if (!m) return false;
|
|
5551
|
+
return dedupeEntries.every(([k, v]) => m[k] === v);
|
|
5552
|
+
};
|
|
5553
|
+
const existing = comments.find((c) => {
|
|
5554
|
+
if (matches(c.metadata)) return true;
|
|
5555
|
+
const replies = Array.isArray(c.replies) ? c.replies : [];
|
|
5556
|
+
return replies.some((r) => matches(r.metadata));
|
|
5557
|
+
});
|
|
5558
|
+
if (existing) {
|
|
5559
|
+
const replies = Array.isArray(existing.replies) ? existing.replies : [];
|
|
5560
|
+
const matched = matches(existing.metadata) ? existing : replies.find((r) => matches(r.metadata));
|
|
5561
|
+
return { result: { comment: matched, skipped: true } };
|
|
5562
|
+
}
|
|
5563
|
+
}
|
|
5564
|
+
}
|
|
5565
|
+
if (parentCommentId) {
|
|
5566
|
+
const idx = comments.findIndex((c) => c.id === parentCommentId);
|
|
5567
|
+
if (idx < 0) {
|
|
5568
|
+
throw Object.assign(
|
|
5569
|
+
new Error(`parent comment ${parentCommentId} not found`),
|
|
5570
|
+
{ code: "not_found" }
|
|
5571
|
+
);
|
|
5572
|
+
}
|
|
5573
|
+
const parent = comments[idx];
|
|
5574
|
+
const replies = Array.isArray(parent.replies) ? parent.replies : [];
|
|
5575
|
+
const reply = { ...comment2, parent_comment_id: parent.id };
|
|
5576
|
+
const updated = [...comments];
|
|
5577
|
+
updated[idx] = { ...parent, replies: [...replies, reply] };
|
|
5578
|
+
raw.comments = updated;
|
|
5579
|
+
return { yaml: raw, result: { comment: reply, skipped: false } };
|
|
5580
|
+
}
|
|
5581
|
+
raw.comments = [...comments, comment2];
|
|
5582
|
+
return { yaml: raw, result: { comment: comment2, skipped: false } };
|
|
5583
|
+
}
|
|
5584
|
+
);
|
|
5585
|
+
return { object_id, comment: result.comment, skipped: result.skipped };
|
|
5586
|
+
},
|
|
5587
|
+
async task_update_comment(params, ctx) {
|
|
5588
|
+
const resolved = resolveTaskDir(params);
|
|
5589
|
+
const commentId = ensureString(params.commentId, "commentId");
|
|
5590
|
+
const patch = params.patch;
|
|
5591
|
+
if (!patch || typeof patch !== "object") {
|
|
5592
|
+
throw Object.assign(new Error("patch (object) is required"), { code: "invalid_params" });
|
|
5593
|
+
}
|
|
5594
|
+
const { object_id, result } = await withTaskYaml(
|
|
5595
|
+
ctx,
|
|
5596
|
+
resolved,
|
|
5597
|
+
(raw) => {
|
|
5598
|
+
const comments = Array.isArray(raw.comments) ? raw.comments : [];
|
|
5599
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5600
|
+
for (let i = 0; i < comments.length; i++) {
|
|
5601
|
+
if (comments[i].id === commentId) {
|
|
5602
|
+
const updated = { ...comments[i], ...patch, updated_at: now };
|
|
5603
|
+
const next = [...comments];
|
|
5604
|
+
next[i] = updated;
|
|
5605
|
+
raw.comments = next;
|
|
5606
|
+
return { yaml: raw, result: { comment: updated } };
|
|
5607
|
+
}
|
|
5608
|
+
const replies = Array.isArray(comments[i].replies) ? comments[i].replies : [];
|
|
5609
|
+
for (let j = 0; j < replies.length; j++) {
|
|
5610
|
+
if (replies[j].id === commentId) {
|
|
5611
|
+
const updatedReply = { ...replies[j], ...patch, updated_at: now };
|
|
5612
|
+
const nextReplies = [...replies];
|
|
5613
|
+
nextReplies[j] = updatedReply;
|
|
5614
|
+
const next = [...comments];
|
|
5615
|
+
next[i] = { ...comments[i], replies: nextReplies };
|
|
5616
|
+
raw.comments = next;
|
|
5617
|
+
return { yaml: raw, result: { comment: updatedReply } };
|
|
5618
|
+
}
|
|
5619
|
+
}
|
|
5620
|
+
}
|
|
5621
|
+
throw Object.assign(new Error(`comment ${commentId} not found`), { code: "not_found" });
|
|
5622
|
+
}
|
|
5623
|
+
);
|
|
5624
|
+
return { object_id, comment: result.comment };
|
|
5625
|
+
},
|
|
5626
|
+
async task_delete_comment(params, ctx) {
|
|
5627
|
+
const resolved = resolveTaskDir(params);
|
|
5628
|
+
const commentId = ensureString(params.commentId, "commentId");
|
|
5629
|
+
const { object_id, result } = await withTaskYaml(
|
|
5630
|
+
ctx,
|
|
5631
|
+
resolved,
|
|
5632
|
+
(raw) => {
|
|
5633
|
+
const comments = Array.isArray(raw.comments) ? raw.comments : [];
|
|
5634
|
+
for (let i = 0; i < comments.length; i++) {
|
|
5635
|
+
if (comments[i].id === commentId) {
|
|
5636
|
+
const deleted = comments[i];
|
|
5637
|
+
const next = comments.filter((_, idx) => idx !== i);
|
|
5638
|
+
if (next.length === 0) delete raw.comments;
|
|
5639
|
+
else raw.comments = next;
|
|
5640
|
+
return { yaml: raw, result: { deleted } };
|
|
5641
|
+
}
|
|
5642
|
+
const replies = Array.isArray(comments[i].replies) ? comments[i].replies : [];
|
|
5643
|
+
for (let j = 0; j < replies.length; j++) {
|
|
5644
|
+
if (replies[j].id === commentId) {
|
|
5645
|
+
const deleted = replies[j];
|
|
5646
|
+
const nextReplies = replies.filter((_, idx) => idx !== j);
|
|
5647
|
+
const next = [...comments];
|
|
5648
|
+
next[i] = { ...comments[i], replies: nextReplies };
|
|
5649
|
+
if (nextReplies.length === 0) delete next[i].replies;
|
|
5650
|
+
raw.comments = next;
|
|
5651
|
+
return { yaml: raw, result: { deleted } };
|
|
5652
|
+
}
|
|
5653
|
+
}
|
|
5654
|
+
}
|
|
5655
|
+
throw Object.assign(new Error(`comment ${commentId} not found`), { code: "not_found" });
|
|
5656
|
+
}
|
|
5657
|
+
);
|
|
5658
|
+
return { object_id, deleted: result.deleted };
|
|
5659
|
+
},
|
|
5660
|
+
// ---------------------------------------------------------------------
|
|
5661
|
+
// Task-directory file ops. task_write_file supports auto_index: { prefix }
|
|
5662
|
+
// for "next IDEA-NN.md" semantics — daemon scans the dir and picks the next
|
|
5663
|
+
// 2-digit number atomically (under taskDir lock) so concurrent saves don't
|
|
5664
|
+
// collide.
|
|
5665
|
+
// ---------------------------------------------------------------------
|
|
5666
|
+
async task_write_file(params, ctx) {
|
|
5667
|
+
const resolved = resolveTaskDir(params);
|
|
5668
|
+
const content = ensureString(params.content, "content");
|
|
5669
|
+
const explicitName = optionalString(params.name);
|
|
5670
|
+
const autoIndex = params.auto_index;
|
|
5671
|
+
if (!explicitName && !autoIndex) {
|
|
5672
|
+
throw Object.assign(new Error("either name or auto_index is required"), { code: "invalid_params" });
|
|
5673
|
+
}
|
|
5674
|
+
return withTaskYamlLock(resolved.taskDir, () => {
|
|
5675
|
+
let fileName;
|
|
5676
|
+
if (explicitName) {
|
|
5677
|
+
if (explicitName.includes("/") || explicitName.includes("\\") || explicitName === "." || explicitName === ".." || explicitName.startsWith(".")) {
|
|
5678
|
+
throw Object.assign(new Error(`invalid file name: ${explicitName}`), { code: "invalid_params" });
|
|
5679
|
+
}
|
|
5680
|
+
fileName = explicitName;
|
|
5681
|
+
} else {
|
|
5682
|
+
const prefix = ensureString(autoIndex.prefix, "auto_index.prefix");
|
|
5683
|
+
const suffix = autoIndex.suffix ?? ".md";
|
|
5684
|
+
const pattern = new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\d+)${suffix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`);
|
|
5685
|
+
const existing = fs28.readdirSync(resolved.taskDir);
|
|
5686
|
+
const maxIdx = existing.reduce((max, name) => {
|
|
5687
|
+
const m = name.match(pattern);
|
|
5688
|
+
if (!m) return max;
|
|
5689
|
+
const n = parseInt(m[1], 10);
|
|
5690
|
+
return n > max ? n : max;
|
|
5691
|
+
}, 0);
|
|
5692
|
+
const nextIdx = String(maxIdx + 1).padStart(2, "0");
|
|
5693
|
+
fileName = `${prefix}${nextIdx}${suffix}`;
|
|
5694
|
+
}
|
|
5695
|
+
const target = path26.resolve(resolved.taskDir, fileName);
|
|
5696
|
+
assertContained(resolved.taskDir, target);
|
|
5697
|
+
fs28.writeFileSync(target, content, "utf-8");
|
|
5698
|
+
ctx.notifyManifestChanged();
|
|
5699
|
+
return { file: fileName, path: target };
|
|
5700
|
+
});
|
|
5701
|
+
},
|
|
5702
|
+
async task_delete_file(params, ctx) {
|
|
5703
|
+
const resolved = resolveTaskDir(params);
|
|
5704
|
+
const fileName = ensureString(params.name, "name");
|
|
5705
|
+
if (fileName.includes("/") || fileName.includes("\\") || fileName === "." || fileName === ".." || fileName === "task0.yml") {
|
|
5706
|
+
throw Object.assign(new Error(`invalid file name: ${fileName}`), { code: "invalid_params" });
|
|
5707
|
+
}
|
|
5708
|
+
const target = path26.resolve(resolved.taskDir, fileName);
|
|
5709
|
+
assertContained(resolved.taskDir, target);
|
|
5710
|
+
if (!fs28.existsSync(target)) {
|
|
5711
|
+
throw Object.assign(new Error(`file not found: ${fileName}`), { code: "not_found" });
|
|
5712
|
+
}
|
|
5713
|
+
fs28.unlinkSync(target);
|
|
5714
|
+
ctx.notifyManifestChanged();
|
|
5715
|
+
return { deleted: true };
|
|
5716
|
+
},
|
|
5717
|
+
// Build the four-section context blob (yaml subset + IDEA + ISSUE + PLAN)
|
|
5718
|
+
// hub uses for summarize / agent run prompts. Reads files daemon-side so
|
|
5719
|
+
// remote projects work the same as local ones. Sections are truncated to
|
|
5720
|
+
// 4000 chars to keep prompts bounded.
|
|
5721
|
+
async task_build_context(params) {
|
|
5722
|
+
const resolved = resolveTaskDir(params);
|
|
5723
|
+
const SECTION_CHAR_LIMIT = 4e3;
|
|
5724
|
+
function truncate2(text, max) {
|
|
5725
|
+
if (text.length <= max) return text;
|
|
5726
|
+
return text.slice(0, max) + "\n\n[...truncated]";
|
|
5727
|
+
}
|
|
5728
|
+
function latestMatching(pattern) {
|
|
5729
|
+
const matches = fs28.readdirSync(resolved.taskDir).filter((n) => pattern.test(n)).sort();
|
|
5730
|
+
return matches.length === 0 ? null : matches[matches.length - 1];
|
|
5731
|
+
}
|
|
5732
|
+
function readOrEmpty(file) {
|
|
5733
|
+
try {
|
|
5734
|
+
return fs28.readFileSync(file, "utf-8");
|
|
5735
|
+
} catch {
|
|
5736
|
+
return "";
|
|
5737
|
+
}
|
|
5738
|
+
}
|
|
5739
|
+
let ymlSection = "";
|
|
5740
|
+
if (fs28.existsSync(resolved.taskYml)) {
|
|
5741
|
+
const raw = yaml10.load(fs28.readFileSync(resolved.taskYml, "utf-8"));
|
|
5742
|
+
if (raw) {
|
|
5743
|
+
const slim = {};
|
|
5744
|
+
for (const key of ["title", "tags", "status", "current_step", "linked_issues"]) {
|
|
5745
|
+
if (raw[key] !== void 0) slim[key] = raw[key];
|
|
5746
|
+
}
|
|
5747
|
+
ymlSection = yaml10.dump(slim, { lineWidth: 120 }).trim();
|
|
5748
|
+
}
|
|
5749
|
+
}
|
|
5750
|
+
const ideaFile = latestMatching(/^IDEA-\d+\.md$/);
|
|
5751
|
+
const ideaSection = ideaFile ? truncate2(readOrEmpty(path26.join(resolved.taskDir, ideaFile)), SECTION_CHAR_LIMIT) : "";
|
|
5752
|
+
let issueSection = "";
|
|
5753
|
+
const issueOverview = path26.join(resolved.taskDir, "ISSUE.md");
|
|
5754
|
+
if (fs28.existsSync(issueOverview)) {
|
|
5755
|
+
issueSection = truncate2(readOrEmpty(issueOverview), SECTION_CHAR_LIMIT);
|
|
5756
|
+
} else if (fs28.existsSync(resolved.taskDir)) {
|
|
5757
|
+
const details = fs28.readdirSync(resolved.taskDir).filter((n) => /^ISSUE-\d+\.md$/.test(n)).sort();
|
|
5758
|
+
if (details.length > 0) {
|
|
5759
|
+
const concatenated = details.map((name) => `## ${name}
|
|
5760
|
+
${readOrEmpty(path26.join(resolved.taskDir, name))}`).join("\n\n");
|
|
5761
|
+
issueSection = truncate2(concatenated, SECTION_CHAR_LIMIT);
|
|
5762
|
+
}
|
|
5763
|
+
}
|
|
5764
|
+
const refinedPlan = latestMatching(/^PLAN-\d+-refined\.md$/);
|
|
5765
|
+
const anyPlan = refinedPlan ?? latestMatching(/^PLAN-\d+.*\.md$/);
|
|
5766
|
+
const planSection = anyPlan ? truncate2(readOrEmpty(path26.join(resolved.taskDir, anyPlan)), SECTION_CHAR_LIMIT) : "";
|
|
5767
|
+
return { yaml: ymlSection, idea: ideaSection, issue: issueSection, plan: planSection };
|
|
5768
|
+
},
|
|
5769
|
+
// List files (non-yaml) in a task directory with size + mtime. Hub calls
|
|
5770
|
+
// this from GET /api/tasks/:taskId/files so the dashboard can render the
|
|
5771
|
+
// task-detail file list regardless of which daemon hosts the project.
|
|
5772
|
+
async task_list_files(params) {
|
|
5773
|
+
const resolved = resolveTaskDir(params);
|
|
5774
|
+
const entries = fs28.readdirSync(resolved.taskDir, { withFileTypes: true });
|
|
5775
|
+
const files = entries.filter((entry) => entry.isFile()).map((entry) => {
|
|
5776
|
+
const full = path26.join(resolved.taskDir, entry.name);
|
|
5777
|
+
const stat = fs28.statSync(full);
|
|
5778
|
+
return { name: entry.name, size: stat.size, modified: stat.mtime.toISOString(), path: full };
|
|
5779
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
5780
|
+
return { files };
|
|
5781
|
+
},
|
|
5782
|
+
// List archived tasks (one per top-level directory inside `<tasks_dir>.tar`)
|
|
5783
|
+
// along with a summary derived from each task's archived task0.yml + first
|
|
5784
|
+
// matching markdown file. Hub uses this to populate the archived-tasks view.
|
|
5785
|
+
async archive_list(params) {
|
|
5786
|
+
const projectName = ensureString(params.projectName, "projectName");
|
|
5787
|
+
const project2 = listProjects().find((p) => p.name === projectName);
|
|
5788
|
+
if (!project2) {
|
|
5789
|
+
throw Object.assign(new Error(`project not found: ${projectName}`), { code: "not_found" });
|
|
5790
|
+
}
|
|
5791
|
+
const projectAbs = path26.resolve(project2.path);
|
|
5792
|
+
const projectConfig = readYaml(path26.join(projectAbs, "task0.yml"));
|
|
5793
|
+
if (!projectConfig) {
|
|
5794
|
+
throw Object.assign(
|
|
5795
|
+
new Error(`invalid project: missing task0.yml at ${projectAbs}`),
|
|
5796
|
+
{ code: "invalid_project" }
|
|
5797
|
+
);
|
|
5798
|
+
}
|
|
5799
|
+
const tasksRoot = path26.resolve(projectAbs, projectConfig.tasks_dir);
|
|
5800
|
+
const tarFile = tasksRoot + ".tar";
|
|
5801
|
+
if (!fs28.existsSync(tarFile)) return { tasks: [] };
|
|
5802
|
+
const listing = execSync3(`tar -tf ${JSON.stringify(tarFile)}`).toString();
|
|
5803
|
+
const lines = listing.split("\n");
|
|
5804
|
+
const dirs = [...new Set(lines.map((line) => line.split("/")[0]).filter(Boolean))];
|
|
5805
|
+
const tasks = [];
|
|
5806
|
+
for (const dir of dirs) {
|
|
5807
|
+
try {
|
|
5808
|
+
const ymlContent = execSync3(
|
|
5809
|
+
`tar -xf ${JSON.stringify(tarFile)} -O ${JSON.stringify(dir + "/task0.yml")}`
|
|
5810
|
+
).toString();
|
|
5811
|
+
const raw = yaml10.load(ymlContent);
|
|
5812
|
+
if (!raw || raw.kind !== "task") continue;
|
|
5813
|
+
const mdFiles = lines.filter((line) => line.startsWith(dir + "/") && line.endsWith(".md"));
|
|
5814
|
+
let context = "";
|
|
5815
|
+
if (mdFiles.length > 0) {
|
|
5816
|
+
try {
|
|
5817
|
+
const md = execSync3(
|
|
5818
|
+
`tar -xf ${JSON.stringify(tarFile)} -O ${JSON.stringify(mdFiles[0])}`
|
|
5819
|
+
).toString();
|
|
5820
|
+
context = md.replace(/^---[\s\S]*?---\s*/, "").trim();
|
|
5821
|
+
} catch {
|
|
5822
|
+
}
|
|
5823
|
+
}
|
|
5824
|
+
tasks.push({
|
|
5825
|
+
id: raw.id || dir,
|
|
5826
|
+
object_id: raw.object_id,
|
|
5827
|
+
title: raw.title || dir,
|
|
5828
|
+
status: "archived",
|
|
5829
|
+
created_at: raw.created_at || "",
|
|
5830
|
+
context
|
|
5831
|
+
});
|
|
5832
|
+
} catch {
|
|
5833
|
+
}
|
|
5834
|
+
}
|
|
5835
|
+
return { tasks };
|
|
5836
|
+
},
|
|
5837
|
+
// List files inside a specific archived task. Returns name + size; mtime is
|
|
5838
|
+
// not preserved by tar's default flags so we omit it.
|
|
5839
|
+
async archive_list_task_files(params) {
|
|
5840
|
+
const projectName = ensureString(params.projectName, "projectName");
|
|
5841
|
+
const taskId = ensureSafeTaskId(params.taskId);
|
|
5842
|
+
const project2 = listProjects().find((p) => p.name === projectName);
|
|
5843
|
+
if (!project2) {
|
|
5844
|
+
throw Object.assign(new Error(`project not found: ${projectName}`), { code: "not_found" });
|
|
5845
|
+
}
|
|
5846
|
+
const projectAbs = path26.resolve(project2.path);
|
|
5847
|
+
const projectConfig = readYaml(path26.join(projectAbs, "task0.yml"));
|
|
5848
|
+
if (!projectConfig) {
|
|
5849
|
+
throw Object.assign(
|
|
5850
|
+
new Error(`invalid project: missing task0.yml at ${projectAbs}`),
|
|
5851
|
+
{ code: "invalid_project" }
|
|
5852
|
+
);
|
|
5853
|
+
}
|
|
5854
|
+
const tarFile = path26.resolve(projectAbs, projectConfig.tasks_dir) + ".tar";
|
|
5855
|
+
if (!fs28.existsSync(tarFile)) return { files: [] };
|
|
5856
|
+
const listing = execSync3(`tar -tvf ${JSON.stringify(tarFile)}`).toString();
|
|
5857
|
+
const prefix = taskId + "/";
|
|
5858
|
+
const files = listing.split("\n").filter((line) => {
|
|
5859
|
+
const parts = line.trim().split(/\s+/);
|
|
5860
|
+
const filePath = parts[parts.length - 1];
|
|
5861
|
+
return filePath && filePath.startsWith(prefix) && filePath !== prefix && !filePath.endsWith("/");
|
|
5862
|
+
}).map((line) => {
|
|
5863
|
+
const parts = line.trim().split(/\s+/);
|
|
5864
|
+
const filePath = parts[parts.length - 1];
|
|
5865
|
+
const size = parseInt(parts[2], 10) || 0;
|
|
5866
|
+
const name = filePath.slice(prefix.length);
|
|
5867
|
+
return { name, size, modified: "", path: filePath };
|
|
5868
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
5869
|
+
return { files };
|
|
5870
|
+
},
|
|
5871
|
+
// Read a specific file (path is relative to a directory inside the tar:
|
|
5872
|
+
// e.g., `<taskId>/task0.yml` or `<taskId>/IDEA-01.md`). Hub uses this for
|
|
5873
|
+
// the archived-task detail view.
|
|
5874
|
+
async archive_read_file(params) {
|
|
5875
|
+
const projectName = ensureString(params.projectName, "projectName");
|
|
5876
|
+
const archivePath = ensureString(params.path, "path");
|
|
5877
|
+
if (archivePath.includes("..") || archivePath.startsWith("/")) {
|
|
5878
|
+
throw Object.assign(new Error(`invalid archive path: ${archivePath}`), { code: "invalid_params" });
|
|
5879
|
+
}
|
|
5880
|
+
const project2 = listProjects().find((p) => p.name === projectName);
|
|
5881
|
+
if (!project2) {
|
|
5882
|
+
throw Object.assign(new Error(`project not found: ${projectName}`), { code: "not_found" });
|
|
5883
|
+
}
|
|
5884
|
+
const projectAbs = path26.resolve(project2.path);
|
|
5885
|
+
const projectConfig = readYaml(path26.join(projectAbs, "task0.yml"));
|
|
5886
|
+
if (!projectConfig) {
|
|
5887
|
+
throw Object.assign(
|
|
5888
|
+
new Error(`invalid project: missing task0.yml at ${projectAbs}`),
|
|
5889
|
+
{ code: "invalid_project" }
|
|
5890
|
+
);
|
|
5891
|
+
}
|
|
5892
|
+
const tasksRoot = path26.resolve(projectAbs, projectConfig.tasks_dir);
|
|
5893
|
+
const tarFile = tasksRoot + ".tar";
|
|
5894
|
+
if (!fs28.existsSync(tarFile)) {
|
|
5895
|
+
throw Object.assign(new Error("no archive found"), { code: "not_found" });
|
|
5896
|
+
}
|
|
5897
|
+
const content = execSync3(
|
|
5898
|
+
`tar -xOf ${JSON.stringify(tarFile)} ${JSON.stringify(archivePath)}`
|
|
5899
|
+
).toString("utf-8");
|
|
5900
|
+
return { content, path: archivePath };
|
|
5901
|
+
},
|
|
5268
5902
|
// ---------------------------------------------------------------------
|
|
5269
5903
|
// Agent run lifecycle (P3.A scaffolding — handlers reject until P3.B
|
|
5270
5904
|
// moves the actual spawn / tmux / supervise logic from hub's
|
|
@@ -5693,15 +6327,119 @@ function readCliVersion() {
|
|
|
5693
6327
|
return cached2;
|
|
5694
6328
|
}
|
|
5695
6329
|
|
|
6330
|
+
// src/core/project-watcher.ts
|
|
6331
|
+
init_node();
|
|
6332
|
+
import fs32 from "fs";
|
|
6333
|
+
import path31 from "path";
|
|
6334
|
+
var ProjectWatcher = class {
|
|
6335
|
+
perProject = /* @__PURE__ */ new Map();
|
|
6336
|
+
debounceTimers = /* @__PURE__ */ new Map();
|
|
6337
|
+
debounceMs;
|
|
6338
|
+
onChange;
|
|
6339
|
+
constructor(opts) {
|
|
6340
|
+
this.onChange = opts.onChange;
|
|
6341
|
+
this.debounceMs = opts.debounceMs ?? 300;
|
|
6342
|
+
}
|
|
6343
|
+
/**
|
|
6344
|
+
* Reconcile watchers against the desired project list. Idempotent — call it
|
|
6345
|
+
* whenever the daemon's project set changes (project_add / project_remove /
|
|
6346
|
+
* project_set_enabled RPCs, or on reconnect with the latest config).
|
|
6347
|
+
*/
|
|
6348
|
+
setProjects(projects) {
|
|
6349
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
6350
|
+
for (const p of projects) {
|
|
6351
|
+
if (p.enabled) wanted.set(p.name, p);
|
|
6352
|
+
}
|
|
6353
|
+
for (const [name, entry] of this.perProject) {
|
|
6354
|
+
const target = wanted.get(name);
|
|
6355
|
+
if (!target || path31.resolve(target.path) !== entry.rootPath) {
|
|
6356
|
+
this.stopProject(name);
|
|
6357
|
+
}
|
|
6358
|
+
}
|
|
6359
|
+
for (const [name, proj] of wanted) {
|
|
6360
|
+
if (!this.perProject.has(name)) {
|
|
6361
|
+
this.startProject(proj);
|
|
6362
|
+
}
|
|
6363
|
+
}
|
|
6364
|
+
}
|
|
6365
|
+
close() {
|
|
6366
|
+
for (const name of [...this.perProject.keys()]) this.stopProject(name);
|
|
6367
|
+
for (const [, t] of this.debounceTimers) clearTimeout(t);
|
|
6368
|
+
this.debounceTimers.clear();
|
|
6369
|
+
}
|
|
6370
|
+
startProject(proj) {
|
|
6371
|
+
const rootPath = path31.resolve(proj.path);
|
|
6372
|
+
const projectYml = path31.join(rootPath, "task0.yml");
|
|
6373
|
+
const cfg = readYaml(projectYml);
|
|
6374
|
+
if (!cfg || typeof cfg.tasks_dir !== "string" || cfg.tasks_dir.length === 0) {
|
|
6375
|
+
return;
|
|
6376
|
+
}
|
|
6377
|
+
const tasksDir = path31.resolve(rootPath, cfg.tasks_dir);
|
|
6378
|
+
const watchers = [];
|
|
6379
|
+
try {
|
|
6380
|
+
const w = fs32.watch(tasksDir, { recursive: true }, () => this.notify(proj.name));
|
|
6381
|
+
w.on("error", () => {
|
|
6382
|
+
});
|
|
6383
|
+
watchers.push(w);
|
|
6384
|
+
} catch {
|
|
6385
|
+
}
|
|
6386
|
+
try {
|
|
6387
|
+
const parentDir = path31.dirname(tasksDir);
|
|
6388
|
+
const tasksDirName = path31.basename(tasksDir);
|
|
6389
|
+
const tarName = tasksDirName + ".tar";
|
|
6390
|
+
const w = fs32.watch(parentDir, (_event, filename) => {
|
|
6391
|
+
if (!filename) return;
|
|
6392
|
+
if (filename === tasksDirName || filename === tarName) {
|
|
6393
|
+
this.notify(proj.name);
|
|
6394
|
+
}
|
|
6395
|
+
});
|
|
6396
|
+
w.on("error", () => {
|
|
6397
|
+
});
|
|
6398
|
+
watchers.push(w);
|
|
6399
|
+
} catch {
|
|
6400
|
+
}
|
|
6401
|
+
this.perProject.set(proj.name, { watchers, rootPath });
|
|
6402
|
+
}
|
|
6403
|
+
stopProject(name) {
|
|
6404
|
+
const entry = this.perProject.get(name);
|
|
6405
|
+
if (!entry) return;
|
|
6406
|
+
for (const w of entry.watchers) {
|
|
6407
|
+
try {
|
|
6408
|
+
w.close();
|
|
6409
|
+
} catch {
|
|
6410
|
+
}
|
|
6411
|
+
}
|
|
6412
|
+
this.perProject.delete(name);
|
|
6413
|
+
const timer = this.debounceTimers.get(name);
|
|
6414
|
+
if (timer) {
|
|
6415
|
+
clearTimeout(timer);
|
|
6416
|
+
this.debounceTimers.delete(name);
|
|
6417
|
+
}
|
|
6418
|
+
}
|
|
6419
|
+
notify(projectName) {
|
|
6420
|
+
const prev = this.debounceTimers.get(projectName);
|
|
6421
|
+
if (prev) clearTimeout(prev);
|
|
6422
|
+
const t = setTimeout(() => {
|
|
6423
|
+
this.debounceTimers.delete(projectName);
|
|
6424
|
+
try {
|
|
6425
|
+
this.onChange(projectName);
|
|
6426
|
+
} catch (err) {
|
|
6427
|
+
console.error(`project watcher onChange threw for ${projectName}:`, err);
|
|
6428
|
+
}
|
|
6429
|
+
}, this.debounceMs);
|
|
6430
|
+
this.debounceTimers.set(projectName, t);
|
|
6431
|
+
}
|
|
6432
|
+
};
|
|
6433
|
+
|
|
5696
6434
|
// src/commands/daemon.ts
|
|
5697
|
-
async function dispatchRpc(ws, id, method, params) {
|
|
6435
|
+
async function dispatchRpc(ws, id, method, params, notifyManifestChanged) {
|
|
5698
6436
|
const handler = rpcHandlers[method];
|
|
5699
6437
|
if (!handler) {
|
|
5700
6438
|
sendRpc(ws, { type: "rpc_error", id, error: { code: "unknown_method", message: `unknown method: ${method}` } });
|
|
5701
6439
|
return;
|
|
5702
6440
|
}
|
|
5703
6441
|
try {
|
|
5704
|
-
const ctx = { notifyManifestChanged
|
|
6442
|
+
const ctx = { notifyManifestChanged };
|
|
5705
6443
|
const result = await handler(params ?? {}, ctx);
|
|
5706
6444
|
sendRpc(ws, { type: "rpc_response", id, result });
|
|
5707
6445
|
} catch (error2) {
|
|
@@ -5748,6 +6486,9 @@ function buildManifest() {
|
|
|
5748
6486
|
}
|
|
5749
6487
|
return { type: "manifest", projects, tasks, scan_errors: scanErrors };
|
|
5750
6488
|
}
|
|
6489
|
+
function currentWatchedProjects() {
|
|
6490
|
+
return loadConfig().sources.filter((source2) => source2.type === "project").map((source2) => ({ name: source2.name, path: source2.path, enabled: source2.enabled }));
|
|
6491
|
+
}
|
|
5751
6492
|
function pushManifest(ws) {
|
|
5752
6493
|
if (ws.readyState !== ws.OPEN) return;
|
|
5753
6494
|
const manifest = buildManifest();
|
|
@@ -5965,11 +6706,21 @@ daemonCmd.command("run").description("Run the daemon WebSocket loop in foregroun
|
|
|
5965
6706
|
let reconnectDelay = 1e3;
|
|
5966
6707
|
let shouldRun = true;
|
|
5967
6708
|
let activeWs = null;
|
|
6709
|
+
let activeWatcher = null;
|
|
5968
6710
|
function connect() {
|
|
5969
6711
|
const ws = new WebSocket(wsUrl, {
|
|
5970
6712
|
headers: { authorization: `Bearer ${identity.token}` }
|
|
5971
6713
|
});
|
|
5972
6714
|
activeWs = ws;
|
|
6715
|
+
const watcher = new ProjectWatcher({
|
|
6716
|
+
onChange: () => pushManifest(ws),
|
|
6717
|
+
debounceMs: 300
|
|
6718
|
+
});
|
|
6719
|
+
activeWatcher = watcher;
|
|
6720
|
+
const notifyManifestChanged = () => {
|
|
6721
|
+
pushManifest(ws);
|
|
6722
|
+
watcher.setProjects(currentWatchedProjects());
|
|
6723
|
+
};
|
|
5973
6724
|
ws.on("open", () => {
|
|
5974
6725
|
reconnectDelay = 1e3;
|
|
5975
6726
|
console.log(chalk19.green(`[${(/* @__PURE__ */ new Date()).toISOString()}] connected`));
|
|
@@ -5995,6 +6746,7 @@ daemonCmd.command("run").description("Run the daemon WebSocket loop in foregroun
|
|
|
5995
6746
|
const manifest = buildManifest();
|
|
5996
6747
|
ws.send(JSON.stringify(manifest));
|
|
5997
6748
|
console.log(chalk19.dim(`pushed manifest: ${manifest.projects.length} project(s)`));
|
|
6749
|
+
watcher.setProjects(currentWatchedProjects());
|
|
5998
6750
|
const sink = {
|
|
5999
6751
|
send: (frame) => {
|
|
6000
6752
|
if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(frame));
|
|
@@ -6017,7 +6769,7 @@ daemonCmd.command("run").description("Run the daemon WebSocket loop in foregroun
|
|
|
6017
6769
|
} else if (msg.type === "error") {
|
|
6018
6770
|
console.error(chalk19.yellow(`server error: ${msg.message}`));
|
|
6019
6771
|
} else if (msg.type === "rpc_request") {
|
|
6020
|
-
void dispatchRpc(ws, msg.id, msg.method, msg.params);
|
|
6772
|
+
void dispatchRpc(ws, msg.id, msg.method, msg.params, notifyManifestChanged);
|
|
6021
6773
|
} else if (msg.type === "agent_run_resume_request") {
|
|
6022
6774
|
const sent = replayAfterRanges(msg.ranges);
|
|
6023
6775
|
if (sent > 0) {
|
|
@@ -6028,6 +6780,8 @@ daemonCmd.command("run").description("Run the daemon WebSocket loop in foregroun
|
|
|
6028
6780
|
ws.on("close", (code, reason) => {
|
|
6029
6781
|
activeWs = null;
|
|
6030
6782
|
bindAgentRunFrameSink(null);
|
|
6783
|
+
watcher.close();
|
|
6784
|
+
if (activeWatcher === watcher) activeWatcher = null;
|
|
6031
6785
|
const reasonText = reason.toString("utf-8") || "no reason";
|
|
6032
6786
|
console.log(chalk19.yellow(`[${(/* @__PURE__ */ new Date()).toISOString()}] disconnected (code=${code}, ${reasonText})`));
|
|
6033
6787
|
if (code === 4001) {
|
|
@@ -6045,6 +6799,10 @@ daemonCmd.command("run").description("Run the daemon WebSocket loop in foregroun
|
|
|
6045
6799
|
}
|
|
6046
6800
|
const shutdown = () => {
|
|
6047
6801
|
shouldRun = false;
|
|
6802
|
+
try {
|
|
6803
|
+
activeWatcher?.close();
|
|
6804
|
+
} catch {
|
|
6805
|
+
}
|
|
6048
6806
|
try {
|
|
6049
6807
|
activeWs?.close(1e3, "shutdown");
|
|
6050
6808
|
} catch {
|
|
@@ -6173,8 +6931,8 @@ function fail7(message, code = 1) {
|
|
|
6173
6931
|
console.error(chalk20.red(message));
|
|
6174
6932
|
process.exit(code);
|
|
6175
6933
|
}
|
|
6176
|
-
async function callServer(
|
|
6177
|
-
const url = `${serverBase2()}${
|
|
6934
|
+
async function callServer(path33, init = {}) {
|
|
6935
|
+
const url = `${serverBase2()}${path33}`;
|
|
6178
6936
|
let auth;
|
|
6179
6937
|
try {
|
|
6180
6938
|
auth = adminAuthHeader();
|
|
@@ -6679,11 +7437,11 @@ automation.command("runs <id>").description("List recent runs for an automation"
|
|
|
6679
7437
|
|
|
6680
7438
|
// src/commands/profile.ts
|
|
6681
7439
|
init_node();
|
|
6682
|
-
import
|
|
6683
|
-
import
|
|
7440
|
+
import fs33 from "fs";
|
|
7441
|
+
import path32 from "path";
|
|
6684
7442
|
import { Command as Command23 } from "commander";
|
|
6685
7443
|
import chalk23 from "chalk";
|
|
6686
|
-
import
|
|
7444
|
+
import yaml11 from "js-yaml";
|
|
6687
7445
|
var PROFILE_SCALAR_KEYS = ["api_url"];
|
|
6688
7446
|
function isProfileScalarKey(key) {
|
|
6689
7447
|
return PROFILE_SCALAR_KEYS.includes(key);
|
|
@@ -6693,19 +7451,19 @@ function fail9(msg) {
|
|
|
6693
7451
|
process.exit(1);
|
|
6694
7452
|
}
|
|
6695
7453
|
function readDaemonAt(dir) {
|
|
6696
|
-
const file =
|
|
6697
|
-
if (!
|
|
7454
|
+
const file = path32.join(dir, "daemon.json");
|
|
7455
|
+
if (!fs33.existsSync(file)) return null;
|
|
6698
7456
|
try {
|
|
6699
|
-
return JSON.parse(
|
|
7457
|
+
return JSON.parse(fs33.readFileSync(file, "utf-8"));
|
|
6700
7458
|
} catch {
|
|
6701
7459
|
return null;
|
|
6702
7460
|
}
|
|
6703
7461
|
}
|
|
6704
7462
|
function readProfileApiUrl(name) {
|
|
6705
|
-
const file =
|
|
6706
|
-
if (!
|
|
7463
|
+
const file = path32.join(profileDir(name), "config.yml");
|
|
7464
|
+
if (!fs33.existsSync(file)) return void 0;
|
|
6707
7465
|
try {
|
|
6708
|
-
const data =
|
|
7466
|
+
const data = yaml11.load(fs33.readFileSync(file, "utf-8"));
|
|
6709
7467
|
return data?.api_url;
|
|
6710
7468
|
} catch {
|
|
6711
7469
|
return void 0;
|
|
@@ -6734,10 +7492,10 @@ profile2.command("add <name>").description("Create a new profile directory (use
|
|
|
6734
7492
|
fail9(`"${DEFAULT_PROFILE_NAME}" is reserved; it always exists.`);
|
|
6735
7493
|
}
|
|
6736
7494
|
const dir = profileDir(name);
|
|
6737
|
-
if (
|
|
7495
|
+
if (fs33.existsSync(dir)) {
|
|
6738
7496
|
fail9(`Profile "${name}" already exists at ${dir}.`);
|
|
6739
7497
|
}
|
|
6740
|
-
|
|
7498
|
+
fs33.mkdirSync(dir, { recursive: true });
|
|
6741
7499
|
const prev = process.env.TASK0_PROFILE;
|
|
6742
7500
|
process.env.TASK0_PROFILE = name;
|
|
6743
7501
|
try {
|
|
@@ -6761,14 +7519,14 @@ profile2.command("remove <name>").description('Delete a profile directory (refus
|
|
|
6761
7519
|
fail9(`"${DEFAULT_PROFILE_NAME}" cannot be removed.`);
|
|
6762
7520
|
}
|
|
6763
7521
|
const dir = profileDir(name);
|
|
6764
|
-
if (!
|
|
7522
|
+
if (!fs33.existsSync(dir)) {
|
|
6765
7523
|
fail9(`Profile "${name}" not found.`);
|
|
6766
7524
|
}
|
|
6767
7525
|
const current = currentProfileName();
|
|
6768
7526
|
if (current === name && !opts.force) {
|
|
6769
7527
|
fail9(`Profile "${name}" is current. Re-run with --force to remove it.`);
|
|
6770
7528
|
}
|
|
6771
|
-
|
|
7529
|
+
fs33.rmSync(dir, { recursive: true, force: true });
|
|
6772
7530
|
if (current === name) {
|
|
6773
7531
|
writeCurrentProfile(null);
|
|
6774
7532
|
console.log(chalk23.dim('Current profile reset to "default".'));
|
|
@@ -6779,7 +7537,7 @@ profile2.command("use <name>").description("Set the current profile").action((na
|
|
|
6779
7537
|
if (!isValidProfileName(name)) {
|
|
6780
7538
|
fail9(`Invalid profile name "${name}".`);
|
|
6781
7539
|
}
|
|
6782
|
-
if (!
|
|
7540
|
+
if (!fs33.existsSync(profileDir(name))) {
|
|
6783
7541
|
const names = listProfileNames();
|
|
6784
7542
|
fail9(`Profile "${name}" not found. Available: ${names.join(", ")}.`);
|
|
6785
7543
|
}
|
|
@@ -6814,7 +7572,7 @@ profile2.command("show [name]").description("Show a profile's configuration and
|
|
|
6814
7572
|
fail9(`Invalid profile name "${target}".`);
|
|
6815
7573
|
}
|
|
6816
7574
|
const dir = profileDir(target);
|
|
6817
|
-
if (!
|
|
7575
|
+
if (!fs33.existsSync(dir)) {
|
|
6818
7576
|
fail9(`Profile "${target}" not found.`);
|
|
6819
7577
|
}
|
|
6820
7578
|
const current = currentProfileName();
|
|
@@ -6824,7 +7582,7 @@ profile2.command("show [name]").description("Show a profile's configuration and
|
|
|
6824
7582
|
const effective = envOverride ?? apiUrl;
|
|
6825
7583
|
console.log(`${chalk23.bold(target)}${current === target ? chalk23.green(" (current)") : ""}`);
|
|
6826
7584
|
console.log(` dir: ${dir}`);
|
|
6827
|
-
console.log(` config: ${
|
|
7585
|
+
console.log(` config: ${path32.join(dir, "config.yml")}`);
|
|
6828
7586
|
if (envOverride && apiUrl && envOverride !== apiUrl) {
|
|
6829
7587
|
console.log(` api_url: ${envOverride} ${chalk23.dim("(TASK0_API_URL env, overrides profile)")}`);
|
|
6830
7588
|
console.log(` ${chalk23.dim(`profile: ${apiUrl}`)}`);
|