@task0/cli 0.4.0 → 0.5.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.
Files changed (2) hide show
  1. package/dist/main.js +1439 -495
  2. package/package.json +12 -11
package/dist/main.js CHANGED
@@ -9,27 +9,38 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
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;
12
+ // ../../packages/shared/dist/types/task.js
13
+ function isActiveTaskStatus(status) {
14
+ return ACTIVE_TASK_STATUS_SET.has(status);
29
15
  }
30
- var init_yaml = __esm({
31
- "../../packages/shared/dist/node/yaml.js"() {
16
+ var TASK_STATUS_VALUES, ACTIVE_TASK_STATUSES, ATTENTION_TASK_STATUSES, ACTIVE_TASK_STATUS_SET, ATTENTION_TASK_STATUS_SET;
17
+ var init_task = __esm({
18
+ "../../packages/shared/dist/types/task.js"() {
32
19
  "use strict";
20
+ TASK_STATUS_VALUES = [
21
+ "todo",
22
+ "triaging",
23
+ "triaged",
24
+ "planning",
25
+ "planned",
26
+ "refining",
27
+ "refined",
28
+ "executing",
29
+ "blocked",
30
+ "done",
31
+ "archived"
32
+ ];
33
+ ACTIVE_TASK_STATUSES = [
34
+ "triaging",
35
+ "planning",
36
+ "refining",
37
+ "executing"
38
+ ];
39
+ ATTENTION_TASK_STATUSES = [
40
+ "blocked"
41
+ ];
42
+ ACTIVE_TASK_STATUS_SET = new Set(ACTIVE_TASK_STATUSES);
43
+ ATTENTION_TASK_STATUS_SET = new Set(ATTENTION_TASK_STATUSES);
33
44
  }
34
45
  });
35
46
 
@@ -105,65 +116,57 @@ var init_object_id = __esm({
105
116
  }
106
117
  });
107
118
 
108
- // ../../packages/shared/dist/types/task.js
109
- function isActiveTaskStatus(status) {
110
- return ACTIVE_TASK_STATUS_SET.has(status);
119
+ // ../../packages/shared/dist/node/yaml.js
120
+ import fs5 from "fs";
121
+ import path6 from "path";
122
+ import yaml3 from "js-yaml";
123
+ function readYaml(filePath) {
124
+ if (!fs5.existsSync(filePath))
125
+ return null;
126
+ const raw = fs5.readFileSync(filePath, "utf-8");
127
+ return yaml3.load(raw);
111
128
  }
112
- var TASK_STATUS_VALUES, ACTIVE_TASK_STATUSES, ATTENTION_TASK_STATUSES, ACTIVE_TASK_STATUS_SET, ATTENTION_TASK_STATUS_SET;
113
- var init_task = __esm({
114
- "../../packages/shared/dist/types/task.js"() {
129
+ function writeYaml(filePath, data) {
130
+ fs5.writeFileSync(filePath, yaml3.dump(data, { lineWidth: 120 }), "utf-8");
131
+ }
132
+ function readContext(taskDir) {
133
+ const files = fs5.readdirSync(taskDir).filter((f) => f.endsWith(".md"));
134
+ if (files.length === 0)
135
+ return void 0;
136
+ const content = fs5.readFileSync(path6.join(taskDir, files[0]), "utf-8");
137
+ const stripped = content.replace(/^---[\s\S]*?---\s*/, "");
138
+ return stripped.trim() || void 0;
139
+ }
140
+ var init_yaml = __esm({
141
+ "../../packages/shared/dist/node/yaml.js"() {
115
142
  "use strict";
116
- TASK_STATUS_VALUES = [
117
- "todo",
118
- "triaging",
119
- "triaged",
120
- "planning",
121
- "planned",
122
- "refining",
123
- "refined",
124
- "executing",
125
- "blocked",
126
- "done",
127
- "archived"
128
- ];
129
- ACTIVE_TASK_STATUSES = [
130
- "triaging",
131
- "planning",
132
- "refining",
133
- "executing"
134
- ];
135
- ATTENTION_TASK_STATUSES = [
136
- "blocked"
137
- ];
138
- ACTIVE_TASK_STATUS_SET = new Set(ACTIVE_TASK_STATUSES);
139
- ATTENTION_TASK_STATUS_SET = new Set(ATTENTION_TASK_STATUSES);
140
143
  }
141
144
  });
142
145
 
143
146
  // ../../packages/shared/dist/node/scanner.js
144
- import fs3 from "fs";
145
- import path3 from "path";
147
+ import fs6 from "fs";
148
+ import path7 from "path";
146
149
  function scanProject(projectPath, sourceName) {
147
- const absPath = path3.resolve(projectPath);
148
- const resolvedSourceName = sourceName ?? path3.basename(absPath);
150
+ const absPath = path7.resolve(projectPath);
151
+ const resolvedSourceName = sourceName ?? path7.basename(absPath);
149
152
  const errors = [];
150
153
  const tasks = [];
151
154
  const repairs = [];
152
- const projectYml = path3.join(absPath, "task0.yml");
155
+ const projectYml = path7.join(absPath, "task0.yml");
153
156
  const projectConfig = readYaml(projectYml);
154
157
  if (!projectConfig)
155
158
  return { tasks, errors: [`${projectYml} not found`], repairs };
156
159
  if (projectConfig.kind !== "project")
157
160
  return { tasks, errors: [`Invalid: kind="${projectConfig.kind}"`], repairs };
158
- const tasksDir = path3.join(absPath, projectConfig.tasks_dir);
159
- if (!fs3.existsSync(tasksDir))
161
+ const tasksDir = path7.join(absPath, projectConfig.tasks_dir);
162
+ if (!fs6.existsSync(tasksDir))
160
163
  return { tasks, errors: [`tasks_dir not found: ${tasksDir}`], repairs };
161
- const entries = fs3.readdirSync(tasksDir, { withFileTypes: true });
164
+ const entries = fs6.readdirSync(tasksDir, { withFileTypes: true });
162
165
  for (const entry of entries) {
163
166
  if (!entry.isDirectory())
164
167
  continue;
165
- const taskDir = path3.join(tasksDir, entry.name);
166
- const taskYml = path3.join(taskDir, "task0.yml");
168
+ const taskDir = path7.join(tasksDir, entry.name);
169
+ const taskYml = path7.join(taskDir, "task0.yml");
167
170
  const raw = readYaml(taskYml);
168
171
  if (!raw || raw.kind !== "task")
169
172
  continue;
@@ -199,7 +202,7 @@ function scanProject(projectPath, sourceName) {
199
202
  continue;
200
203
  }
201
204
  const context = readContext(taskDir);
202
- const stat = fs3.statSync(taskYml);
205
+ const stat = fs6.statSync(taskYml);
203
206
  const tags = raw.tags || [];
204
207
  const summary = raw.summary || void 0;
205
208
  const displayTitle = summary?.title || title;
@@ -242,18 +245,18 @@ var init_scanner = __esm({
242
245
  });
243
246
 
244
247
  // ../../packages/shared/dist/node/open-questions.js
245
- import fs4 from "fs";
246
- import path4 from "path";
248
+ import fs7 from "fs";
249
+ import path8 from "path";
247
250
  function readOpenQuestions(taskDir, issueFiles) {
248
251
  const result = [];
249
252
  for (const file of issueFiles) {
250
253
  const m = file.match(/^ISSUE-(\d+)\.md$/);
251
254
  if (!m)
252
255
  continue;
253
- const fullPath = path4.join(taskDir, file);
254
- if (!fs4.existsSync(fullPath))
256
+ const fullPath = path8.join(taskDir, file);
257
+ if (!fs7.existsSync(fullPath))
255
258
  continue;
256
- const md = fs4.readFileSync(fullPath, "utf-8");
259
+ const md = fs7.readFileSync(fullPath, "utf-8");
257
260
  const match = md.match(OPEN_QUESTIONS_SECTION_RE);
258
261
  if (!match)
259
262
  continue;
@@ -276,34 +279,34 @@ var init_open_questions = __esm({
276
279
  });
277
280
 
278
281
  // ../../packages/shared/dist/node/task-state.js
279
- import fs5 from "fs";
280
- import os from "os";
281
- import path5 from "path";
282
- import yaml3 from "js-yaml";
282
+ import fs8 from "fs";
283
+ import os3 from "os";
284
+ import path9 from "path";
285
+ import yaml4 from "js-yaml";
283
286
  function findProjectRoot(start = process.cwd()) {
284
- let dir = path5.resolve(start);
287
+ let dir = path9.resolve(start);
285
288
  while (true) {
286
- const ymlPath = path5.join(dir, "task0.yml");
287
- if (fs5.existsSync(ymlPath)) {
289
+ const ymlPath = path9.join(dir, "task0.yml");
290
+ if (fs8.existsSync(ymlPath)) {
288
291
  try {
289
- const raw = yaml3.load(fs5.readFileSync(ymlPath, "utf-8"));
292
+ const raw = yaml4.load(fs8.readFileSync(ymlPath, "utf-8"));
290
293
  if (raw?.kind === "project")
291
294
  return dir;
292
295
  } catch {
293
296
  }
294
297
  }
295
- const parent = path5.dirname(dir);
298
+ const parent = path9.dirname(dir);
296
299
  if (parent === dir)
297
300
  return null;
298
301
  dir = parent;
299
302
  }
300
303
  }
301
304
  function readProjectConfig(projectRoot) {
302
- return yaml3.load(fs5.readFileSync(path5.join(projectRoot, "task0.yml"), "utf-8"));
305
+ return yaml4.load(fs8.readFileSync(path9.join(projectRoot, "task0.yml"), "utf-8"));
303
306
  }
304
307
  function resolveTasksDir(projectRoot, projectConfig) {
305
308
  const cfg = projectConfig ?? readProjectConfig(projectRoot);
306
- return path5.isAbsolute(cfg.tasks_dir) ? cfg.tasks_dir : path5.join(projectRoot, cfg.tasks_dir);
309
+ return path9.isAbsolute(cfg.tasks_dir) ? cfg.tasks_dir : path9.join(projectRoot, cfg.tasks_dir);
307
310
  }
308
311
  function resolveTaskByObjectId(objectId, projectRoot) {
309
312
  const root = projectRoot ?? findProjectRoot();
@@ -314,16 +317,16 @@ function resolveTaskByObjectId(objectId, projectRoot) {
314
317
  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.`);
315
318
  }
316
319
  const tasksDir = resolveTasksDir(root);
317
- const entries = fs5.readdirSync(tasksDir, { withFileTypes: true });
320
+ const entries = fs8.readdirSync(tasksDir, { withFileTypes: true });
318
321
  for (const entry of entries) {
319
322
  if (!entry.isDirectory())
320
323
  continue;
321
- const taskDir = path5.join(tasksDir, entry.name);
322
- const taskYml = path5.join(taskDir, "task0.yml");
323
- if (!fs5.existsSync(taskYml))
324
+ const taskDir = path9.join(tasksDir, entry.name);
325
+ const taskYml = path9.join(taskDir, "task0.yml");
326
+ if (!fs8.existsSync(taskYml))
324
327
  continue;
325
328
  try {
326
- const raw = yaml3.load(fs5.readFileSync(taskYml, "utf-8"));
329
+ const raw = yaml4.load(fs8.readFileSync(taskYml, "utf-8"));
327
330
  if (raw && raw.object_id === objectId) {
328
331
  return { projectRoot: root, tasksDir, taskDir, taskYml };
329
332
  }
@@ -333,15 +336,15 @@ function resolveTaskByObjectId(objectId, projectRoot) {
333
336
  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.`);
334
337
  }
335
338
  function readTaskYaml(taskYml) {
336
- if (!fs5.existsSync(taskYml))
339
+ if (!fs8.existsSync(taskYml))
337
340
  throw new Error(`task0.yml not found: ${taskYml}`);
338
- return yaml3.load(fs5.readFileSync(taskYml, "utf-8")) || {};
341
+ return yaml4.load(fs8.readFileSync(taskYml, "utf-8")) || {};
339
342
  }
340
343
  function writeTaskYaml(taskYml, data) {
341
- fs5.writeFileSync(taskYml, yaml3.dump(data, { lineWidth: 120 }), "utf-8");
344
+ fs8.writeFileSync(taskYml, yaml4.dump(data, { lineWidth: 120 }), "utf-8");
342
345
  }
343
346
  function taskYamlLockPath(taskDir) {
344
- return path5.join(taskDir, TASK_YAML_LOCKFILE);
347
+ return path9.join(taskDir, TASK_YAML_LOCKFILE);
345
348
  }
346
349
  function isProcessAlive(pid) {
347
350
  try {
@@ -353,17 +356,17 @@ function isProcessAlive(pid) {
353
356
  }
354
357
  function readTaskYamlLockInfo(file) {
355
358
  try {
356
- return JSON.parse(fs5.readFileSync(file, "utf-8"));
359
+ return JSON.parse(fs8.readFileSync(file, "utf-8"));
357
360
  } catch {
358
361
  return null;
359
362
  }
360
363
  }
361
364
  function writeTaskYamlLockInfo(file, info) {
362
- const fd = fs5.openSync(file, "wx");
365
+ const fd = fs8.openSync(file, "wx");
363
366
  try {
364
- fs5.writeFileSync(fd, JSON.stringify(info, null, 2), "utf-8");
367
+ fs8.writeFileSync(fd, JSON.stringify(info, null, 2), "utf-8");
365
368
  } finally {
366
- fs5.closeSync(fd);
369
+ fs8.closeSync(fd);
367
370
  }
368
371
  }
369
372
  function sleep(ms) {
@@ -373,7 +376,7 @@ async function acquireTaskYamlLock(taskDir) {
373
376
  const file = taskYamlLockPath(taskDir);
374
377
  const info = {
375
378
  pid: process.pid,
376
- hostname: os.hostname(),
379
+ hostname: os3.hostname(),
377
380
  started_at: (/* @__PURE__ */ new Date()).toISOString()
378
381
  };
379
382
  const deadline = Date.now() + TASK_YAML_LOCK_TIMEOUT_MS;
@@ -389,7 +392,7 @@ async function acquireTaskYamlLock(taskDir) {
389
392
  const existing = readTaskYamlLockInfo(file);
390
393
  if (existing && existing.hostname === info.hostname && !isProcessAlive(existing.pid)) {
391
394
  try {
392
- fs5.unlinkSync(file);
395
+ fs8.unlinkSync(file);
393
396
  continue;
394
397
  } catch (err) {
395
398
  if (err.code !== "ENOENT") {
@@ -411,7 +414,7 @@ function releaseTaskYamlLock(taskDir, info) {
411
414
  return;
412
415
  }
413
416
  try {
414
- fs5.unlinkSync(file);
417
+ fs8.unlinkSync(file);
415
418
  } catch (err) {
416
419
  if (err.code !== "ENOENT") {
417
420
  throw err;
@@ -419,7 +422,7 @@ function releaseTaskYamlLock(taskDir, info) {
419
422
  }
420
423
  }
421
424
  async function withTaskYamlLock(taskDir, fn) {
422
- const key = path5.resolve(taskDir);
425
+ const key = path9.resolve(taskDir);
423
426
  const prev = yamlLocks.get(key) ?? Promise.resolve();
424
427
  const next = prev.then(async () => {
425
428
  const lock = await acquireTaskYamlLock(taskDir);
@@ -450,7 +453,7 @@ function readTaskWorkflow(taskYml) {
450
453
  return raw.workflow || {};
451
454
  }
452
455
  async function updateTaskWorkflow(taskYml, patch) {
453
- const taskDir = path5.dirname(taskYml);
456
+ const taskDir = path9.dirname(taskYml);
454
457
  return withTaskYamlLock(taskDir, () => {
455
458
  const raw = readTaskYaml(taskYml);
456
459
  const current = raw.workflow || {};
@@ -580,19 +583,19 @@ var init_redact = __esm({
580
583
  });
581
584
 
582
585
  // ../../packages/shared/dist/node/error-reports.js
583
- import fs6 from "fs";
584
- import os2 from "os";
585
- import path6 from "path";
586
+ import fs9 from "fs";
587
+ import os4 from "os";
588
+ import path10 from "path";
586
589
  import { spawnSync } from "child_process";
587
590
  import crypto from "crypto";
588
591
  function task0Home() {
589
592
  const override = process.env.TASK0_HOME;
590
593
  if (override && override.length > 0)
591
594
  return override;
592
- return path6.join(os2.homedir(), ".task0");
595
+ return path10.join(os4.homedir(), ".task0");
593
596
  }
594
597
  function errorsRoot() {
595
- return path6.join(task0Home(), "errors");
598
+ return path10.join(task0Home(), "errors");
596
599
  }
597
600
  function createErrorReportId() {
598
601
  return `err_${crypto.randomBytes(4).toString("hex")}`;
@@ -693,13 +696,13 @@ function buildErrorReport(input) {
693
696
  function writeErrorReportSync(report, rootOverride) {
694
697
  const root = rootOverride ?? errorsRoot();
695
698
  const dirName = errorReportDirName(new Date(report.captured_at), report.id);
696
- const dir = path6.join(root, dirName);
697
- fs6.mkdirSync(dir, { recursive: true });
698
- const finalPath = path6.join(dir, REPORT_FILENAME);
699
- const tmpPath = path6.join(dir, TMP_FILENAME);
699
+ const dir = path10.join(root, dirName);
700
+ fs9.mkdirSync(dir, { recursive: true });
701
+ const finalPath = path10.join(dir, REPORT_FILENAME);
702
+ const tmpPath = path10.join(dir, TMP_FILENAME);
700
703
  const json = JSON.stringify(report, null, 2);
701
- fs6.writeFileSync(tmpPath, json, "utf-8");
702
- fs6.renameSync(tmpPath, finalPath);
704
+ fs9.writeFileSync(tmpPath, json, "utf-8");
705
+ fs9.renameSync(tmpPath, finalPath);
703
706
  return { dir, path: finalPath };
704
707
  }
705
708
  function parseReportDir(name) {
@@ -716,7 +719,7 @@ function listErrorReports(rootOverride) {
716
719
  };
717
720
  let entries;
718
721
  try {
719
- entries = fs6.readdirSync(root, { withFileTypes: true });
722
+ entries = fs9.readdirSync(root, { withFileTypes: true });
720
723
  } catch (err) {
721
724
  if (err.code === "ENOENT")
722
725
  return result;
@@ -728,11 +731,11 @@ function listErrorReports(rootOverride) {
728
731
  const parsed = parseReportDir(entry.name);
729
732
  if (!parsed)
730
733
  continue;
731
- const dir = path6.join(root, entry.name);
732
- const file = path6.join(dir, REPORT_FILENAME);
734
+ const dir = path10.join(root, entry.name);
735
+ const file = path10.join(dir, REPORT_FILENAME);
733
736
  let raw;
734
737
  try {
735
- raw = fs6.readFileSync(file, "utf-8");
738
+ raw = fs9.readFileSync(file, "utf-8");
736
739
  } catch {
737
740
  result.skipped.unreadable += 1;
738
741
  continue;
@@ -750,7 +753,7 @@ function listErrorReports(rootOverride) {
750
753
  }
751
754
  let size = 0;
752
755
  try {
753
- size = fs6.statSync(file).size;
756
+ size = fs9.statSync(file).size;
754
757
  } catch {
755
758
  }
756
759
  result.reports.push({
@@ -785,7 +788,7 @@ function resolveErrorReport(query, rootOverride) {
785
788
  return { kind: "miss", query };
786
789
  }
787
790
  function readErrorReport(summary) {
788
- const raw = fs6.readFileSync(summary.path, "utf-8");
791
+ const raw = fs9.readFileSync(summary.path, "utf-8");
789
792
  return JSON.parse(raw);
790
793
  }
791
794
  function pruneErrorReports(opts, rootOverride) {
@@ -823,7 +826,7 @@ function pruneErrorReports(opts, rootOverride) {
823
826
  }
824
827
  function removeReportDir(dir) {
825
828
  try {
826
- fs6.rmSync(dir, { recursive: true, force: true });
829
+ fs9.rmSync(dir, { recursive: true, force: true });
827
830
  return true;
828
831
  } catch {
829
832
  return false;
@@ -841,9 +844,9 @@ var init_error_reports = __esm({
841
844
  });
842
845
 
843
846
  // ../../packages/shared/dist/node/file-lock.js
844
- import fs7 from "fs";
845
- import os3 from "os";
846
- import path7 from "path";
847
+ import fs10 from "fs";
848
+ import os5 from "os";
849
+ import path11 from "path";
847
850
  var init_file_lock = __esm({
848
851
  "../../packages/shared/dist/node/file-lock.js"() {
849
852
  "use strict";
@@ -858,6 +861,202 @@ var init_tmux = __esm({
858
861
  }
859
862
  });
860
863
 
864
+ // ../../packages/shared/dist/node/agent-skills.js
865
+ import fs11 from "fs";
866
+ import path12 from "path";
867
+ import yaml5 from "js-yaml";
868
+ function isRecord(value) {
869
+ return !!value && typeof value === "object" && !Array.isArray(value);
870
+ }
871
+ function extractFrontmatter(raw) {
872
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
873
+ if (!match)
874
+ return null;
875
+ try {
876
+ const parsed = yaml5.load(match[1]);
877
+ return isRecord(parsed) ? parsed : null;
878
+ } catch {
879
+ return null;
880
+ }
881
+ }
882
+ function readTextIfExists(filePath) {
883
+ try {
884
+ if (!fs11.existsSync(filePath))
885
+ return null;
886
+ return fs11.readFileSync(filePath, "utf-8");
887
+ } catch {
888
+ return null;
889
+ }
890
+ }
891
+ function getSymlinkInfo(...candidatePaths) {
892
+ for (const candidatePath of candidatePaths) {
893
+ try {
894
+ const stat = fs11.lstatSync(candidatePath);
895
+ if (!stat.isSymbolicLink())
896
+ continue;
897
+ const target = fs11.readlinkSync(candidatePath);
898
+ return {
899
+ isSymlink: true,
900
+ symlinkTarget: path12.isAbsolute(target) ? target : path12.resolve(path12.dirname(candidatePath), target)
901
+ };
902
+ } catch {
903
+ }
904
+ }
905
+ return { isSymlink: false };
906
+ }
907
+ function normalizeString(value) {
908
+ return typeof value === "string" ? value.trim() : "";
909
+ }
910
+ function normalizeGlobs(value) {
911
+ if (typeof value === "string") {
912
+ const glob = value.trim();
913
+ return glob ? [glob] : void 0;
914
+ }
915
+ if (Array.isArray(value)) {
916
+ const globs = value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
917
+ return globs.length > 0 ? globs : void 0;
918
+ }
919
+ return void 0;
920
+ }
921
+ function normalizeBoolean(value) {
922
+ if (typeof value === "boolean")
923
+ return value;
924
+ if (value === "true")
925
+ return true;
926
+ if (value === "false")
927
+ return false;
928
+ return void 0;
929
+ }
930
+ function sortSkills(skills) {
931
+ return skills.sort((a, b) => AGENT_ORDER[a.agent] - AGENT_ORDER[b.agent] || SCOPE_ORDER[a.scope] - SCOPE_ORDER[b.scope] || KIND_ORDER[a.kind] - KIND_ORDER[b.kind] || a.name.localeCompare(b.name) || a.filePath.localeCompare(b.filePath));
932
+ }
933
+ function pushInstructionIfExists(skills, agent2, scope, filePath, name = path12.basename(filePath), description2 = "", kind = "instruction") {
934
+ const raw = readTextIfExists(filePath);
935
+ if (raw === null)
936
+ return;
937
+ skills.push({
938
+ agent: agent2,
939
+ scope,
940
+ kind,
941
+ name,
942
+ description: description2,
943
+ filePath,
944
+ ...getSymlinkInfo(filePath)
945
+ });
946
+ }
947
+ function scanClaudeSkillDir(skills, skillsDir, scope) {
948
+ let entries = [];
949
+ try {
950
+ if (!fs11.existsSync(skillsDir))
951
+ return;
952
+ entries = fs11.readdirSync(skillsDir, { withFileTypes: true });
953
+ } catch {
954
+ return;
955
+ }
956
+ for (const entry of entries) {
957
+ if (!entry.isDirectory() && !entry.isSymbolicLink())
958
+ continue;
959
+ const skillDirPath = path12.join(skillsDir, entry.name);
960
+ const skillFilePath = path12.join(skillDirPath, "SKILL.md");
961
+ const raw = readTextIfExists(skillFilePath);
962
+ if (raw === null)
963
+ continue;
964
+ const frontmatter = extractFrontmatter(raw);
965
+ skills.push({
966
+ agent: "claude_code",
967
+ scope,
968
+ kind: "skill",
969
+ name: normalizeString(frontmatter?.name) || entry.name,
970
+ description: normalizeString(frontmatter?.description),
971
+ filePath: skillFilePath,
972
+ ...getSymlinkInfo(skillDirPath, skillFilePath)
973
+ });
974
+ }
975
+ }
976
+ function scanCursorRuleFile(skills, filePath, scope) {
977
+ const raw = readTextIfExists(filePath);
978
+ if (raw === null)
979
+ return;
980
+ const frontmatter = extractFrontmatter(raw);
981
+ const baseName = path12.basename(filePath, ".mdc");
982
+ skills.push({
983
+ agent: "cursor",
984
+ scope,
985
+ kind: "rule",
986
+ name: normalizeString(frontmatter?.name) || baseName,
987
+ description: normalizeString(frontmatter?.description),
988
+ filePath,
989
+ globs: normalizeGlobs(frontmatter?.globs),
990
+ alwaysApply: normalizeBoolean(frontmatter?.alwaysApply),
991
+ ...getSymlinkInfo(filePath)
992
+ });
993
+ }
994
+ function scanCursorRulesDir(skills, rulesDir, scope) {
995
+ let entries = [];
996
+ try {
997
+ if (!fs11.existsSync(rulesDir))
998
+ return;
999
+ entries = fs11.readdirSync(rulesDir, { withFileTypes: true });
1000
+ } catch {
1001
+ return;
1002
+ }
1003
+ for (const entry of entries) {
1004
+ if (!entry.name.endsWith(".mdc"))
1005
+ continue;
1006
+ if (!entry.isFile() && !entry.isSymbolicLink())
1007
+ continue;
1008
+ scanCursorRuleFile(skills, path12.join(rulesDir, entry.name), scope);
1009
+ }
1010
+ }
1011
+ function getProjectAgentSkills(projectPath) {
1012
+ const absProjectPath = path12.resolve(projectPath);
1013
+ let projectStat;
1014
+ try {
1015
+ projectStat = fs11.statSync(absProjectPath);
1016
+ } catch {
1017
+ return [];
1018
+ }
1019
+ if (!projectStat.isDirectory())
1020
+ return [];
1021
+ const skills = [];
1022
+ scanClaudeSkillDir(skills, path12.join(absProjectPath, ".claude", "skills"), "project");
1023
+ pushInstructionIfExists(skills, "claude_code", "project", path12.join(absProjectPath, "CLAUDE.md"));
1024
+ pushInstructionIfExists(skills, "claude_code", "project", path12.join(absProjectPath, "AGENTS.md"));
1025
+ pushInstructionIfExists(skills, "codex", "project", path12.join(absProjectPath, "AGENTS.md"));
1026
+ scanCursorRulesDir(skills, path12.join(absProjectPath, ".cursor", "rules"), "project");
1027
+ pushInstructionIfExists(skills, "cursor", "project", path12.join(absProjectPath, ".cursorrules"), "Legacy Cursor Rules", "", "rule");
1028
+ return sortSkills(skills);
1029
+ }
1030
+ function getGlobalAgentSkills() {
1031
+ const homeDir = process.env.HOME;
1032
+ if (!homeDir)
1033
+ return [];
1034
+ const skills = [];
1035
+ scanClaudeSkillDir(skills, path12.join(homeDir, ".claude", "skills"), "global");
1036
+ scanCursorRulesDir(skills, path12.join(homeDir, ".cursor", "rules"), "global");
1037
+ return sortSkills(skills);
1038
+ }
1039
+ var AGENT_ORDER, SCOPE_ORDER, KIND_ORDER;
1040
+ var init_agent_skills = __esm({
1041
+ "../../packages/shared/dist/node/agent-skills.js"() {
1042
+ "use strict";
1043
+ AGENT_ORDER = {
1044
+ claude_code: 0,
1045
+ codex: 1,
1046
+ cursor: 2
1047
+ };
1048
+ SCOPE_ORDER = {
1049
+ project: 0,
1050
+ global: 1
1051
+ };
1052
+ KIND_ORDER = {
1053
+ skill: 0,
1054
+ rule: 1,
1055
+ instruction: 2
1056
+ };
1057
+ }
1058
+ });
1059
+
861
1060
  // ../../packages/shared/dist/node/index.js
862
1061
  var init_node = __esm({
863
1062
  "../../packages/shared/dist/node/index.js"() {
@@ -870,6 +1069,7 @@ var init_node = __esm({
870
1069
  init_redact();
871
1070
  init_file_lock();
872
1071
  init_tmux();
1072
+ init_agent_skills();
873
1073
  init_error_report();
874
1074
  }
875
1075
  });
@@ -889,7 +1089,7 @@ __export(task_state_exports, {
889
1089
  withTaskYamlLock: () => withTaskYamlLock,
890
1090
  writeTaskYaml: () => writeTaskYaml
891
1091
  });
892
- import fs9 from "fs";
1092
+ import fs12 from "fs";
893
1093
  function readWorkflow(taskYml) {
894
1094
  return readTaskWorkflow(taskYml);
895
1095
  }
@@ -898,14 +1098,14 @@ async function updateWorkflow(taskYml, patch) {
898
1098
  }
899
1099
  function nextArtifactIndex(taskDir, prefix, ext = "md") {
900
1100
  const pattern = new RegExp(`^${prefix}-(\\d+).*\\.${ext}$`);
901
- const entries = fs9.readdirSync(taskDir);
1101
+ const entries = fs12.readdirSync(taskDir);
902
1102
  const indices = entries.map((name) => name.match(pattern)?.[1]).filter((v) => v != null).map(Number);
903
1103
  const next = indices.length > 0 ? Math.max(...indices) + 1 : 1;
904
1104
  return String(next).padStart(2, "0");
905
1105
  }
906
1106
  function latestArtifact(taskDir, pattern) {
907
- if (!fs9.existsSync(taskDir)) return null;
908
- const matches = fs9.readdirSync(taskDir).filter((name) => pattern.test(name));
1107
+ if (!fs12.existsSync(taskDir)) return null;
1108
+ const matches = fs12.readdirSync(taskDir).filter((name) => pattern.test(name));
909
1109
  if (matches.length === 0) return null;
910
1110
  matches.sort();
911
1111
  return matches[matches.length - 1] || null;
@@ -918,153 +1118,292 @@ var init_task_state2 = __esm({
918
1118
  });
919
1119
 
920
1120
  // src/main.ts
921
- import { readFileSync } from "fs";
922
- import { fileURLToPath as fileURLToPath2 } from "url";
923
- import path25 from "path";
924
1121
  import { Command as Command23 } from "commander";
925
1122
 
926
1123
  // src/commands/source.ts
927
1124
  import { Command } from "commander";
928
- import path8 from "path";
1125
+ import path3 from "path";
929
1126
  import chalk from "chalk";
930
1127
 
931
- // src/core/config.ts
1128
+ // src/types.ts
1129
+ init_task();
1130
+
1131
+ // src/core/admin-token.ts
932
1132
  import fs from "fs";
1133
+ import os from "os";
933
1134
  import path from "path";
934
- import yaml from "js-yaml";
935
- var CONFIG_DIR = path.join(
936
- process.env.HOME || process.env.USERPROFILE || "~",
937
- ".config",
938
- "task0"
939
- );
940
- var CONFIG_FILE = path.join(CONFIG_DIR, "config.yml");
941
- function ensureConfigDir() {
942
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
1135
+ var CONFIG_DIR = path.join(os.homedir(), ".config", "task0");
1136
+ var TOKEN_FILE = path.join(CONFIG_DIR, "admin.token");
1137
+ var cached = null;
1138
+ var AdminTokenUnavailableError = class extends Error {
1139
+ constructor() {
1140
+ super(
1141
+ `Admin token not found.
1142
+ \u2022 If the task0 server runs on this host, run the task0-server binary once \u2014 the token will be generated at ${TOKEN_FILE}.
1143
+ \u2022 Otherwise, copy the token from the server host and set TASK0_ADMIN_TOKEN.`
1144
+ );
1145
+ this.name = "AdminTokenUnavailableError";
1146
+ }
1147
+ };
1148
+ function readAdminToken() {
1149
+ if (cached) return cached;
1150
+ const fromEnv = process.env.TASK0_ADMIN_TOKEN?.trim();
1151
+ if (fromEnv) {
1152
+ cached = fromEnv;
1153
+ return cached;
1154
+ }
1155
+ if (fs.existsSync(TOKEN_FILE)) {
1156
+ const v = fs.readFileSync(TOKEN_FILE, "utf-8").trim();
1157
+ if (v) {
1158
+ cached = v;
1159
+ return cached;
1160
+ }
1161
+ }
1162
+ throw new AdminTokenUnavailableError();
943
1163
  }
944
- function defaultConfig() {
945
- return { sources: [] };
1164
+ function adminAuthHeader() {
1165
+ return { authorization: `Bearer ${readAdminToken()}` };
946
1166
  }
947
- function loadConfig() {
948
- if (!fs.existsSync(CONFIG_FILE)) return defaultConfig();
949
- const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
950
- const data = yaml.load(raw);
951
- return {
952
- ...data ?? {},
953
- sources: Array.isArray(data?.sources) ? data.sources : []
954
- };
1167
+
1168
+ // src/core/daemon-config.ts
1169
+ import fs2 from "fs";
1170
+ import os2 from "os";
1171
+ import path2 from "path";
1172
+ var CONFIG_DIR2 = path2.join(os2.homedir(), ".config", "task0");
1173
+ var CONFIG_FILE = path2.join(CONFIG_DIR2, "daemon.json");
1174
+ function daemonConfigPath() {
1175
+ return CONFIG_FILE;
955
1176
  }
956
- function saveConfig(config) {
957
- ensureConfigDir();
958
- fs.writeFileSync(CONFIG_FILE, yaml.dump(config, { lineWidth: 120 }), "utf-8");
1177
+ function readDaemonIdentity() {
1178
+ if (!fs2.existsSync(CONFIG_FILE)) return null;
1179
+ try {
1180
+ const raw = fs2.readFileSync(CONFIG_FILE, "utf-8");
1181
+ return JSON.parse(raw);
1182
+ } catch {
1183
+ return null;
1184
+ }
959
1185
  }
960
- function addSource(entry) {
961
- const config = loadConfig();
962
- const existing = config.sources.findIndex((s) => s.name === entry.name);
963
- if (existing >= 0) {
964
- config.sources[existing] = entry;
965
- } else {
966
- config.sources.push(entry);
1186
+ function writeDaemonIdentity(identity) {
1187
+ fs2.mkdirSync(CONFIG_DIR2, { recursive: true });
1188
+ fs2.writeFileSync(CONFIG_FILE, JSON.stringify(identity, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
1189
+ try {
1190
+ fs2.chmodSync(CONFIG_FILE, 384);
1191
+ } catch {
967
1192
  }
968
- saveConfig(config);
969
1193
  }
970
- function removeSource(name) {
971
- const config = loadConfig();
972
- const idx = config.sources.findIndex((s) => s.name === name);
973
- if (idx < 0) return false;
974
- config.sources.splice(idx, 1);
975
- saveConfig(config);
1194
+ function clearDaemonIdentity() {
1195
+ if (!fs2.existsSync(CONFIG_FILE)) return false;
1196
+ fs2.unlinkSync(CONFIG_FILE);
976
1197
  return true;
977
1198
  }
978
- function getSource(name) {
979
- return loadConfig().sources.find((s) => s.name === name);
980
- }
981
-
982
- // src/commands/source.ts
983
- init_node();
984
1199
 
985
- // src/types.ts
986
- init_task();
1200
+ // src/core/hub-client.ts
1201
+ var HubUnreachableError = class extends Error {
1202
+ constructor(url, cause) {
1203
+ super(`Cannot reach hub at ${url}: ${cause instanceof Error ? cause.message : String(cause)}`);
1204
+ this.name = "HubUnreachableError";
1205
+ }
1206
+ };
1207
+ var HubResponseError = class extends Error {
1208
+ status;
1209
+ code;
1210
+ body;
1211
+ constructor(status, body, code) {
1212
+ super(`Hub returned ${status}: ${body}`);
1213
+ this.name = "HubResponseError";
1214
+ this.status = status;
1215
+ this.code = code;
1216
+ this.body = body;
1217
+ }
1218
+ };
1219
+ function resolveHubUrl() {
1220
+ const fromEnv = process.env.TASK0_API_URL?.trim();
1221
+ if (fromEnv) return fromEnv.replace(/\/$/, "");
1222
+ const identity = readDaemonIdentity();
1223
+ if (identity?.server_url) return identity.server_url.replace(/\/$/, "");
1224
+ return "http://127.0.0.1:4318";
1225
+ }
1226
+ function resolveAuthHeader() {
1227
+ const apiToken = process.env.TASK0_API_TOKEN?.trim();
1228
+ if (apiToken) return { authorization: `Bearer ${apiToken}` };
1229
+ try {
1230
+ return adminAuthHeader();
1231
+ } catch (error2) {
1232
+ if (error2 instanceof AdminTokenUnavailableError) {
1233
+ throw new Error(
1234
+ `No CLI credential available. Set TASK0_API_TOKEN (preferred \u2014 issue one via the dashboard) or place a server admin token at ${error2.message.includes(".token") ? error2.message.split("at ")[1]?.split(".")[0] + ".token" : "~/.config/task0/admin.token"}.`
1235
+ );
1236
+ }
1237
+ throw error2;
1238
+ }
1239
+ }
1240
+ async function callHub(pathname, opts = {}) {
1241
+ const base = resolveHubUrl();
1242
+ const url = new URL(pathname.startsWith("/") ? pathname : `/${pathname}`, base + "/");
1243
+ for (const [k, v] of Object.entries(opts.query ?? {})) {
1244
+ if (v !== void 0) url.searchParams.set(k, v);
1245
+ }
1246
+ const init = {
1247
+ method: opts.method ?? "GET",
1248
+ headers: {
1249
+ ...resolveAuthHeader(),
1250
+ ...opts.body !== void 0 ? { "content-type": "application/json" } : {}
1251
+ }
1252
+ };
1253
+ if (opts.body !== void 0) init.body = JSON.stringify(opts.body);
1254
+ if (opts.timeoutMs) init.signal = AbortSignal.timeout(opts.timeoutMs);
1255
+ let res;
1256
+ try {
1257
+ res = await fetch(url, init);
1258
+ } catch (error2) {
1259
+ throw new HubUnreachableError(url.toString(), error2);
1260
+ }
1261
+ if (!res.ok) {
1262
+ const body = await res.text().catch(() => "");
1263
+ let code = null;
1264
+ try {
1265
+ const parsed = JSON.parse(body);
1266
+ if (typeof parsed?.code === "string") code = parsed.code;
1267
+ } catch {
1268
+ }
1269
+ throw new HubResponseError(res.status, body, code);
1270
+ }
1271
+ if (res.status === 204) return void 0;
1272
+ const text = await res.text();
1273
+ if (!text) return void 0;
1274
+ return JSON.parse(text);
1275
+ }
1276
+ function localDaemonId() {
1277
+ const identity = readDaemonIdentity();
1278
+ return identity?.daemon_id ?? null;
1279
+ }
987
1280
 
988
1281
  // src/commands/source.ts
989
- var source = new Command("source").description("Manage task sources");
990
- 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) => {
991
- const absPath = path8.resolve(inputPath);
992
- const name = opts.name || path8.basename(absPath);
993
- const result = scanProject(absPath);
994
- if (result.errors.length > 0 && result.tasks.length === 0) {
995
- for (const err of result.errors) console.error(chalk.red(` error: ${err}`));
1282
+ function reportHubError(error2) {
1283
+ if (error2 instanceof HubUnreachableError) {
1284
+ console.error(chalk.red(error2.message));
1285
+ console.error(chalk.dim("Tip: ensure the hub is running and TASK0_API_URL points to it."));
1286
+ process.exit(1);
1287
+ }
1288
+ if (error2 instanceof HubResponseError) {
1289
+ console.error(chalk.red(`Hub error (${error2.status}): ${error2.body || "(empty body)"}`));
996
1290
  process.exit(1);
997
1291
  }
998
- addSource({ name, type: "project", path: absPath, enabled: true });
1292
+ throw error2;
1293
+ }
1294
+ function requireLocalDaemon() {
1295
+ const id = localDaemonId();
1296
+ if (!id) {
1297
+ console.error(chalk.red("This host is not registered as a daemon."));
1298
+ console.error(chalk.dim("Run `task0 daemon register --server <url>` first."));
1299
+ process.exit(1);
1300
+ }
1301
+ return id;
1302
+ }
1303
+ var source = new Command("source").description("Manage task sources");
1304
+ source.command("add <path>").description("Register a local project on this host's daemon (via hub)").option("-n, --name <name>", "Source name (defaults to directory name)").action(async (inputPath, opts) => {
1305
+ const absPath = path3.resolve(inputPath);
1306
+ const name = opts.name || path3.basename(absPath);
1307
+ const daemonId = requireLocalDaemon();
1308
+ let resp;
1309
+ try {
1310
+ resp = await callHub(`/api/daemons/${encodeURIComponent(daemonId)}/projects`, {
1311
+ method: "POST",
1312
+ body: { path: absPath, name }
1313
+ });
1314
+ } catch (error2) {
1315
+ reportHubError(error2);
1316
+ }
999
1317
  console.log(chalk.green(`Added source "${name}" \u2192 ${absPath}`));
1000
- console.log(` ${result.tasks.length} tasks found`);
1001
- if (result.errors.length > 0) {
1002
- for (const err of result.errors) console.warn(chalk.yellow(` warn: ${err}`));
1318
+ if (typeof resp.taskCount === "number") {
1319
+ console.log(` ${resp.taskCount} tasks found`);
1003
1320
  }
1004
1321
  });
1005
- source.command("list").description("List registered task sources").action(() => {
1006
- const config = loadConfig();
1007
- if (config.sources.length === 0) {
1322
+ source.command("list").description("List registered task sources (queries the hub)").action(async () => {
1323
+ let data;
1324
+ try {
1325
+ data = await callHub("/api/config");
1326
+ } catch (error2) {
1327
+ if (error2 instanceof HubUnreachableError) {
1328
+ console.error(chalk.red(error2.message));
1329
+ console.error(chalk.dim("Tip: ensure the hub is running and TASK0_API_URL points to it."));
1330
+ process.exit(1);
1331
+ }
1332
+ if (error2 instanceof HubResponseError) {
1333
+ console.error(chalk.red(`Hub error (${error2.status}): ${error2.body || "(empty body)"}`));
1334
+ process.exit(1);
1335
+ }
1336
+ throw error2;
1337
+ }
1338
+ if (data.sources.length === 0) {
1008
1339
  console.log("No sources registered. Use `task0 source add <path>` to add one.");
1009
1340
  return;
1010
1341
  }
1011
- for (const s of config.sources) {
1342
+ for (const s of data.sources) {
1012
1343
  const status = s.enabled ? chalk.green("\u25CF") : chalk.dim("\u25CB");
1013
- console.log(`${status} ${s.name} ${chalk.dim(s.type)} ${s.path}`);
1344
+ const where = s.path ?? (s.type === "github" ? "(github)" : s.type === "linear" ? "(linear)" : "");
1345
+ const owner = s.daemon_id ? chalk.dim(` @${s.daemon_id}`) : "";
1346
+ console.log(`${status} ${s.name} ${chalk.dim(s.type)} ${where}${owner}`);
1014
1347
  }
1015
1348
  });
1016
- source.command("remove <name>").description("Remove a task source").action((name) => {
1017
- if (removeSource(name)) {
1018
- console.log(chalk.green(`Removed source "${name}"`));
1019
- } else {
1020
- console.error(chalk.red(`Source "${name}" not found`));
1021
- process.exit(1);
1022
- }
1023
- });
1024
- source.command("scan [name]").description("Scan source(s) and display tasks").option("--json", "Output as JSON").action((name, opts) => {
1025
- const config = loadConfig();
1026
- const sources = name ? (() => {
1027
- const s = getSource(name);
1028
- if (!s) {
1029
- console.error(chalk.red(`Source "${name}" not found`));
1349
+ source.command("remove <name>").description("Remove a project source from this host's daemon (via hub)").action(async (name) => {
1350
+ const daemonId = requireLocalDaemon();
1351
+ try {
1352
+ await callHub(`/api/daemons/${encodeURIComponent(daemonId)}/projects/${encodeURIComponent(name)}`, {
1353
+ method: "DELETE"
1354
+ });
1355
+ } catch (error2) {
1356
+ if (error2 instanceof HubResponseError && error2.status === 404) {
1357
+ console.error(chalk.red(`Source "${name}" not found on this daemon.`));
1030
1358
  process.exit(1);
1031
1359
  }
1032
- return [s];
1033
- })() : config.sources.filter((s) => s.enabled);
1034
- if (sources.length === 0) {
1035
- console.log("No sources to scan.");
1360
+ reportHubError(error2);
1361
+ }
1362
+ console.log(chalk.green(`Removed source "${name}"`));
1363
+ });
1364
+ source.command("scan [name]").description("Scan source(s) via the hub and display tasks").option("--json", "Output as JSON").action(async (name, opts) => {
1365
+ let data;
1366
+ try {
1367
+ data = await callHub("/api/tasks", { query: { source: name } });
1368
+ } catch (error2) {
1369
+ reportHubError(error2);
1370
+ }
1371
+ if (data.tasks.length === 0) {
1372
+ console.log(name ? `No tasks in source "${name}".` : "No tasks.");
1036
1373
  return;
1037
1374
  }
1038
- const allTasks = [];
1039
- for (const s of sources) {
1040
- const result = scanProject(s.path, s.name);
1041
- if (opts.json) {
1042
- allTasks.push(...result.tasks.map((t) => ({ ...t, _source: s.name })));
1043
- } else {
1044
- console.log(chalk.bold(`
1045
- ${s.name}`) + chalk.dim(` (${s.path})`));
1046
- if (result.errors.length > 0) {
1047
- for (const err of result.errors) console.warn(chalk.yellow(` warn: ${err}`));
1048
- }
1049
- const objectIdWidth = Math.max(...result.tasks.map((t) => (t.object_id || "").length), 9);
1050
- for (const t of result.tasks) {
1051
- const statusColor2 = isActiveTaskStatus(t.status) ? chalk.green : t.status === "blocked" ? chalk.red : t.status === "todo" ? chalk.yellow : t.status === "done" ? chalk.dim : chalk.white;
1052
- const objectId = chalk.cyan((t.object_id || "-").padEnd(objectIdWidth));
1053
- console.log(` ${objectId} ${statusColor2(t.status.padEnd(8))} ${chalk.dim(t.id)} ${t.title}`);
1054
- }
1055
- console.log(chalk.dim(` ${result.tasks.length} tasks`));
1375
+ if (opts.json) {
1376
+ const out = data.tasks.map((t) => ({ ...t, _source: t.project }));
1377
+ console.log(JSON.stringify(out, null, 2));
1378
+ return;
1379
+ }
1380
+ const byProject = /* @__PURE__ */ new Map();
1381
+ for (const t of data.tasks) {
1382
+ const list = byProject.get(t.project) ?? [];
1383
+ list.push(t);
1384
+ byProject.set(t.project, list);
1385
+ }
1386
+ for (const [project2, tasks] of byProject) {
1387
+ console.log(chalk.bold(`
1388
+ ${project2}`));
1389
+ const objectIdWidth = Math.max(...tasks.map((t) => (t.object_id || "").length), 9);
1390
+ for (const t of tasks) {
1391
+ const statusColor2 = isActiveTaskStatus(t.status) ? chalk.green : t.status === "blocked" ? chalk.red : t.status === "todo" ? chalk.yellow : t.status === "done" ? chalk.dim : chalk.white;
1392
+ const objectId = chalk.cyan((t.object_id || "-").padEnd(objectIdWidth));
1393
+ console.log(` ${objectId} ${statusColor2(t.status.padEnd(8))} ${chalk.dim(t.id)} ${t.title}`);
1056
1394
  }
1395
+ console.log(chalk.dim(` ${tasks.length} tasks`));
1057
1396
  }
1058
- if (opts.json) {
1059
- console.log(JSON.stringify(allTasks, null, 2));
1397
+ if (data.errors.length > 0) {
1398
+ for (const err of data.errors) console.warn(chalk.yellow(`warn: ${err}`));
1060
1399
  }
1061
1400
  });
1062
1401
 
1063
1402
  // src/commands/project.ts
1064
1403
  import { Command as Command2 } from "commander";
1065
- import fs8 from "fs";
1066
- import path9 from "path";
1067
- import yaml4 from "js-yaml";
1404
+ import fs3 from "fs";
1405
+ import path4 from "path";
1406
+ import yaml from "js-yaml";
1068
1407
  import chalk2 from "chalk";
1069
1408
 
1070
1409
  // ../../packages/shared/dist/index.js
@@ -1072,44 +1411,45 @@ init_object_id();
1072
1411
 
1073
1412
  // src/commands/project.ts
1074
1413
  var project = new Command2("project").description("Manage projects");
1075
- function readProjectObjectId(projectPath) {
1076
- const ymlPath = path9.join(projectPath, "task0.yml");
1077
- if (!fs8.existsSync(ymlPath)) return "-";
1414
+ project.command("list").description("List registered projects (queries the hub)").action(async () => {
1415
+ let data;
1078
1416
  try {
1079
- const raw = yaml4.load(fs8.readFileSync(ymlPath, "utf-8"));
1080
- const id = raw && typeof raw.object_id === "string" ? raw.object_id : "";
1081
- return id || "-";
1082
- } catch {
1083
- return "-";
1417
+ data = await callHub("/api/config");
1418
+ } catch (error2) {
1419
+ if (error2 instanceof HubUnreachableError) {
1420
+ console.error(chalk2.red(error2.message));
1421
+ process.exit(1);
1422
+ }
1423
+ if (error2 instanceof HubResponseError) {
1424
+ console.error(chalk2.red(`Hub error (${error2.status}): ${error2.body || "(empty body)"}`));
1425
+ process.exit(1);
1426
+ }
1427
+ throw error2;
1084
1428
  }
1085
- }
1086
- project.command("list").description("List registered projects").action(() => {
1087
- const projects = loadConfig().sources.filter((s) => s.type === "project");
1429
+ const projects = data.sources.filter((s) => s.type === "project");
1088
1430
  if (projects.length === 0) {
1089
1431
  console.log("No projects registered. Use `task0 source add <path>` to add one.");
1090
1432
  return;
1091
1433
  }
1092
- const rows = projects.map((p) => ({ p, objectId: readProjectObjectId(p.path) }));
1093
- const objectIdWidth = Math.max(...rows.map((r) => r.objectId.length), 9);
1094
- const nameWidth = Math.max(...rows.map((r) => r.p.name.length), 4);
1095
- for (const { p, objectId } of rows) {
1434
+ const nameWidth = Math.max(...projects.map((p) => p.name.length), 4);
1435
+ for (const p of projects) {
1096
1436
  const status = p.enabled ? chalk2.green("\u25CF") : chalk2.dim("\u25CB");
1097
- const oid = chalk2.cyan(objectId.padEnd(objectIdWidth));
1098
1437
  const name = p.name.padEnd(nameWidth);
1099
- console.log(`${status} ${oid} ${name} ${chalk2.dim(p.path)}`);
1438
+ const owner = p.daemon_id ? chalk2.dim(` @${p.daemon_id}`) : "";
1439
+ console.log(`${status} ${name} ${chalk2.dim(p.path ?? "")}${owner}`);
1100
1440
  }
1101
1441
  });
1102
1442
  project.command("init").description("Initialize task0.yml in the current directory").option("-d, --tasks-dir <dir>", "Tasks directory", ".task0/tasks").action((opts) => {
1103
1443
  const cwd = process.cwd();
1104
- const ymlPath = path9.join(cwd, "task0.yml");
1105
- if (fs8.existsSync(ymlPath)) {
1444
+ const ymlPath = path4.join(cwd, "task0.yml");
1445
+ if (fs3.existsSync(ymlPath)) {
1106
1446
  console.error(chalk2.yellow("task0.yml already exists"));
1107
1447
  process.exit(1);
1108
1448
  }
1109
1449
  const config = { kind: "project", object_id: generateObjectId("project"), tasks_dir: opts.tasksDir };
1110
- fs8.writeFileSync(ymlPath, yaml4.dump(config), "utf-8");
1111
- const tasksDir = path9.join(cwd, opts.tasksDir);
1112
- fs8.mkdirSync(tasksDir, { recursive: true });
1450
+ fs3.writeFileSync(ymlPath, yaml.dump(config), "utf-8");
1451
+ const tasksDir = path4.join(cwd, opts.tasksDir);
1452
+ fs3.mkdirSync(tasksDir, { recursive: true });
1113
1453
  console.log(chalk2.green("Initialized task0 project"));
1114
1454
  console.log(` ${ymlPath}`);
1115
1455
  console.log(` ${tasksDir}/`);
@@ -1118,61 +1458,57 @@ project.command("init").description("Initialize task0.yml in the current directo
1118
1458
  // src/commands/task.ts
1119
1459
  import { Command as Command8 } from "commander";
1120
1460
  import { execSync } from "child_process";
1121
- import fs14 from "fs";
1122
- import path12 from "path";
1123
- import yaml5 from "js-yaml";
1461
+ import fs17 from "fs";
1462
+ import path15 from "path";
1463
+ import yaml6 from "js-yaml";
1124
1464
  import chalk8 from "chalk";
1125
1465
 
1126
1466
  // src/lib/api.ts
1127
- var DEFAULT_BASE = "http://127.0.0.1:4318";
1128
1467
  function apiBaseUrl() {
1129
- return process.env.TASK0_API_URL || DEFAULT_BASE;
1468
+ return process.env.TASK0_API_URL || "http://127.0.0.1:4318";
1130
1469
  }
1131
- async function request(method, pathname, body) {
1132
- const url = apiBaseUrl().replace(/\/$/, "") + pathname;
1133
- let res;
1134
- try {
1135
- res = await fetch(url, {
1136
- method,
1137
- headers: body !== void 0 ? { "content-type": "application/json" } : void 0,
1138
- body: body !== void 0 ? JSON.stringify(body) : void 0
1139
- });
1140
- } catch (error2) {
1470
+ function toApiError(method, pathname, error2) {
1471
+ if (error2 instanceof HubUnreachableError) {
1141
1472
  const err = new Error(
1142
1473
  `Cannot reach task0 API at ${apiBaseUrl()}. Start the task0-server binary (download from GitHub Releases) or repoint TASK0_API_URL.`
1143
1474
  );
1144
1475
  err.cause = error2;
1145
- throw err;
1476
+ return err;
1146
1477
  }
1147
- const text = await res.text();
1148
- let parsed = null;
1149
- try {
1150
- parsed = text ? JSON.parse(text) : null;
1151
- } catch {
1152
- parsed = text;
1478
+ if (error2 instanceof HubResponseError) {
1479
+ let parsedBody = error2.body;
1480
+ try {
1481
+ parsedBody = error2.body ? JSON.parse(error2.body) : null;
1482
+ } catch {
1483
+ }
1484
+ const message = parsedBody?.error || error2.body || error2.message;
1485
+ const err = new Error(`API ${method} ${pathname} failed (${error2.status}): ${message}`);
1486
+ err.status = error2.status;
1487
+ err.body = parsedBody;
1488
+ return err;
1153
1489
  }
1154
- if (!res.ok) {
1155
- const message = parsed?.error || text || res.statusText;
1156
- const err = new Error(`API ${method} ${pathname} failed (${res.status}): ${message}`);
1157
- err.status = res.status;
1158
- err.body = parsed;
1159
- throw err;
1490
+ return error2 instanceof Error ? error2 : new Error(String(error2));
1491
+ }
1492
+ async function request(method, pathname, body) {
1493
+ try {
1494
+ return await callHub(pathname, { method, body });
1495
+ } catch (error2) {
1496
+ throw toApiError(method, pathname, error2);
1160
1497
  }
1161
- return parsed;
1162
1498
  }
1163
1499
  var api = {
1164
- get: (path26) => request("GET", path26),
1165
- post: (path26, body) => request("POST", path26, body ?? {}),
1166
- put: (path26, body) => request("PUT", path26, body ?? {}),
1167
- patch: (path26, body) => request("PATCH", path26, body ?? {}),
1168
- del: (path26) => request("DELETE", path26)
1500
+ get: (path29) => request("GET", path29),
1501
+ post: (path29, body) => request("POST", path29, body ?? {}),
1502
+ put: (path29, body) => request("PUT", path29, body ?? {}),
1503
+ patch: (path29, body) => request("PATCH", path29, body ?? {}),
1504
+ del: (path29) => request("DELETE", path29)
1169
1505
  };
1170
1506
 
1171
1507
  // src/commands/task/triage.ts
1172
1508
  import { Command as Command3 } from "commander";
1173
1509
  import chalk3 from "chalk";
1174
- import fs10 from "fs";
1175
- import path10 from "path";
1510
+ import fs13 from "fs";
1511
+ import path13 from "path";
1176
1512
 
1177
1513
  // src/core/agent-run-wait.ts
1178
1514
  async function getAgentRun(id) {
@@ -1196,6 +1532,54 @@ async function waitForAgentRun(id, opts = {}) {
1196
1532
  }
1197
1533
  }
1198
1534
 
1535
+ // src/core/config.ts
1536
+ import fs4 from "fs";
1537
+ import path5 from "path";
1538
+ import yaml2 from "js-yaml";
1539
+ var CONFIG_DIR3 = path5.join(
1540
+ process.env.HOME || process.env.USERPROFILE || "~",
1541
+ ".config",
1542
+ "task0"
1543
+ );
1544
+ var CONFIG_FILE2 = path5.join(CONFIG_DIR3, "config.yml");
1545
+ function ensureConfigDir() {
1546
+ fs4.mkdirSync(CONFIG_DIR3, { recursive: true });
1547
+ }
1548
+ function defaultConfig() {
1549
+ return { sources: [] };
1550
+ }
1551
+ function loadConfig() {
1552
+ if (!fs4.existsSync(CONFIG_FILE2)) return defaultConfig();
1553
+ const raw = fs4.readFileSync(CONFIG_FILE2, "utf-8");
1554
+ const data = yaml2.load(raw);
1555
+ return {
1556
+ ...data ?? {},
1557
+ sources: Array.isArray(data?.sources) ? data.sources : []
1558
+ };
1559
+ }
1560
+ function saveConfig(config) {
1561
+ ensureConfigDir();
1562
+ fs4.writeFileSync(CONFIG_FILE2, yaml2.dump(config, { lineWidth: 120 }), "utf-8");
1563
+ }
1564
+ function addSource(entry) {
1565
+ const config = loadConfig();
1566
+ const existing = config.sources.findIndex((s) => s.name === entry.name);
1567
+ if (existing >= 0) {
1568
+ config.sources[existing] = entry;
1569
+ } else {
1570
+ config.sources.push(entry);
1571
+ }
1572
+ saveConfig(config);
1573
+ }
1574
+ function removeSource(name) {
1575
+ const config = loadConfig();
1576
+ const idx = config.sources.findIndex((s) => s.name === name);
1577
+ if (idx < 0) return false;
1578
+ config.sources.splice(idx, 1);
1579
+ saveConfig(config);
1580
+ return true;
1581
+ }
1582
+
1199
1583
  // src/core/model-options.ts
1200
1584
  function resolveModelOptions(agent2, cli, warn = () => {
1201
1585
  }) {
@@ -1223,8 +1607,8 @@ init_task_state2();
1223
1607
  var ISSUE_DETAIL_RE = /^ISSUE-\d+\.md$/;
1224
1608
  var TRIAGE_SKILL_NAME = "triage";
1225
1609
  function resolveSkillFilePath(projectRoot, skillName) {
1226
- const p = path10.join(projectRoot, ".claude", "skills", skillName, "SKILL.md");
1227
- return fs10.existsSync(p) ? p : null;
1610
+ const p = path13.join(projectRoot, ".claude", "skills", skillName, "SKILL.md");
1611
+ return fs13.existsSync(p) ? p : null;
1228
1612
  }
1229
1613
  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) => {
1230
1614
  try {
@@ -1243,7 +1627,7 @@ var triage = new Command3("triage").description("Decompose IDEA into ISSUE.md +
1243
1627
  process.exit(1);
1244
1628
  }
1245
1629
  for (const name of existingIssues) {
1246
- fs10.rmSync(path10.join(loc.taskDir, name), { force: true });
1630
+ fs13.rmSync(path13.join(loc.taskDir, name), { force: true });
1247
1631
  }
1248
1632
  }
1249
1633
  if (opts.model || opts.effort) {
@@ -1281,8 +1665,8 @@ var triage = new Command3("triage").description("Decompose IDEA into ISSUE.md +
1281
1665
  console.error(chalk3.red(`triage failed: ${final.error || "unknown"}`));
1282
1666
  process.exit(1);
1283
1667
  }
1284
- const hasOverview = fs10.existsSync(path10.join(loc.taskDir, "ISSUE.md"));
1285
- const issueFiles = fs10.readdirSync(loc.taskDir).filter((name) => ISSUE_DETAIL_RE.test(name)).sort();
1668
+ const hasOverview = fs13.existsSync(path13.join(loc.taskDir, "ISSUE.md"));
1669
+ const issueFiles = fs13.readdirSync(loc.taskDir).filter((name) => ISSUE_DETAIL_RE.test(name)).sort();
1286
1670
  if (!hasOverview || issueFiles.length === 0) {
1287
1671
  const missing = [];
1288
1672
  if (!hasOverview) missing.push("ISSUE.md");
@@ -1292,7 +1676,7 @@ var triage = new Command3("triage").description("Decompose IDEA into ISSUE.md +
1292
1676
  }
1293
1677
  let blockingQuestionCount = 0;
1294
1678
  for (const name of issueFiles) {
1295
- const content = fs10.readFileSync(path10.join(loc.taskDir, name), "utf-8");
1679
+ const content = fs13.readFileSync(path13.join(loc.taskDir, name), "utf-8");
1296
1680
  blockingQuestionCount += countBlockingQuestions(content);
1297
1681
  }
1298
1682
  await updateWorkflow(loc.taskYml, {
@@ -1320,8 +1704,8 @@ var triage = new Command3("triage").description("Decompose IDEA into ISSUE.md +
1320
1704
  }
1321
1705
  });
1322
1706
  function listIssueArtifacts(taskDir) {
1323
- if (!fs10.existsSync(taskDir)) return [];
1324
- return fs10.readdirSync(taskDir).filter((name) => name === "ISSUE.md" || ISSUE_DETAIL_RE.test(name)).sort();
1707
+ if (!fs13.existsSync(taskDir)) return [];
1708
+ return fs13.readdirSync(taskDir).filter((name) => name === "ISSUE.md" || ISSUE_DETAIL_RE.test(name)).sort();
1325
1709
  }
1326
1710
  function countBlockingQuestions(md) {
1327
1711
  const match = md.match(/## Open Questions\s*\n([\s\S]*?)(\n## |\n*$)/i);
@@ -1335,13 +1719,13 @@ function countBlockingQuestions(md) {
1335
1719
  // src/commands/task/exec.ts
1336
1720
  import { Command as Command4 } from "commander";
1337
1721
  import chalk4 from "chalk";
1338
- import fs11 from "fs";
1339
- import path11 from "path";
1722
+ import fs14 from "fs";
1723
+ import path14 from "path";
1340
1724
  init_task_state2();
1341
1725
  var PLAN_EXECUTE_SKILL_NAME = "plan-execute";
1342
1726
  function resolveSkillFilePath2(projectRoot, skillName) {
1343
- const p = path11.join(projectRoot, ".claude", "skills", skillName, "SKILL.md");
1344
- return fs11.existsSync(p) ? p : null;
1727
+ const p = path14.join(projectRoot, ".claude", "skills", skillName, "SKILL.md");
1728
+ return fs14.existsSync(p) ? p : null;
1345
1729
  }
1346
1730
  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) => {
1347
1731
  try {
@@ -1434,7 +1818,7 @@ var summarize = new Command5("summarize").description("Generate a concise title
1434
1818
  });
1435
1819
 
1436
1820
  // src/commands/task/comment.ts
1437
- import fs12 from "fs";
1821
+ import fs15 from "fs";
1438
1822
  import { Command as Command6 } from "commander";
1439
1823
  import chalk6 from "chalk";
1440
1824
  var comment = new Command6("comment").description("Manage comments on a task");
@@ -1444,8 +1828,8 @@ function fail(err) {
1444
1828
  }
1445
1829
  function readBodyFromOpts(opts) {
1446
1830
  if (opts.body !== void 0) return opts.body;
1447
- if (opts.file === "-") return fs12.readFileSync(0, "utf-8");
1448
- if (opts.file) return fs12.readFileSync(opts.file, "utf-8");
1831
+ if (opts.file === "-") return fs15.readFileSync(0, "utf-8");
1832
+ if (opts.file) return fs15.readFileSync(opts.file, "utf-8");
1449
1833
  throw new Error("Provide --body or --file");
1450
1834
  }
1451
1835
  function preview(body, width = 60) {
@@ -1544,7 +1928,7 @@ comment.command("delete <cmtId>").description("Delete a comment by its cmt_ id")
1544
1928
  });
1545
1929
 
1546
1930
  // src/commands/task/description.ts
1547
- import fs13 from "fs";
1931
+ import fs16 from "fs";
1548
1932
  import { Command as Command7 } from "commander";
1549
1933
  import chalk7 from "chalk";
1550
1934
  var description = new Command7("description").description("Show or update the task description");
@@ -1554,8 +1938,8 @@ function fail2(err) {
1554
1938
  }
1555
1939
  function readBodyFromOpts2(opts) {
1556
1940
  if (opts.body !== void 0) return opts.body;
1557
- if (opts.file === "-") return fs13.readFileSync(0, "utf-8");
1558
- if (opts.file) return fs13.readFileSync(opts.file, "utf-8");
1941
+ if (opts.file === "-") return fs16.readFileSync(0, "utf-8");
1942
+ if (opts.file) return fs16.readFileSync(opts.file, "utf-8");
1559
1943
  throw new Error("Provide --body or --file (use --file - to read stdin)");
1560
1944
  }
1561
1945
  description.command("show <taskId>").description("Print the current description (taskId is short id or tsk_)").option("--json", "Output JSON").action(async (taskId, opts) => {
@@ -1604,12 +1988,12 @@ task.addCommand(comment);
1604
1988
  task.addCommand(description);
1605
1989
  task.command("init <input>").description("Create a task from a description or Linear/GitHub issue URL").action(async (input) => {
1606
1990
  const cwd = process.cwd();
1607
- const projectYml = path12.join(cwd, "task0.yml");
1608
- if (!fs14.existsSync(projectYml)) {
1991
+ const projectYml = path15.join(cwd, "task0.yml");
1992
+ if (!fs17.existsSync(projectYml)) {
1609
1993
  console.error(chalk8.red("Not a task0 project (task0.yml not found). Run `task0 project init` first."));
1610
1994
  process.exit(1);
1611
1995
  }
1612
- const projectConfig = yaml5.load(fs14.readFileSync(projectYml, "utf-8"));
1996
+ const projectConfig = yaml6.load(fs17.readFileSync(projectYml, "utf-8"));
1613
1997
  if (projectConfig.kind !== "project") {
1614
1998
  console.error(chalk8.red('Invalid task0.yml: kind is not "project"'));
1615
1999
  process.exit(1);
@@ -1625,38 +2009,38 @@ task.command("init <input>").description("Create a task from a description or Li
1625
2009
  });
1626
2010
  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) => {
1627
2011
  const cwd = process.cwd();
1628
- const projectYml = path12.join(cwd, "task0.yml");
1629
- if (!fs14.existsSync(projectYml)) {
2012
+ const projectYml = path15.join(cwd, "task0.yml");
2013
+ if (!fs17.existsSync(projectYml)) {
1630
2014
  console.error(chalk8.red("Not a task0 project (task0.yml not found). Run `task0 project init` first."));
1631
2015
  process.exit(1);
1632
2016
  }
1633
- const projectConfig = yaml5.load(fs14.readFileSync(projectYml, "utf-8"));
2017
+ const projectConfig = yaml6.load(fs17.readFileSync(projectYml, "utf-8"));
1634
2018
  if (projectConfig.kind !== "project") {
1635
2019
  console.error(chalk8.red('Invalid task0.yml: kind is not "project"'));
1636
2020
  process.exit(1);
1637
2021
  }
1638
- const tasksDir = path12.join(cwd, projectConfig.tasks_dir);
1639
- if (!fs14.existsSync(tasksDir)) {
2022
+ const tasksDir = path15.join(cwd, projectConfig.tasks_dir);
2023
+ if (!fs17.existsSync(tasksDir)) {
1640
2024
  console.error(chalk8.red(`Tasks directory not found: ${tasksDir}`));
1641
2025
  process.exit(1);
1642
2026
  }
1643
- const entries = fs14.readdirSync(tasksDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
2027
+ const entries = fs17.readdirSync(tasksDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
1644
2028
  let migrated = 0;
1645
2029
  let skipped = 0;
1646
2030
  let seededObjectIds = 0;
1647
2031
  for (const name of entries) {
1648
- const taskDir = path12.join(tasksDir, name);
1649
- const taskYml = path12.join(taskDir, "task0.yml");
1650
- if (fs14.existsSync(taskYml)) {
2032
+ const taskDir = path15.join(tasksDir, name);
2033
+ const taskYml = path15.join(taskDir, "task0.yml");
2034
+ if (fs17.existsSync(taskYml)) {
1651
2035
  skipped++;
1652
2036
  try {
1653
- const raw = yaml5.load(fs14.readFileSync(taskYml, "utf-8"));
2037
+ const raw = yaml6.load(fs17.readFileSync(taskYml, "utf-8"));
1654
2038
  if (raw && raw.kind === "task" && !raw.object_id) {
1655
2039
  raw.object_id = generateObjectId("task");
1656
2040
  if (opts.dryRun) {
1657
2041
  console.log(chalk8.dim(`[dry-run] seed object_id: ${taskYml}`));
1658
2042
  } else {
1659
- fs14.writeFileSync(taskYml, yaml5.dump(raw, { lineWidth: 120 }), "utf-8");
2043
+ fs17.writeFileSync(taskYml, yaml6.dump(raw, { lineWidth: 120 }), "utf-8");
1660
2044
  console.log(chalk8.green(` seed object_id: ${taskYml}`));
1661
2045
  }
1662
2046
  seededObjectIds++;
@@ -1681,7 +2065,7 @@ task.command("migrate").description("Add task0.yml to legacy task directories th
1681
2065
  if (opts.dryRun) {
1682
2066
  console.log(chalk8.dim(`[dry-run] ${taskYml}`));
1683
2067
  } else {
1684
- fs14.writeFileSync(taskYml, yaml5.dump(taskConfig), "utf-8");
2068
+ fs17.writeFileSync(taskYml, yaml6.dump(taskConfig), "utf-8");
1685
2069
  console.log(chalk8.green(` ${taskYml}`));
1686
2070
  }
1687
2071
  migrated++;
@@ -1741,7 +2125,7 @@ task.command("done <id>").description("Write task lifecycle state to task0.yml \
1741
2125
  try {
1742
2126
  const { resolveTaskByObjectId: resolveTaskByObjectId2 } = await Promise.resolve().then(() => (init_task_state2(), task_state_exports));
1743
2127
  const loc = resolveTaskByObjectId2(id);
1744
- const raw = yaml5.load(fs14.readFileSync(loc.taskYml, "utf-8"));
2128
+ const raw = yaml6.load(fs17.readFileSync(loc.taskYml, "utf-8"));
1745
2129
  if (opts.phase) {
1746
2130
  const phase = opts.phase.trim();
1747
2131
  if (!phase) {
@@ -1754,7 +2138,7 @@ task.command("done <id>").description("Write task lifecycle state to task0.yml \
1754
2138
  return;
1755
2139
  }
1756
2140
  raw.workflow = { ...workflow2, phase };
1757
- fs14.writeFileSync(loc.taskYml, yaml5.dump(raw, { lineWidth: 120 }), "utf-8");
2141
+ fs17.writeFileSync(loc.taskYml, yaml6.dump(raw, { lineWidth: 120 }), "utf-8");
1758
2142
  console.log(chalk8.green(`${id} phase: ${phase}`));
1759
2143
  return;
1760
2144
  }
@@ -1765,7 +2149,7 @@ task.command("done <id>").description("Write task lifecycle state to task0.yml \
1765
2149
  raw.status = "done";
1766
2150
  const workflow = raw.workflow ?? {};
1767
2151
  raw.workflow = { ...workflow, phase: "completed" };
1768
- fs14.writeFileSync(loc.taskYml, yaml5.dump(raw, { lineWidth: 120 }), "utf-8");
2152
+ fs17.writeFileSync(loc.taskYml, yaml6.dump(raw, { lineWidth: 120 }), "utf-8");
1769
2153
  console.log(chalk8.green(`${id} marked as done`));
1770
2154
  } catch (err) {
1771
2155
  console.error(chalk8.red(err.message));
@@ -1774,33 +2158,33 @@ task.command("done <id>").description("Write task lifecycle state to task0.yml \
1774
2158
  });
1775
2159
  task.command("archive <id>").description("Archive a task (append to tasks.tar, remove from tasks/)").action((id) => {
1776
2160
  const cwd = process.cwd();
1777
- const projectYml = path12.join(cwd, "task0.yml");
1778
- if (!fs14.existsSync(projectYml)) {
2161
+ const projectYml = path15.join(cwd, "task0.yml");
2162
+ if (!fs17.existsSync(projectYml)) {
1779
2163
  console.error(chalk8.red("Not a task0 project (task0.yml not found)."));
1780
2164
  process.exit(1);
1781
2165
  }
1782
- const projectConfig = yaml5.load(fs14.readFileSync(projectYml, "utf-8"));
2166
+ const projectConfig = yaml6.load(fs17.readFileSync(projectYml, "utf-8"));
1783
2167
  if (projectConfig.kind !== "project") {
1784
2168
  console.error(chalk8.red('Invalid task0.yml: kind is not "project"'));
1785
2169
  process.exit(1);
1786
2170
  }
1787
- const tasksDir = path12.join(cwd, projectConfig.tasks_dir);
1788
- const taskDir = path12.join(tasksDir, id);
1789
- if (!fs14.existsSync(taskDir)) {
2171
+ const tasksDir = path15.join(cwd, projectConfig.tasks_dir);
2172
+ const taskDir = path15.join(tasksDir, id);
2173
+ if (!fs17.existsSync(taskDir)) {
1790
2174
  console.error(chalk8.red(`Task "${id}" not found at ${taskDir}`));
1791
2175
  process.exit(1);
1792
2176
  }
1793
2177
  const tarFile = tasksDir + ".tar";
1794
2178
  archiveTaskToTar(tasksDir, id, tarFile);
1795
- fs14.rmSync(taskDir, { recursive: true });
2179
+ fs17.rmSync(taskDir, { recursive: true });
1796
2180
  console.log(chalk8.green(`Archived "${id}" \u2192 ${tarFile}`));
1797
2181
  });
1798
2182
  function archiveTaskToTar(tasksDir, taskId, tarFile) {
1799
- if (fs14.existsSync(tarFile) && !fs14.statSync(tarFile).isFile()) {
2183
+ if (fs17.existsSync(tarFile) && !fs17.statSync(tarFile).isFile()) {
1800
2184
  console.error(chalk8.red(`${tarFile} exists but is not a file (likely a leftover directory). Remove it manually first.`));
1801
2185
  process.exit(1);
1802
2186
  }
1803
- if (fs14.existsSync(tarFile)) {
2187
+ if (fs17.existsSync(tarFile)) {
1804
2188
  execSync(`tar -rf ${JSON.stringify(tarFile)} -C ${JSON.stringify(tasksDir)} ${JSON.stringify(taskId)}`);
1805
2189
  } else {
1806
2190
  execSync(`tar -cf ${JSON.stringify(tarFile)} -C ${JSON.stringify(tasksDir)} ${JSON.stringify(taskId)}`);
@@ -1810,9 +2194,9 @@ function archiveTaskToTar(tasksDir, taskId, tarFile) {
1810
2194
  // src/commands/ui.ts
1811
2195
  import { Command as Command9 } from "commander";
1812
2196
  import { spawn } from "child_process";
1813
- import path13 from "path";
2197
+ import path16 from "path";
1814
2198
  import chalk9 from "chalk";
1815
- var DASHBOARD_DIR = path13.resolve(
2199
+ var DASHBOARD_DIR = path16.resolve(
1816
2200
  import.meta.dirname,
1817
2201
  "..",
1818
2202
  "..",
@@ -2093,12 +2477,12 @@ models.command("default <agent>").description("Get or set default model / effort
2093
2477
  });
2094
2478
 
2095
2479
  // src/commands/agent-run.ts
2096
- import fs15 from "fs";
2097
- import path14 from "path";
2480
+ import fs18 from "fs";
2481
+ import path17 from "path";
2098
2482
  import { Command as Command11 } from "commander";
2099
2483
  import chalk11 from "chalk";
2100
2484
  import { spawn as spawn2, spawnSync as spawnSync3 } from "child_process";
2101
- import yaml6 from "js-yaml";
2485
+ import yaml7 from "js-yaml";
2102
2486
  var agentRun = new Command11("agent-run").description("Inspect and control agent runs");
2103
2487
  agentRun.command("list").description("List active agent runs").option("--json", "Output JSON").option("--task <id>", "Filter by task id").action(async (opts) => {
2104
2488
  try {
@@ -2222,7 +2606,7 @@ profile.command("get <ref>").description("Show one runtime profile by slug or ob
2222
2606
  console.log(formatProfile(result.runtime));
2223
2607
  console.log();
2224
2608
  console.log(chalk11.dim("--- exec ---"));
2225
- console.log(yaml6.dump(result.runtime.exec, { lineWidth: 100 }));
2609
+ console.log(yaml7.dump(result.runtime.exec, { lineWidth: 100 }));
2226
2610
  } catch (err) {
2227
2611
  const apiErr = err;
2228
2612
  if (apiErr.status === 404) failProfile(`not found: ${ref}`);
@@ -2232,7 +2616,7 @@ profile.command("get <ref>").description("Show one runtime profile by slug or ob
2232
2616
  profile.command("create").description("Create a runtime profile from a YAML spec file").requiredOption("--from-file <file>", "YAML spec file with kind, slug, exec").option("--scope <scope>", "Storage scope: user (default) or project", "user").option("--project-root <path>", "Project root (required for --scope project)").action(async (opts) => {
2233
2617
  let parsed;
2234
2618
  try {
2235
- parsed = yaml6.load(fs15.readFileSync(path14.resolve(opts.fromFile), "utf-8"));
2619
+ parsed = yaml7.load(fs18.readFileSync(path17.resolve(opts.fromFile), "utf-8"));
2236
2620
  } catch (err) {
2237
2621
  failProfile(`cannot read ${opts.fromFile}: ${err.message}`);
2238
2622
  }
@@ -2248,14 +2632,14 @@ profile.command("edit <ref>").description("Open the runtime profile YAML in $EDI
2248
2632
  try {
2249
2633
  const result = await api.get(`/api/runtime-profiles/${encodeURIComponent(ref)}`);
2250
2634
  if (result.runtime.system) failProfile("cannot edit a system runtime profile");
2251
- const tmp = path14.join(process.env.TMPDIR || "/tmp", `task0-runtime-${result.runtime.slug}-${Date.now()}.yml`);
2252
- fs15.writeFileSync(tmp, yaml6.dump(result.runtime, { lineWidth: 100 }), "utf-8");
2635
+ const tmp = path17.join(process.env.TMPDIR || "/tmp", `task0-runtime-${result.runtime.slug}-${Date.now()}.yml`);
2636
+ fs18.writeFileSync(tmp, yaml7.dump(result.runtime, { lineWidth: 100 }), "utf-8");
2253
2637
  const editor = process.env.EDITOR || "vi";
2254
2638
  const r = spawnSync3(editor, [tmp], { stdio: "inherit" });
2255
2639
  if (r.status !== 0) failProfile(`editor exited with status ${r.status}`);
2256
- const updated = yaml6.load(fs15.readFileSync(tmp, "utf-8"));
2640
+ const updated = yaml7.load(fs18.readFileSync(tmp, "utf-8"));
2257
2641
  await api.put(`/api/runtime-profiles/${encodeURIComponent(ref)}`, updated);
2258
- fs15.unlinkSync(tmp);
2642
+ fs18.unlinkSync(tmp);
2259
2643
  console.log(chalk11.green(`updated ${ref}`));
2260
2644
  } catch (err) {
2261
2645
  failProfile(err.message);
@@ -2275,13 +2659,13 @@ agentRun.addCommand(profile);
2275
2659
  // src/commands/plan.ts
2276
2660
  import { Command as Command12 } from "commander";
2277
2661
  import chalk12 from "chalk";
2278
- import fs16 from "fs";
2279
- import path15 from "path";
2662
+ import fs19 from "fs";
2663
+ import path18 from "path";
2280
2664
  init_task_state2();
2281
2665
  var PLAN_GENERATE_SKILL_NAME = "plan-generate";
2282
2666
  function resolveSkillFilePath3(projectRoot, skillName) {
2283
- const p = path15.join(projectRoot, ".claude", "skills", skillName, "SKILL.md");
2284
- return fs16.existsSync(p) ? p : null;
2667
+ const p = path18.join(projectRoot, ".claude", "skills", skillName, "SKILL.md");
2668
+ return fs19.existsSync(p) ? p : null;
2285
2669
  }
2286
2670
  var plan = new Command12("plan").description("Generate and refine plans");
2287
2671
  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) => {
@@ -2413,7 +2797,7 @@ var PLAN_REFINE_SKILL_NAME = "plan-refine";
2413
2797
  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) => {
2414
2798
  try {
2415
2799
  const loc = resolveTaskByObjectId(objectId);
2416
- const files = fs16.readdirSync(loc.taskDir);
2800
+ const files = fs19.readdirSync(loc.taskDir);
2417
2801
  const planFiles = files.filter((f) => /^PLAN-\d+-(codex|claude-code|cursor)\.md$/.test(f));
2418
2802
  if (planFiles.length === 0) {
2419
2803
  console.error(chalk12.red("No PLAN-NN-<agent>.md files found. Run `task0 plan generate` first."));
@@ -2455,8 +2839,8 @@ plan.command("refine <objectId>").description("Synthesize plan files + ISSUE fil
2455
2839
  if (!opts.json) console.log(chalk12.dim(`[refine] ${s.status}${s.phase ? " " + s.phase : ""}`));
2456
2840
  }
2457
2841
  });
2458
- const refinedPath = path15.join(loc.taskDir, refinedFile);
2459
- const wrote = fs16.existsSync(refinedPath);
2842
+ const refinedPath = path18.join(loc.taskDir, refinedFile);
2843
+ const wrote = fs19.existsSync(refinedPath);
2460
2844
  if (final.status === "done" && wrote) {
2461
2845
  await updateWorkflow(loc.taskYml, {
2462
2846
  phase: "refined",
@@ -2608,8 +2992,8 @@ import { Command as Command14 } from "commander";
2608
2992
  import chalk14 from "chalk";
2609
2993
 
2610
2994
  // src/lib/project.ts
2611
- import path16 from "path";
2612
- import fs17 from "fs";
2995
+ import path19 from "path";
2996
+ import fs20 from "fs";
2613
2997
  function resolveProjectName(opts) {
2614
2998
  if (opts.project && opts.project.length > 0) return opts.project;
2615
2999
  const config = loadConfig();
@@ -2619,15 +3003,15 @@ function resolveProjectName(opts) {
2619
3003
  "Cannot resolve project: no registered projects. Use `task0 source add <path>` first, or pass --project <name>."
2620
3004
  );
2621
3005
  }
2622
- const cwd = fs17.realpathSync(process.cwd());
3006
+ const cwd = fs20.realpathSync(process.cwd());
2623
3007
  for (const source2 of projects) {
2624
3008
  let sourceAbs;
2625
3009
  try {
2626
- sourceAbs = fs17.realpathSync(path16.resolve(source2.path));
3010
+ sourceAbs = fs20.realpathSync(path19.resolve(source2.path));
2627
3011
  } catch {
2628
- sourceAbs = path16.resolve(source2.path);
3012
+ sourceAbs = path19.resolve(source2.path);
2629
3013
  }
2630
- if (cwd === sourceAbs || cwd.startsWith(sourceAbs + path16.sep)) {
3014
+ if (cwd === sourceAbs || cwd.startsWith(sourceAbs + path19.sep)) {
2631
3015
  return source2.name;
2632
3016
  }
2633
3017
  }
@@ -3267,12 +3651,12 @@ import chalk17 from "chalk";
3267
3651
 
3268
3652
  // src/core/issue/decision.ts
3269
3653
  init_node();
3270
- import fs18 from "fs";
3271
- import path17 from "path";
3654
+ import fs21 from "fs";
3655
+ import path20 from "path";
3272
3656
  init_task_state2();
3273
3657
  var DECISION_FILE_RE = /^DECISION-(\d+)-([a-z0-9-]+)\.md$/;
3274
3658
  function selectBlockingIssues(taskDir, explicitIssue) {
3275
- const files = fs18.existsSync(taskDir) ? fs18.readdirSync(taskDir) : [];
3659
+ const files = fs21.existsSync(taskDir) ? fs21.readdirSync(taskDir) : [];
3276
3660
  const issueFiles = files.filter((f) => /^ISSUE-\d+\.md$/.test(f)).sort();
3277
3661
  const all = readOpenQuestions(taskDir, issueFiles);
3278
3662
  if (!explicitIssue) return all;
@@ -3376,12 +3760,12 @@ function parseConsolidatedAnswers(md) {
3376
3760
  return sections;
3377
3761
  }
3378
3762
  function rewriteIssueWithDecisions(taskDir, issueFile, consolidatedFile) {
3379
- const issuePath = path17.join(taskDir, issueFile);
3380
- const consolidatedPath = path17.join(taskDir, consolidatedFile);
3381
- if (!fs18.existsSync(issuePath)) throw new Error(`${issueFile} not found in ${taskDir}`);
3382
- if (!fs18.existsSync(consolidatedPath)) throw new Error(`${consolidatedFile} not found in ${taskDir}`);
3383
- const issueMd = fs18.readFileSync(issuePath, "utf-8");
3384
- const consolidatedMd = fs18.readFileSync(consolidatedPath, "utf-8");
3763
+ const issuePath = path20.join(taskDir, issueFile);
3764
+ const consolidatedPath = path20.join(taskDir, consolidatedFile);
3765
+ if (!fs21.existsSync(issuePath)) throw new Error(`${issueFile} not found in ${taskDir}`);
3766
+ if (!fs21.existsSync(consolidatedPath)) throw new Error(`${consolidatedFile} not found in ${taskDir}`);
3767
+ const issueMd = fs21.readFileSync(issuePath, "utf-8");
3768
+ const consolidatedMd = fs21.readFileSync(consolidatedPath, "utf-8");
3385
3769
  const answers = parseConsolidatedAnswers(consolidatedMd);
3386
3770
  if (answers.length === 0) {
3387
3771
  throw new Error(`${consolidatedFile} has no "## Qn" sections; cannot derive Decisions`);
@@ -3403,7 +3787,7 @@ _Resolved from [${consolidatedFile}](${consolidatedFile}); see that file for rea
3403
3787
  throw new Error(`${issueFile} has no "## Open Questions" section to replace`);
3404
3788
  }
3405
3789
  const next = issueMd.replace(openQRe, decisionsSection.trimEnd() + "\n");
3406
- fs18.writeFileSync(issuePath, next, "utf-8");
3790
+ fs21.writeFileSync(issuePath, next, "utf-8");
3407
3791
  return { replaced: answers.length };
3408
3792
  }
3409
3793
  async function propose(opts) {
@@ -3431,8 +3815,8 @@ async function propose(opts) {
3431
3815
  const kicks = [];
3432
3816
  for (const agent2 of opts.agents) {
3433
3817
  const decisionFile = decisionFileName(issue2, agent2);
3434
- const decisionPath = path17.join(loc.taskDir, decisionFile);
3435
- if (fs18.existsSync(decisionPath) && !opts.force) {
3818
+ const decisionPath = path20.join(loc.taskDir, decisionFile);
3819
+ if (fs21.existsSync(decisionPath) && !opts.force) {
3436
3820
  if (opts.ifNeeded) {
3437
3821
  kicks.push({
3438
3822
  issue: issue2.file,
@@ -3507,7 +3891,7 @@ async function propose(opts) {
3507
3891
  const final = await waitForAgentRun(k.agentRunId);
3508
3892
  if (final.status !== "done") {
3509
3893
  k.error = final.error || `runtime ${k.agentRunId} ended with ${final.status}`;
3510
- } else if (!fs18.existsSync(path17.join(loc.taskDir, k.decisionFile))) {
3894
+ } else if (!fs21.existsSync(path20.join(loc.taskDir, k.decisionFile))) {
3511
3895
  k.error = `runtime completed but ${k.decisionFile} was not written`;
3512
3896
  }
3513
3897
  } catch (err) {
@@ -3528,7 +3912,7 @@ async function consolidate(opts) {
3528
3912
  const kicks = [];
3529
3913
  const modelOpts = resolveModelOptions(opts.agent, { model: opts.model, effort: opts.effort }, opts.warn);
3530
3914
  for (const issue2 of issues) {
3531
- const files = fs18.readdirSync(loc.taskDir);
3915
+ const files = fs21.readdirSync(loc.taskDir);
3532
3916
  const proposalFiles = files.filter((f) => {
3533
3917
  const m = f.match(DECISION_FILE_RE);
3534
3918
  return !!m && m[1] === issue2.index && m[2] !== "consolidated";
@@ -3574,7 +3958,7 @@ async function consolidate(opts) {
3574
3958
  if (opts.wait) {
3575
3959
  try {
3576
3960
  const final = await waitForAgentRun(agentRunId);
3577
- const wrote = fs18.existsSync(path17.join(loc.taskDir, consolidatedFile));
3961
+ const wrote = fs21.existsSync(path20.join(loc.taskDir, consolidatedFile));
3578
3962
  if (final.status !== "done") {
3579
3963
  kick.error = final.error || `runtime ${agentRunId} ended with ${final.status}`;
3580
3964
  } else if (!wrote) {
@@ -3640,7 +4024,7 @@ async function approve(opts) {
3640
4024
  return { updated };
3641
4025
  }
3642
4026
  function buildReferenceFiles(taskDir, issue2) {
3643
- const names = fs18.readdirSync(taskDir);
4027
+ const names = fs21.readdirSync(taskDir);
3644
4028
  const refs = [issue2.file];
3645
4029
  if (names.includes("ISSUE.md")) refs.push("ISSUE.md");
3646
4030
  const ideaFile = `IDEA-${issue2.index}.md`;
@@ -3748,12 +4132,12 @@ issue.command("approve <taskId>").description("Apply the consolidated decisions
3748
4132
  });
3749
4133
 
3750
4134
  // src/commands/agent.ts
3751
- import fs19 from "fs";
3752
- import path18 from "path";
4135
+ import fs22 from "fs";
4136
+ import path21 from "path";
3753
4137
  import { spawnSync as spawnSync5 } from "child_process";
3754
4138
  import { Command as Command18 } from "commander";
3755
4139
  import chalk18 from "chalk";
3756
- import yaml7 from "js-yaml";
4140
+ import yaml8 from "js-yaml";
3757
4141
  function statusBadge(s) {
3758
4142
  if (s === "working") return chalk18.cyan("\u25CF");
3759
4143
  if (s === "error") return chalk18.red("\u25CF");
@@ -3813,10 +4197,10 @@ agent.command("get <ref>").description("Show one agent by object_id or slug").op
3813
4197
  }
3814
4198
  console.log();
3815
4199
  console.log(chalk18.dim("--- spec ---"));
3816
- console.log(yaml7.dump(result.agent.spec, { lineWidth: 100 }));
4200
+ console.log(yaml8.dump(result.agent.spec, { lineWidth: 100 }));
3817
4201
  if (result.agent.mcp_config) {
3818
4202
  console.log(chalk18.dim("--- mcp_config ---"));
3819
- console.log(yaml7.dump(result.agent.mcp_config, { lineWidth: 100 }));
4203
+ console.log(yaml8.dump(result.agent.mcp_config, { lineWidth: 100 }));
3820
4204
  }
3821
4205
  } catch (err) {
3822
4206
  const apiErr = err;
@@ -3827,8 +4211,8 @@ agent.command("get <ref>").description("Show one agent by object_id or slug").op
3827
4211
  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) => {
3828
4212
  let parsed;
3829
4213
  try {
3830
- const raw = fs19.readFileSync(path18.resolve(opts.fromFile), "utf-8");
3831
- parsed = yaml7.load(raw);
4214
+ const raw = fs22.readFileSync(path21.resolve(opts.fromFile), "utf-8");
4215
+ parsed = yaml8.load(raw);
3832
4216
  } catch (err) {
3833
4217
  fail5(`cannot read ${opts.fromFile}: ${err.message}`);
3834
4218
  }
@@ -3844,17 +4228,17 @@ agent.command("edit <ref>").description("Open the agent YAML in $EDITOR and save
3844
4228
  try {
3845
4229
  const result = await api.get(`/api/agents/${encodeURIComponent(ref)}`);
3846
4230
  if (result.agent.system) fail5("cannot edit a system agent");
3847
- const tmp = path18.join(
4231
+ const tmp = path21.join(
3848
4232
  process.env.TMPDIR || "/tmp",
3849
4233
  `task0-agent-${result.agent.slug}-${Date.now()}.yml`
3850
4234
  );
3851
- fs19.writeFileSync(tmp, yaml7.dump(result.agent, { lineWidth: 100 }), "utf-8");
4235
+ fs22.writeFileSync(tmp, yaml8.dump(result.agent, { lineWidth: 100 }), "utf-8");
3852
4236
  const editor = process.env.EDITOR || "vi";
3853
4237
  const r = spawnSync5(editor, [tmp], { stdio: "inherit" });
3854
4238
  if (r.status !== 0) fail5(`editor exited with status ${r.status}`);
3855
- const updated = yaml7.load(fs19.readFileSync(tmp, "utf-8"));
4239
+ const updated = yaml8.load(fs22.readFileSync(tmp, "utf-8"));
3856
4240
  await api.put(`/api/agents/${encodeURIComponent(ref)}`, updated);
3857
- fs19.unlinkSync(tmp);
4241
+ fs22.unlinkSync(tmp);
3858
4242
  console.log(chalk18.green(`updated ${ref}`));
3859
4243
  } catch (err) {
3860
4244
  fail5(err.message);
@@ -3925,7 +4309,7 @@ var mcp = new Command18("mcp").description("Manage per-agent MCP config (mcp_con
3925
4309
  mcp.command("set <ref>").description("Set mcp_config from a JSON file (replaces existing config)").requiredOption("--from-file <file>", "JSON file containing the mcp_config object").action(async (ref, opts) => {
3926
4310
  let parsed;
3927
4311
  try {
3928
- parsed = JSON.parse(fs19.readFileSync(path18.resolve(opts.fromFile), "utf-8"));
4312
+ parsed = JSON.parse(fs22.readFileSync(path21.resolve(opts.fromFile), "utf-8"));
3929
4313
  } catch (err) {
3930
4314
  fail5(`cannot parse ${opts.fromFile}: ${err.message}`);
3931
4315
  }
@@ -3968,78 +4352,76 @@ async function streamOutput(ref, agentRunId) {
3968
4352
  }
3969
4353
 
3970
4354
  // src/commands/daemon.ts
3971
- import os7 from "os";
4355
+ import os8 from "os";
3972
4356
  import { Command as Command19 } from "commander";
3973
4357
  import chalk19 from "chalk";
3974
4358
  import WebSocket from "ws";
3975
4359
 
3976
- // src/core/admin-token.ts
3977
- import fs20 from "fs";
3978
- import os4 from "os";
3979
- import path19 from "path";
3980
- var CONFIG_DIR2 = path19.join(os4.homedir(), ".config", "task0");
3981
- var TOKEN_FILE = path19.join(CONFIG_DIR2, "admin.token");
3982
- var cached = null;
3983
- var AdminTokenUnavailableError = class extends Error {
3984
- constructor() {
3985
- super(
3986
- `Admin token not found.
3987
- \u2022 If the task0 server runs on this host, run the task0-server binary once \u2014 the token will be generated at ${TOKEN_FILE}.
3988
- \u2022 Otherwise, copy the token from the server host and set TASK0_ADMIN_TOKEN.`
3989
- );
3990
- this.name = "AdminTokenUnavailableError";
3991
- }
3992
- };
3993
- function readAdminToken() {
3994
- if (cached) return cached;
3995
- const fromEnv = process.env.TASK0_ADMIN_TOKEN?.trim();
3996
- if (fromEnv) {
3997
- cached = fromEnv;
3998
- return cached;
3999
- }
4000
- if (fs20.existsSync(TOKEN_FILE)) {
4001
- const v = fs20.readFileSync(TOKEN_FILE, "utf-8").trim();
4002
- if (v) {
4003
- cached = v;
4004
- return cached;
4360
+ // src/core/daemon-agent-run-sink.ts
4361
+ var BUFFER_CAP = 5e3;
4362
+ var bound = null;
4363
+ var seqByRun = /* @__PURE__ */ new Map();
4364
+ var buffer = [];
4365
+ var logHistoryByRun = /* @__PURE__ */ new Map();
4366
+ function bindAgentRunFrameSink(sink) {
4367
+ bound = sink;
4368
+ if (!sink) return;
4369
+ while (buffer.length > 0) {
4370
+ const frame = buffer.shift();
4371
+ try {
4372
+ sink.send(frame);
4373
+ } catch {
4005
4374
  }
4006
4375
  }
4007
- throw new AdminTokenUnavailableError();
4008
4376
  }
4009
- function adminAuthHeader() {
4010
- return { authorization: `Bearer ${readAdminToken()}` };
4011
- }
4012
-
4013
- // src/core/daemon-config.ts
4014
- import fs21 from "fs";
4015
- import os5 from "os";
4016
- import path20 from "path";
4017
- var CONFIG_DIR3 = path20.join(os5.homedir(), ".config", "task0");
4018
- var CONFIG_FILE2 = path20.join(CONFIG_DIR3, "daemon.json");
4019
- function daemonConfigPath() {
4020
- return CONFIG_FILE2;
4021
- }
4022
- function readDaemonIdentity() {
4023
- if (!fs21.existsSync(CONFIG_FILE2)) return null;
4024
- try {
4025
- const raw = fs21.readFileSync(CONFIG_FILE2, "utf-8");
4026
- return JSON.parse(raw);
4027
- } catch {
4028
- return null;
4377
+ function deliverOrBuffer(frame) {
4378
+ if (bound) {
4379
+ try {
4380
+ bound.send(frame);
4381
+ return;
4382
+ } catch {
4383
+ }
4029
4384
  }
4030
- }
4031
- function writeDaemonIdentity(identity) {
4032
- fs21.mkdirSync(CONFIG_DIR3, { recursive: true });
4033
- fs21.writeFileSync(CONFIG_FILE2, JSON.stringify(identity, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
4034
- try {
4035
- fs21.chmodSync(CONFIG_FILE2, 384);
4036
- } catch {
4385
+ if (buffer.length >= BUFFER_CAP) {
4386
+ buffer.shift();
4037
4387
  }
4388
+ buffer.push(frame);
4038
4389
  }
4039
- function clearDaemonIdentity() {
4040
- if (!fs21.existsSync(CONFIG_FILE2)) return false;
4041
- fs21.unlinkSync(CONFIG_FILE2);
4042
- return true;
4390
+ function pruneRunHistory(runId) {
4391
+ logHistoryByRun.delete(runId);
4392
+ seqByRun.delete(runId);
4393
+ }
4394
+ function emitAgentRunStatus(runId, status, opts = {}) {
4395
+ deliverOrBuffer({
4396
+ type: "agent_run_status",
4397
+ run_id: runId,
4398
+ status,
4399
+ phase: opts.phase ?? null,
4400
+ exit_code: opts.exitCode ?? null,
4401
+ error: opts.error ?? null,
4402
+ ts: (/* @__PURE__ */ new Date()).toISOString()
4403
+ });
4404
+ if (status === "completed" || status === "failed" || status === "killed") {
4405
+ pruneRunHistory(runId);
4406
+ }
4407
+ }
4408
+ function replayAfterRanges(ranges) {
4409
+ if (!bound) return 0;
4410
+ let sent = 0;
4411
+ for (const { run_id, after_seq } of ranges) {
4412
+ const ring = logHistoryByRun.get(run_id);
4413
+ if (!ring) continue;
4414
+ for (const frame of ring) {
4415
+ if (frame.seq <= after_seq) continue;
4416
+ try {
4417
+ bound.send(frame);
4418
+ sent += 1;
4419
+ } catch {
4420
+ return sent;
4421
+ }
4422
+ }
4423
+ }
4424
+ return sent;
4043
4425
  }
4044
4426
 
4045
4427
  // src/core/register-auth.ts
@@ -4075,8 +4457,191 @@ function pickRegisterAuth(flagToken) {
4075
4457
 
4076
4458
  // src/core/daemon-rpc-handlers.ts
4077
4459
  init_node();
4078
- import fs22 from "fs";
4079
- import path21 from "path";
4460
+ import fs25 from "fs";
4461
+ import path24 from "path";
4462
+
4463
+ // src/core/daemon-agent-run-runner.ts
4464
+ import { execFileSync } from "child_process";
4465
+ import fs24 from "fs";
4466
+ import path23 from "path";
4467
+
4468
+ // src/core/daemon-agent-run-dir.ts
4469
+ import fs23 from "fs";
4470
+ import os6 from "os";
4471
+ import path22 from "path";
4472
+ function agentRunRoot() {
4473
+ return process.env.TASK0_DAEMON_AGENT_RUN_DIR || path22.join(os6.homedir(), ".config", "task0", "agent-run");
4474
+ }
4475
+ function agentRunDir(runId) {
4476
+ return path22.join(agentRunRoot(), runId);
4477
+ }
4478
+ function agentRunStatusPath(runId) {
4479
+ return path22.join(agentRunDir(runId), "status.json");
4480
+ }
4481
+ function ensureAgentRunDir(runId) {
4482
+ const dir = agentRunDir(runId);
4483
+ fs23.mkdirSync(dir, { recursive: true });
4484
+ return dir;
4485
+ }
4486
+ function removeAgentRunDir(runId) {
4487
+ const dir = agentRunDir(runId);
4488
+ if (!fs23.existsSync(dir)) return;
4489
+ fs23.rmSync(dir, { recursive: true, force: true });
4490
+ }
4491
+
4492
+ // src/core/daemon-agent-run-runner.ts
4493
+ var active = /* @__PURE__ */ new Map();
4494
+ var STATUS_POLL_MS = 500;
4495
+ var SESSION_PROBE_MS = 3e3;
4496
+ function getTmuxBin() {
4497
+ const fromEnv = process.env.TASK0_TMUX_BIN;
4498
+ if (fromEnv) return fromEnv;
4499
+ return "tmux";
4500
+ }
4501
+ function checkTmuxAvailable() {
4502
+ try {
4503
+ execFileSync(getTmuxBin(), ["-V"], { stdio: ["ignore", "pipe", "pipe"] });
4504
+ return { ok: true };
4505
+ } catch (err) {
4506
+ return { ok: false, reason: `tmux not available: ${err instanceof Error ? err.message : String(err)}` };
4507
+ }
4508
+ }
4509
+ function isSessionAlive2(sessionName) {
4510
+ try {
4511
+ execFileSync(getTmuxBin(), ["has-session", "-t", sessionName], {
4512
+ stdio: ["ignore", "pipe", "pipe"]
4513
+ });
4514
+ return true;
4515
+ } catch {
4516
+ return false;
4517
+ }
4518
+ }
4519
+ function killSession2(sessionName) {
4520
+ try {
4521
+ execFileSync(getTmuxBin(), ["kill-session", "-t", sessionName], {
4522
+ stdio: ["ignore", "pipe", "pipe"]
4523
+ });
4524
+ return true;
4525
+ } catch {
4526
+ return false;
4527
+ }
4528
+ }
4529
+ function readStatusFile(runId) {
4530
+ const filePath = agentRunStatusPath(runId);
4531
+ if (!fs24.existsSync(filePath)) return null;
4532
+ try {
4533
+ const raw = fs24.readFileSync(filePath, "utf-8");
4534
+ const parsed = JSON.parse(raw);
4535
+ return { raw, parsed };
4536
+ } catch {
4537
+ return null;
4538
+ }
4539
+ }
4540
+ function deriveStatus(parsed) {
4541
+ const raw = typeof parsed.status === "string" ? parsed.status : "starting";
4542
+ let status = "starting";
4543
+ if (raw === "running") status = "running";
4544
+ else if (raw === "done" || raw === "completed") status = "completed";
4545
+ else if (raw === "error" || raw === "failed") status = "failed";
4546
+ const phase = typeof parsed.phase === "string" ? parsed.phase : null;
4547
+ const error2 = typeof parsed.error === "string" && parsed.error ? parsed.error : null;
4548
+ return { status, phase, error: error2 };
4549
+ }
4550
+ function launchAgentRun(params) {
4551
+ const tmuxCheck = checkTmuxAvailable();
4552
+ if (!tmuxCheck.ok) {
4553
+ throw Object.assign(new Error(tmuxCheck.reason ?? "tmux unavailable"), { code: "tmux_unavailable" });
4554
+ }
4555
+ if (active.has(params.runId)) {
4556
+ throw Object.assign(new Error(`agent run already active: ${params.runId}`), { code: "already_running" });
4557
+ }
4558
+ if (!fs24.existsSync(params.workspace)) {
4559
+ throw Object.assign(new Error(`workspace not found: ${params.workspace}`), { code: "workspace_missing" });
4560
+ }
4561
+ const runDir = ensureAgentRunDir(params.runId);
4562
+ const scriptPath = path23.join(runDir, "script.sh");
4563
+ fs24.writeFileSync(scriptPath, params.scriptContent, { mode: 493 });
4564
+ if (params.promptContent !== void 0) {
4565
+ fs24.writeFileSync(path23.join(runDir, "prompt.txt"), params.promptContent, "utf-8");
4566
+ }
4567
+ for (const [name, content] of Object.entries(params.auxFiles ?? {})) {
4568
+ if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
4569
+ throw Object.assign(new Error(`invalid aux file name: ${name}`), { code: "invalid_params" });
4570
+ }
4571
+ fs24.writeFileSync(path23.join(runDir, name), content, "utf-8");
4572
+ }
4573
+ fs24.writeFileSync(
4574
+ agentRunStatusPath(params.runId),
4575
+ JSON.stringify({ status: "starting", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
4576
+ "utf-8"
4577
+ );
4578
+ try {
4579
+ execFileSync(
4580
+ getTmuxBin(),
4581
+ ["new-session", "-d", "-s", params.sessionName, "-c", params.workspace, `bash ${JSON.stringify(scriptPath)}`],
4582
+ { encoding: "utf-8", timeout: 5e3, stdio: ["ignore", "pipe", "pipe"] }
4583
+ );
4584
+ } catch (err) {
4585
+ removeAgentRunDir(params.runId);
4586
+ throw Object.assign(
4587
+ new Error(`failed to start tmux session: ${err instanceof Error ? err.message : String(err)}`),
4588
+ { code: "spawn_failed" }
4589
+ );
4590
+ }
4591
+ emitAgentRunStatus(params.runId, "starting", { phase: null });
4592
+ const entry = {
4593
+ runId: params.runId,
4594
+ sessionName: params.sessionName,
4595
+ lastStatusJson: null,
4596
+ finished: false,
4597
+ pollInterval: setInterval(() => pollOne(params.runId), STATUS_POLL_MS)
4598
+ };
4599
+ active.set(params.runId, entry);
4600
+ const sessionProbe = setInterval(() => {
4601
+ const cur = active.get(params.runId);
4602
+ if (!cur || cur.finished) {
4603
+ clearInterval(sessionProbe);
4604
+ return;
4605
+ }
4606
+ if (!isSessionAlive2(cur.sessionName)) {
4607
+ finishRun(params.runId, "killed", { error: "tmux session disappeared" });
4608
+ clearInterval(sessionProbe);
4609
+ }
4610
+ }, SESSION_PROBE_MS);
4611
+ return { agentRunDir: runDir, sessionName: params.sessionName };
4612
+ }
4613
+ function pollOne(runId) {
4614
+ const entry = active.get(runId);
4615
+ if (!entry || entry.finished) return;
4616
+ const current = readStatusFile(runId);
4617
+ if (!current) return;
4618
+ if (current.raw === entry.lastStatusJson) return;
4619
+ entry.lastStatusJson = current.raw;
4620
+ const { status, phase, error: error2 } = deriveStatus(current.parsed);
4621
+ emitAgentRunStatus(runId, status, { phase, error: error2 });
4622
+ if (status === "completed" || status === "failed") {
4623
+ finishRun(runId, status, { phase, error: error2 });
4624
+ }
4625
+ }
4626
+ function finishRun(runId, status, opts = {}) {
4627
+ const entry = active.get(runId);
4628
+ if (!entry || entry.finished) return;
4629
+ entry.finished = true;
4630
+ clearInterval(entry.pollInterval);
4631
+ emitAgentRunStatus(runId, status, opts);
4632
+ active.delete(runId);
4633
+ }
4634
+ function cancelAgentRun(runId) {
4635
+ const entry = active.get(runId);
4636
+ if (!entry) {
4637
+ return { ok: false, sessionName: null };
4638
+ }
4639
+ killSession2(entry.sessionName);
4640
+ finishRun(runId, "killed", { error: "cancelled by hub" });
4641
+ return { ok: true, sessionName: entry.sessionName };
4642
+ }
4643
+
4644
+ // src/core/daemon-rpc-handlers.ts
4080
4645
  var MAX_FILE_BYTES = 1 * 1024 * 1024;
4081
4646
  function ensureString(value, name) {
4082
4647
  if (typeof value !== "string" || value.length === 0) {
@@ -4084,13 +4649,47 @@ function ensureString(value, name) {
4084
4649
  }
4085
4650
  return value;
4086
4651
  }
4652
+ function optionalString(value) {
4653
+ if (value === void 0 || value === null) return void 0;
4654
+ if (typeof value !== "string") {
4655
+ throw Object.assign(new Error("expected string"), { code: "invalid_params" });
4656
+ }
4657
+ return value;
4658
+ }
4659
+ function optionalBoolean(value) {
4660
+ if (value === void 0 || value === null) return void 0;
4661
+ if (typeof value !== "boolean") {
4662
+ throw Object.assign(new Error("expected boolean"), { code: "invalid_params" });
4663
+ }
4664
+ return value;
4665
+ }
4666
+ function listProjects() {
4667
+ return loadConfig().sources.filter((source2) => source2.type === "project");
4668
+ }
4669
+ function applyRepair(r) {
4670
+ const raw = readYaml(r.taskYml);
4671
+ if (!raw) return;
4672
+ if (r.reason === "missing_object_id") raw.object_id = generateObjectId("task");
4673
+ if (r.reason === "id_mismatch") raw.id = r.dirName;
4674
+ writeYaml(r.taskYml, raw);
4675
+ }
4676
+ function scanProjectWithRepair(projectPath, sourceName) {
4677
+ let result = scanProject(projectPath, sourceName);
4678
+ if (result.repairs.length > 0) {
4679
+ for (const r of result.repairs) applyRepair(r);
4680
+ result = scanProject(projectPath, sourceName);
4681
+ }
4682
+ return result;
4683
+ }
4087
4684
  var rpcHandlers = {
4088
4685
  // Scan a local project for its task manifest. Returns the same shape the
4089
- // in-process server's /api/tasks scanProject() returns.
4686
+ // hub's in-process scanProject() used to return — with auto-repair of
4687
+ // missing object_id / id mismatches applied in place on the daemon's FS.
4090
4688
  async scan_project(params) {
4091
4689
  const rootPath = ensureString(params.rootPath, "rootPath");
4092
- const name = typeof params.name === "string" && params.name ? params.name : path21.basename(rootPath);
4093
- return scanProject(rootPath, name);
4690
+ const name = typeof params.name === "string" && params.name ? params.name : path24.basename(rootPath);
4691
+ const result = scanProjectWithRepair(rootPath, name);
4692
+ return { tasks: result.tasks, errors: result.errors };
4094
4693
  },
4095
4694
  // Read a file from disk on the daemon's host. The dashboard uses this to
4096
4695
  // peek into archived task content, runtime logs, etc. Cap is 1 MiB to
@@ -4099,7 +4698,7 @@ var rpcHandlers = {
4099
4698
  const filePath = ensureString(params.path, "path");
4100
4699
  let stat;
4101
4700
  try {
4102
- stat = fs22.statSync(filePath);
4701
+ stat = fs25.statSync(filePath);
4103
4702
  } catch {
4104
4703
  throw Object.assign(new Error("file not found"), { code: "not_found" });
4105
4704
  }
@@ -4109,23 +4708,253 @@ var rpcHandlers = {
4109
4708
  if (stat.size > MAX_FILE_BYTES) {
4110
4709
  throw Object.assign(new Error(`file too large (${stat.size} bytes > ${MAX_FILE_BYTES})`), { code: "too_large" });
4111
4710
  }
4112
- const content = fs22.readFileSync(filePath, "utf-8");
4711
+ const content = fs25.readFileSync(filePath, "utf-8");
4113
4712
  return { content, size: stat.size, modifiedAt: stat.mtime.toISOString() };
4713
+ },
4714
+ // ---------------------------------------------------------------------
4715
+ // Project source CRUD. The daemon owns its host's project list; the hub
4716
+ // routes /api/daemons/:id/projects through these handlers instead of
4717
+ // writing config.yml directly. After every mutation we re-push manifest
4718
+ // so the hub's daemon_projects table catches up without waiting for a
4719
+ // restart.
4720
+ // ---------------------------------------------------------------------
4721
+ async project_list() {
4722
+ return { projects: listProjects() };
4723
+ },
4724
+ async project_add(params, ctx) {
4725
+ const rawPath = ensureString(params.path, "path");
4726
+ const absPath = path24.resolve(rawPath);
4727
+ if (!fs25.existsSync(absPath)) {
4728
+ throw Object.assign(new Error(`path does not exist: ${absPath}`), { code: "not_found" });
4729
+ }
4730
+ const stat = fs25.statSync(absPath);
4731
+ if (!stat.isDirectory()) {
4732
+ throw Object.assign(new Error(`path is not a directory: ${absPath}`), { code: "invalid_target" });
4733
+ }
4734
+ const name = optionalString(params.name)?.trim() || path24.basename(absPath);
4735
+ const result = scanProjectWithRepair(absPath, name);
4736
+ addSource({ name, type: "project", path: absPath, enabled: true });
4737
+ ctx.notifyManifestChanged();
4738
+ return {
4739
+ project: { name, type: "project", path: absPath, enabled: true },
4740
+ taskCount: result.tasks.length
4741
+ };
4742
+ },
4743
+ async project_remove(params, ctx) {
4744
+ const name = ensureString(params.name, "name");
4745
+ const ok = removeSource(name);
4746
+ if (!ok) {
4747
+ throw Object.assign(new Error(`project not found: ${name}`), { code: "not_found" });
4748
+ }
4749
+ ctx.notifyManifestChanged();
4750
+ return { ok: true };
4751
+ },
4752
+ async project_set_enabled(params, ctx) {
4753
+ const name = ensureString(params.name, "name");
4754
+ const enabled = optionalBoolean(params.enabled);
4755
+ if (enabled === void 0) {
4756
+ throw Object.assign(new Error("enabled is required (boolean)"), { code: "invalid_params" });
4757
+ }
4758
+ const config = loadConfig();
4759
+ const source2 = config.sources.find((s) => s.name === name && s.type === "project");
4760
+ if (!source2) {
4761
+ throw Object.assign(new Error(`project not found: ${name}`), { code: "not_found" });
4762
+ }
4763
+ source2.enabled = enabled;
4764
+ saveConfig(config);
4765
+ ctx.notifyManifestChanged();
4766
+ return { project: source2 };
4767
+ },
4768
+ // ---------------------------------------------------------------------
4769
+ // Task content R/W. These are the deep-pull operations from Q5(c): the
4770
+ // hub asks the daemon to read/write/create/delete a task's yaml + sibling
4771
+ // files on the daemon's FS. After every mutation we re-push manifest so
4772
+ // hub's daemon_tasks index reflects the new state without a restart.
4773
+ // ---------------------------------------------------------------------
4774
+ // Read a task's task0.yml (parsed) plus an optional list of sibling files
4775
+ // in the same task directory. Path is resolved from (projectName, taskId).
4776
+ async task_read(params) {
4777
+ const projectName = ensureString(params.projectName, "projectName");
4778
+ const taskId = ensureString(params.taskId, "taskId");
4779
+ const project2 = listProjects().find((p) => p.name === projectName);
4780
+ if (!project2) {
4781
+ throw Object.assign(new Error(`project not found: ${projectName}`), { code: "not_found" });
4782
+ }
4783
+ const projectAbs = path24.resolve(project2.path);
4784
+ const projectConfig = readYaml(path24.join(projectAbs, "task0.yml"));
4785
+ if (!projectConfig) {
4786
+ throw Object.assign(new Error(`invalid project: missing task0.yml at ${projectAbs}`), { code: "invalid_project" });
4787
+ }
4788
+ const taskDir = path24.join(projectAbs, projectConfig.tasks_dir, taskId);
4789
+ if (!fs25.existsSync(taskDir)) {
4790
+ throw Object.assign(new Error(`task not found: ${taskId}`), { code: "not_found" });
4791
+ }
4792
+ const taskYml = path24.join(taskDir, "task0.yml");
4793
+ const yaml9 = readYaml(taskYml);
4794
+ if (!yaml9) {
4795
+ throw Object.assign(new Error(`task yaml missing or unreadable: ${taskYml}`), { code: "not_found" });
4796
+ }
4797
+ const files = fs25.readdirSync(taskDir).filter((name) => name !== "task0.yml");
4798
+ return { task_dir: taskDir, yaml: yaml9, files };
4799
+ },
4800
+ // Create a task directory + write task0.yml + write any additional named
4801
+ // files. Hub decides the taskId and yaml content (slug/object_id are
4802
+ // generated server-side); daemon just commits to disk.
4803
+ async task_create(params, ctx) {
4804
+ const projectName = ensureString(params.projectName, "projectName");
4805
+ const taskId = ensureString(params.taskId, "taskId");
4806
+ const yaml9 = params.yaml;
4807
+ if (!yaml9 || typeof yaml9 !== "object") {
4808
+ throw Object.assign(new Error("yaml (object) is required"), { code: "invalid_params" });
4809
+ }
4810
+ const files = params.files ?? {};
4811
+ const project2 = listProjects().find((p) => p.name === projectName);
4812
+ if (!project2) {
4813
+ throw Object.assign(new Error(`project not found: ${projectName}`), { code: "not_found" });
4814
+ }
4815
+ const projectAbs = path24.resolve(project2.path);
4816
+ const projectConfig = readYaml(path24.join(projectAbs, "task0.yml"));
4817
+ if (!projectConfig) {
4818
+ throw Object.assign(new Error(`invalid project: missing task0.yml at ${projectAbs}`), { code: "invalid_project" });
4819
+ }
4820
+ const taskDir = path24.join(projectAbs, projectConfig.tasks_dir, taskId);
4821
+ if (fs25.existsSync(taskDir)) {
4822
+ throw Object.assign(new Error(`task already exists: ${taskId}`), { code: "already_exists" });
4823
+ }
4824
+ fs25.mkdirSync(taskDir, { recursive: true });
4825
+ writeYaml(path24.join(taskDir, "task0.yml"), yaml9);
4826
+ for (const [name, content] of Object.entries(files)) {
4827
+ if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
4828
+ throw Object.assign(new Error(`invalid file name: ${name}`), { code: "invalid_params" });
4829
+ }
4830
+ fs25.writeFileSync(path24.join(taskDir, name), content, "utf-8");
4831
+ }
4832
+ ctx.notifyManifestChanged();
4833
+ return { task_dir: taskDir };
4834
+ },
4835
+ // Patch a task's task0.yml in place. The hub computes the desired yaml
4836
+ // (typically by loading + applying its own metadata patch), then sends
4837
+ // the full replacement object back. Atomic at the file level — no
4838
+ // partial writes.
4839
+ async task_write_yaml(params, ctx) {
4840
+ const projectName = ensureString(params.projectName, "projectName");
4841
+ const taskId = ensureString(params.taskId, "taskId");
4842
+ const yaml9 = params.yaml;
4843
+ if (!yaml9 || typeof yaml9 !== "object") {
4844
+ throw Object.assign(new Error("yaml (object) is required"), { code: "invalid_params" });
4845
+ }
4846
+ const project2 = listProjects().find((p) => p.name === projectName);
4847
+ if (!project2) {
4848
+ throw Object.assign(new Error(`project not found: ${projectName}`), { code: "not_found" });
4849
+ }
4850
+ const projectAbs = path24.resolve(project2.path);
4851
+ const projectConfig = readYaml(path24.join(projectAbs, "task0.yml"));
4852
+ if (!projectConfig) {
4853
+ throw Object.assign(new Error(`invalid project: missing task0.yml at ${projectAbs}`), { code: "invalid_project" });
4854
+ }
4855
+ const taskDir = path24.join(projectAbs, projectConfig.tasks_dir, taskId);
4856
+ const taskYml = path24.join(taskDir, "task0.yml");
4857
+ if (!fs25.existsSync(taskYml)) {
4858
+ throw Object.assign(new Error(`task yaml not found: ${taskYml}`), { code: "not_found" });
4859
+ }
4860
+ writeYaml(taskYml, yaml9);
4861
+ ctx.notifyManifestChanged();
4862
+ return { task_dir: taskDir };
4863
+ },
4864
+ // Delete an entire task directory (use sparingly; mostly for archive
4865
+ // workflows). Returns the deleted path so the hub can emit an event.
4866
+ async task_delete(params, ctx) {
4867
+ const projectName = ensureString(params.projectName, "projectName");
4868
+ const taskId = ensureString(params.taskId, "taskId");
4869
+ const project2 = listProjects().find((p) => p.name === projectName);
4870
+ if (!project2) {
4871
+ throw Object.assign(new Error(`project not found: ${projectName}`), { code: "not_found" });
4872
+ }
4873
+ const projectAbs = path24.resolve(project2.path);
4874
+ const projectConfig = readYaml(path24.join(projectAbs, "task0.yml"));
4875
+ if (!projectConfig) {
4876
+ throw Object.assign(new Error(`invalid project: missing task0.yml at ${projectAbs}`), { code: "invalid_project" });
4877
+ }
4878
+ const taskDir = path24.join(projectAbs, projectConfig.tasks_dir, taskId);
4879
+ if (!fs25.existsSync(taskDir)) {
4880
+ throw Object.assign(new Error(`task not found: ${taskId}`), { code: "not_found" });
4881
+ }
4882
+ fs25.rmSync(taskDir, { recursive: true, force: true });
4883
+ ctx.notifyManifestChanged();
4884
+ return { task_dir: taskDir, deleted: true };
4885
+ },
4886
+ // ---------------------------------------------------------------------
4887
+ // Agent run lifecycle (P3.A scaffolding — handlers reject until P3.B
4888
+ // moves the actual spawn / tmux / supervise logic from hub's
4889
+ // agent-run-launcher.ts into the daemon).
4890
+ //
4891
+ // Wire shape (subject to refinement in P3.B):
4892
+ // agent_run_launch → {projectName, runId, runType, agentSlug,
4893
+ // workspace, prompt, ...} → {tmuxSession}
4894
+ // Daemon then streams agent_run_log /
4895
+ // agent_run_status frames over WS.
4896
+ // agent_run_cancel → {runId} → {ok: true}
4897
+ // ---------------------------------------------------------------------
4898
+ // Skill discovery on the daemon's host. Mirrors hub-side
4899
+ // resolveAgentSkill() — given a workspace + agent + filePath, returns
4900
+ // the matching AgentSkill record or null. P3.C uses this so the hub
4901
+ // can dispatch agent runs to remote daemons without itself needing FS
4902
+ // access to the workspace.
4903
+ async agent_skill_resolve(params) {
4904
+ const workspace = ensureString(params.workspace, "workspace");
4905
+ const agent2 = ensureString(params.agent, "agent");
4906
+ const filePath = ensureString(params.filePath, "filePath");
4907
+ if (agent2 !== "claude_code" && agent2 !== "codex" && agent2 !== "cursor") {
4908
+ throw Object.assign(new Error(`unknown skill agent: ${agent2}`), { code: "invalid_params" });
4909
+ }
4910
+ const projectSkills = getProjectAgentSkills(workspace);
4911
+ const globalSkills = getGlobalAgentSkills();
4912
+ const all = [...projectSkills, ...globalSkills];
4913
+ const resolved = path24.resolve(filePath);
4914
+ const skill = all.find(
4915
+ (s) => s.agent === agent2 && path24.resolve(s.filePath) === resolved
4916
+ ) ?? null;
4917
+ return { skill };
4918
+ },
4919
+ async agent_run_launch(params) {
4920
+ const runId = ensureString(params.runId, "runId");
4921
+ const sessionName = ensureString(params.sessionName, "sessionName");
4922
+ const workspace = ensureString(params.workspace, "workspace");
4923
+ const scriptContent = ensureString(params.scriptContent, "scriptContent");
4924
+ const auxFiles = params.auxFiles && typeof params.auxFiles === "object" ? params.auxFiles : {};
4925
+ const promptContent = typeof params.promptContent === "string" ? params.promptContent : void 0;
4926
+ const { agentRunDir: agentRunDir2, sessionName: actualSessionName } = launchAgentRun({
4927
+ runId,
4928
+ sessionName,
4929
+ workspace,
4930
+ scriptContent,
4931
+ auxFiles,
4932
+ promptContent
4933
+ });
4934
+ return { agent_run_dir: agentRunDir2, tmux_session: actualSessionName };
4935
+ },
4936
+ async agent_run_cancel(params) {
4937
+ const runId = ensureString(params.runId, "runId");
4938
+ const result = cancelAgentRun(runId);
4939
+ if (!result.ok) {
4940
+ throw Object.assign(new Error(`no active agent run: ${runId}`), { code: "not_found" });
4941
+ }
4942
+ return { ok: true, tmux_session: result.sessionName };
4114
4943
  }
4115
4944
  };
4116
4945
 
4117
4946
  // src/core/daemon-service/launchd.ts
4118
4947
  import { spawnSync as spawnSync6 } from "child_process";
4119
- import fs24 from "fs";
4120
- import path23 from "path";
4948
+ import fs27 from "fs";
4949
+ import path26 from "path";
4121
4950
 
4122
4951
  // src/core/daemon-service/binary.ts
4123
- import fs23 from "fs";
4952
+ import fs26 from "fs";
4124
4953
  import { fileURLToPath } from "url";
4125
4954
  function resolveTask0Invocation() {
4126
4955
  const node = process.execPath;
4127
4956
  const argv1 = process.argv[1] ?? fileURLToPath(import.meta.url);
4128
- const main2 = fs23.realpathSync(argv1);
4957
+ const main2 = fs26.realpathSync(argv1);
4129
4958
  if (!isInstalledBuild(main2) && process.env.TASK0_ALLOW_DEV_SERVICE !== "1") {
4130
4959
  throw new Error(
4131
4960
  `Refusing to install autostart service pointing at ${main2}.
@@ -4141,8 +4970,8 @@ function isInstalledBuild(p) {
4141
4970
  }
4142
4971
 
4143
4972
  // src/core/daemon-service/paths.ts
4144
- import os6 from "os";
4145
- import path22 from "path";
4973
+ import os7 from "os";
4974
+ import path25 from "path";
4146
4975
 
4147
4976
  // src/core/daemon-service/types.ts
4148
4977
  var SERVICE_LABEL = "cc.cy0.task0";
@@ -4151,21 +4980,21 @@ var SERVICE_LABEL = "cc.cy0.task0";
4151
4980
  function unitPath(scope) {
4152
4981
  const platform = process.platform;
4153
4982
  if (platform === "darwin") {
4154
- return scope === "user" ? path22.join(os6.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`) : path22.join("/", "Library", "LaunchDaemons", `${SERVICE_LABEL}.plist`);
4983
+ return scope === "user" ? path25.join(os7.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`) : path25.join("/", "Library", "LaunchDaemons", `${SERVICE_LABEL}.plist`);
4155
4984
  }
4156
4985
  if (platform === "linux") {
4157
- return scope === "user" ? path22.join(os6.homedir(), ".config", "systemd", "user", `${SERVICE_LABEL}.service`) : path22.join("/", "etc", "systemd", "system", `${SERVICE_LABEL}.service`);
4986
+ return scope === "user" ? path25.join(os7.homedir(), ".config", "systemd", "user", `${SERVICE_LABEL}.service`) : path25.join("/", "etc", "systemd", "system", `${SERVICE_LABEL}.service`);
4158
4987
  }
4159
4988
  throw new Error(`Unsupported platform for service install: ${platform}`);
4160
4989
  }
4161
4990
  function logDir() {
4162
- return path22.join(os6.homedir(), ".task0", "logs");
4991
+ return path25.join(os7.homedir(), ".task0", "logs");
4163
4992
  }
4164
4993
  function logPaths() {
4165
4994
  const dir = logDir();
4166
4995
  return {
4167
- out: path22.join(dir, "daemon.out.log"),
4168
- err: path22.join(dir, "daemon.err.log")
4996
+ out: path25.join(dir, "daemon.out.log"),
4997
+ err: path25.join(dir, "daemon.err.log")
4169
4998
  };
4170
4999
  }
4171
5000
 
@@ -4226,8 +5055,8 @@ function createLaunchdManager(scope) {
4226
5055
  const logs = logPaths();
4227
5056
  async function install() {
4228
5057
  const inv = resolveTask0Invocation();
4229
- fs24.mkdirSync(logDir(), { recursive: true });
4230
- fs24.mkdirSync(path23.dirname(file), { recursive: true });
5058
+ fs27.mkdirSync(logDir(), { recursive: true });
5059
+ fs27.mkdirSync(path26.dirname(file), { recursive: true });
4231
5060
  const body = renderPlist({
4232
5061
  node: inv.node,
4233
5062
  main: inv.main,
@@ -4236,7 +5065,7 @@ function createLaunchdManager(scope) {
4236
5065
  out: logs.out,
4237
5066
  err: logs.err
4238
5067
  });
4239
- fs24.writeFileSync(file, body, { mode: 420 });
5068
+ fs27.writeFileSync(file, body, { mode: 420 });
4240
5069
  const bootstrap = run2("launchctl", ["bootstrap", domainTarget(scope), file]);
4241
5070
  if (bootstrap.code !== 0) {
4242
5071
  const already = /already loaded|service already bootstrapped/i.test(bootstrap.stderr);
@@ -4262,9 +5091,9 @@ function createLaunchdManager(scope) {
4262
5091
  }
4263
5092
  async function uninstall() {
4264
5093
  run2("launchctl", ["bootout", serviceTarget(scope)]);
4265
- if (fs24.existsSync(file)) {
5094
+ if (fs27.existsSync(file)) {
4266
5095
  run2("launchctl", ["unload", file]);
4267
- fs24.unlinkSync(file);
5096
+ fs27.unlinkSync(file);
4268
5097
  }
4269
5098
  }
4270
5099
  async function start() {
@@ -4283,7 +5112,7 @@ function createLaunchdManager(scope) {
4283
5112
  }
4284
5113
  }
4285
5114
  async function status() {
4286
- if (!fs24.existsSync(file)) return "absent";
5115
+ if (!fs27.existsSync(file)) return "absent";
4287
5116
  const printed = run2("launchctl", ["print", serviceTarget(scope)]);
4288
5117
  if (printed.code !== 0) return "installed";
4289
5118
  const out = printed.stdout;
@@ -4306,8 +5135,8 @@ function createLaunchdManager(scope) {
4306
5135
 
4307
5136
  // src/core/daemon-service/systemd.ts
4308
5137
  import { spawnSync as spawnSync7 } from "child_process";
4309
- import fs25 from "fs";
4310
- import path24 from "path";
5138
+ import fs28 from "fs";
5139
+ import path27 from "path";
4311
5140
  function shellEscape(s) {
4312
5141
  if (!/[\s"\\$]/.test(s)) return s;
4313
5142
  return `"${s.replace(/[\\"]/g, (m) => `\\${m}`)}"`;
@@ -4346,8 +5175,8 @@ function createSystemdManager(scope) {
4346
5175
  const unitName = `${SERVICE_LABEL}.service`;
4347
5176
  async function install() {
4348
5177
  const inv = resolveTask0Invocation();
4349
- fs25.mkdirSync(logDir(), { recursive: true });
4350
- fs25.mkdirSync(path24.dirname(file), { recursive: true });
5178
+ fs28.mkdirSync(logDir(), { recursive: true });
5179
+ fs28.mkdirSync(path27.dirname(file), { recursive: true });
4351
5180
  const body = renderUnit({
4352
5181
  node: inv.node,
4353
5182
  main: inv.main,
@@ -4356,7 +5185,7 @@ function createSystemdManager(scope) {
4356
5185
  err: logs.err,
4357
5186
  scope
4358
5187
  });
4359
- fs25.writeFileSync(file, body, { mode: 420 });
5188
+ fs28.writeFileSync(file, body, { mode: 420 });
4360
5189
  const reload = run3("systemctl", [...scopeFlag(scope), "daemon-reload"]);
4361
5190
  if (reload.code !== 0) {
4362
5191
  throw new Error(`systemctl daemon-reload failed: ${reload.stderr}`);
@@ -4365,8 +5194,8 @@ function createSystemdManager(scope) {
4365
5194
  }
4366
5195
  async function uninstall() {
4367
5196
  run3("systemctl", [...scopeFlag(scope), "disable", "--now", unitName]);
4368
- if (fs25.existsSync(file)) {
4369
- fs25.unlinkSync(file);
5197
+ if (fs28.existsSync(file)) {
5198
+ fs28.unlinkSync(file);
4370
5199
  }
4371
5200
  run3("systemctl", [...scopeFlag(scope), "daemon-reload"]);
4372
5201
  }
@@ -4383,7 +5212,7 @@ function createSystemdManager(scope) {
4383
5212
  }
4384
5213
  }
4385
5214
  async function status() {
4386
- if (!fs25.existsSync(file)) return "absent";
5215
+ if (!fs28.existsSync(file)) return "absent";
4387
5216
  const res = run3("systemctl", [...scopeFlag(scope), "is-active", unitName]);
4388
5217
  const out = res.stdout.trim();
4389
5218
  if (out === "active") return "running";
@@ -4416,8 +5245,33 @@ function getServiceManager(scope) {
4416
5245
  );
4417
5246
  }
4418
5247
 
5248
+ // src/core/cli-version.ts
5249
+ import { readFileSync } from "fs";
5250
+ import { fileURLToPath as fileURLToPath2 } from "url";
5251
+ import path28 from "path";
5252
+ var cached2 = null;
5253
+ function readCliVersion() {
5254
+ if (cached2 !== null) return cached2;
5255
+ const here = path28.dirname(fileURLToPath2(import.meta.url));
5256
+ const candidates = [
5257
+ path28.resolve(here, "..", "package.json"),
5258
+ path28.resolve(here, "..", "..", "package.json")
5259
+ ];
5260
+ for (const candidate of candidates) {
5261
+ try {
5262
+ const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
5263
+ if (pkg.name === "@task0/cli" && pkg.version) {
5264
+ cached2 = pkg.version;
5265
+ return cached2;
5266
+ }
5267
+ } catch {
5268
+ }
5269
+ }
5270
+ cached2 = "unknown";
5271
+ return cached2;
5272
+ }
5273
+
4419
5274
  // src/commands/daemon.ts
4420
- var DAEMON_VERSION = "0.1.0";
4421
5275
  async function dispatchRpc(ws, id, method, params) {
4422
5276
  const handler = rpcHandlers[method];
4423
5277
  if (!handler) {
@@ -4425,7 +5279,8 @@ async function dispatchRpc(ws, id, method, params) {
4425
5279
  return;
4426
5280
  }
4427
5281
  try {
4428
- const result = await handler(params ?? {});
5282
+ const ctx = { notifyManifestChanged: () => pushManifest(ws) };
5283
+ const result = await handler(params ?? {}, ctx);
4429
5284
  sendRpc(ws, { type: "rpc_response", id, result });
4430
5285
  } catch (error2) {
4431
5286
  const code = error2 && typeof error2 === "object" && "code" in error2 && typeof error2.code === "string" ? error2.code : "handler_error";
@@ -4438,6 +5293,44 @@ function sendRpc(ws, payload) {
4438
5293
  ws.send(JSON.stringify(payload));
4439
5294
  }
4440
5295
  }
5296
+ function summariseTask(projectName, task2) {
5297
+ const { object_id = null, id, status, title, task_dir, ...extra } = task2;
5298
+ return {
5299
+ project: projectName,
5300
+ object_id: typeof object_id === "string" ? object_id : null,
5301
+ id,
5302
+ status,
5303
+ title,
5304
+ task_dir,
5305
+ extra
5306
+ };
5307
+ }
5308
+ function buildManifest() {
5309
+ const projectSources = loadConfig().sources.filter((source2) => source2.type === "project");
5310
+ const projects = projectSources.map((source2) => ({
5311
+ name: source2.name,
5312
+ path: source2.path,
5313
+ enabled: source2.enabled
5314
+ }));
5315
+ const tasks = [];
5316
+ const scanErrors = {};
5317
+ for (const source2 of projectSources) {
5318
+ if (!source2.enabled) continue;
5319
+ try {
5320
+ const result = scanProjectWithRepair(source2.path, source2.name);
5321
+ for (const task2 of result.tasks) tasks.push(summariseTask(source2.name, task2));
5322
+ if (result.errors.length > 0) scanErrors[source2.name] = result.errors;
5323
+ } catch (err) {
5324
+ scanErrors[source2.name] = [err instanceof Error ? err.message : String(err)];
5325
+ }
5326
+ }
5327
+ return { type: "manifest", projects, tasks, scan_errors: scanErrors };
5328
+ }
5329
+ function pushManifest(ws) {
5330
+ if (ws.readyState !== ws.OPEN) return;
5331
+ const manifest = buildManifest();
5332
+ ws.send(JSON.stringify(manifest));
5333
+ }
4441
5334
  function fail6(message, code = 1) {
4442
5335
  console.error(chalk19.red(message));
4443
5336
  process.exit(code);
@@ -4508,7 +5401,7 @@ daemonCmd.command("register").description("Register this host with a central ser
4508
5401
  throw error2;
4509
5402
  }
4510
5403
  const body = {
4511
- hostname: os7.hostname(),
5404
+ hostname: os8.hostname(),
4512
5405
  platform: process.platform,
4513
5406
  name: opts.name
4514
5407
  };
@@ -4616,15 +5509,20 @@ daemonCmd.command("run").description("Run the daemon WebSocket loop in foregroun
4616
5509
  const hello = {
4617
5510
  type: "hello",
4618
5511
  daemon_id: identity.daemon_id,
4619
- version: DAEMON_VERSION,
5512
+ version: readCliVersion(),
4620
5513
  hostname: identity.hostname,
4621
5514
  platform: identity.platform
4622
5515
  };
4623
5516
  ws.send(JSON.stringify(hello));
4624
- const projects = loadConfig().sources.filter((source2) => source2.type === "project").map((source2) => ({ name: source2.name, path: source2.path, enabled: source2.enabled }));
4625
- const manifest = { type: "manifest", projects };
5517
+ const manifest = buildManifest();
4626
5518
  ws.send(JSON.stringify(manifest));
4627
- console.log(chalk19.dim(`pushed manifest: ${projects.length} project(s)`));
5519
+ console.log(chalk19.dim(`pushed manifest: ${manifest.projects.length} project(s)`));
5520
+ const sink = {
5521
+ send: (frame) => {
5522
+ if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(frame));
5523
+ }
5524
+ };
5525
+ bindAgentRunFrameSink(sink);
4628
5526
  });
4629
5527
  ws.on("message", (raw) => {
4630
5528
  let msg;
@@ -4642,10 +5540,16 @@ daemonCmd.command("run").description("Run the daemon WebSocket loop in foregroun
4642
5540
  console.error(chalk19.yellow(`server error: ${msg.message}`));
4643
5541
  } else if (msg.type === "rpc_request") {
4644
5542
  void dispatchRpc(ws, msg.id, msg.method, msg.params);
5543
+ } else if (msg.type === "agent_run_resume_request") {
5544
+ const sent = replayAfterRanges(msg.ranges);
5545
+ if (sent > 0) {
5546
+ console.log(chalk19.dim(`replayed ${sent} agent-run log frame(s) for hub`));
5547
+ }
4645
5548
  }
4646
5549
  });
4647
5550
  ws.on("close", (code, reason) => {
4648
5551
  activeWs = null;
5552
+ bindAgentRunFrameSink(null);
4649
5553
  const reasonText = reason.toString("utf-8") || "no reason";
4650
5554
  console.log(chalk19.yellow(`[${(/* @__PURE__ */ new Date()).toISOString()}] disconnected (code=${code}, ${reasonText})`));
4651
5555
  if (code === 4001) {
@@ -4709,6 +5613,55 @@ daemonCmd.command("show [daemonId]").description("Show local daemon identity (no
4709
5613
  const data = await jsonGet(`${base}/api/daemons/${encodeURIComponent(daemonId)}`);
4710
5614
  console.log(JSON.stringify(data.daemon, null, 2));
4711
5615
  });
5616
+ daemonCmd.command("status").description("Show this host's daemon status (identity, CLI version, service state)").option("--system", "Inspect the system-layer service status (default: user)").option("--json", "Emit a machine-readable JSON report instead of formatted text").action(async (opts) => {
5617
+ const identity = loadRequiredIdentity();
5618
+ const cliVersion = readCliVersion();
5619
+ const scope = opts.system ? "system" : "user";
5620
+ let serviceState = "unsupported";
5621
+ let unitPath2 = null;
5622
+ let serviceError = null;
5623
+ if (isPlatformSupported()) {
5624
+ try {
5625
+ const svc = getServiceManager(scope);
5626
+ serviceState = await svc.status();
5627
+ unitPath2 = svc.unitPath();
5628
+ } catch (error2) {
5629
+ serviceState = "unknown";
5630
+ serviceError = error2 instanceof Error ? error2.message : String(error2);
5631
+ }
5632
+ }
5633
+ if (opts.json) {
5634
+ console.log(JSON.stringify({
5635
+ daemon_id: identity.daemon_id,
5636
+ name: identity.name,
5637
+ hostname: identity.hostname,
5638
+ platform: identity.platform,
5639
+ server_url: identity.server_url,
5640
+ registered_at: identity.registered_at,
5641
+ cli_version: cliVersion,
5642
+ service: {
5643
+ scope,
5644
+ state: serviceState,
5645
+ unit_path: unitPath2,
5646
+ error: serviceError
5647
+ }
5648
+ }, null, 2));
5649
+ return;
5650
+ }
5651
+ const stateColor = serviceState === "running" ? chalk19.green : serviceState === "stopped" || serviceState === "absent" ? chalk19.yellow : serviceState === "errored" || serviceState === "unknown" ? chalk19.red : chalk19.dim;
5652
+ const label = (s) => chalk19.bold(s.padEnd(11));
5653
+ console.log(`${label("daemon")}${identity.daemon_id} (${identity.name})`);
5654
+ console.log(`${label("host")}${identity.hostname} \xB7 ${identity.platform}`);
5655
+ console.log(`${label("server")}${identity.server_url}`);
5656
+ console.log(`${label("cli")}v${cliVersion}`);
5657
+ console.log(`${label("registered")}${identity.registered_at}`);
5658
+ if (isPlatformSupported()) {
5659
+ console.log(`${label("service")}${stateColor(serviceState)} (${scope})${unitPath2 ? ` \u2192 ${unitPath2}` : ""}`);
5660
+ if (serviceError) console.log(chalk19.dim(` ${serviceError}`));
5661
+ } else {
5662
+ console.log(`${label("service")}${chalk19.dim(`not supported on ${process.platform}`)}`);
5663
+ }
5664
+ });
4712
5665
  daemonCmd.command("logout").description("Stop and uninstall the autostart service, then forget the locally stored daemon identity").option("--system", "Target the system-layer service").option("--keep-service", "Leave the installed service unit in place; only clear the identity file").action(async (opts) => {
4713
5666
  const scope = opts.system ? "system" : "user";
4714
5667
  if (!opts.keepService) {
@@ -4735,16 +5688,16 @@ daemonCmd.command("logout").description("Stop and uninstall the autostart servic
4735
5688
  // src/commands/user.ts
4736
5689
  import { Command as Command20 } from "commander";
4737
5690
  import chalk20 from "chalk";
4738
- var DEFAULT_BASE2 = (process.env.TASK0_API_URL || "http://127.0.0.1:4318").replace(/\/$/, "");
5691
+ var DEFAULT_BASE = (process.env.TASK0_API_URL || "http://127.0.0.1:4318").replace(/\/$/, "");
4739
5692
  function serverBase2() {
4740
- return DEFAULT_BASE2;
5693
+ return DEFAULT_BASE;
4741
5694
  }
4742
5695
  function fail7(message, code = 1) {
4743
5696
  console.error(chalk20.red(message));
4744
5697
  process.exit(code);
4745
5698
  }
4746
- async function callServer(path26, init = {}) {
4747
- const url = `${serverBase2()}${path26}`;
5699
+ async function callServer(path29, init = {}) {
5700
+ const url = `${serverBase2()}${path29}`;
4748
5701
  let auth;
4749
5702
  try {
4750
5703
  auth = adminAuthHeader();
@@ -5344,16 +6297,7 @@ function captureTopLevel(err, options) {
5344
6297
  }
5345
6298
 
5346
6299
  // src/main.ts
5347
- var TASK0_VERSION = readVersion();
5348
- function readVersion() {
5349
- try {
5350
- const here = path25.dirname(fileURLToPath2(import.meta.url));
5351
- const pkg = JSON.parse(readFileSync(path25.resolve(here, "..", "package.json"), "utf-8"));
5352
- return pkg.version ?? "unknown";
5353
- } catch {
5354
- return "unknown";
5355
- }
5356
- }
6300
+ var TASK0_VERSION = readCliVersion();
5357
6301
  var program = new Command23().name("task0").description("Task-centric control layer for agent workflow").version(TASK0_VERSION);
5358
6302
  program.addCommand(source);
5359
6303
  program.addCommand(project);