@task0/cli 0.4.1 → 0.7.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 +1923 -456
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -10,20 +10,23 @@ var __export = (target, all) => {
10
10
  };
11
11
 
12
12
  // ../../packages/shared/dist/node/yaml.js
13
- import fs2 from "fs";
14
- import path2 from "path";
15
- import yaml2 from "js-yaml";
13
+ import fs from "fs";
14
+ import path from "path";
15
+ import yaml from "js-yaml";
16
16
  function readYaml(filePath) {
17
- if (!fs2.existsSync(filePath))
17
+ if (!fs.existsSync(filePath))
18
18
  return null;
19
- const raw = fs2.readFileSync(filePath, "utf-8");
20
- return yaml2.load(raw);
19
+ const raw = fs.readFileSync(filePath, "utf-8");
20
+ return yaml.load(raw);
21
+ }
22
+ function writeYaml(filePath, data) {
23
+ fs.writeFileSync(filePath, yaml.dump(data, { lineWidth: 120 }), "utf-8");
21
24
  }
22
25
  function readContext(taskDir) {
23
- const files = fs2.readdirSync(taskDir).filter((f) => f.endsWith(".md"));
26
+ const files = fs.readdirSync(taskDir).filter((f) => f.endsWith(".md"));
24
27
  if (files.length === 0)
25
28
  return void 0;
26
- const content = fs2.readFileSync(path2.join(taskDir, files[0]), "utf-8");
29
+ const content = fs.readFileSync(path.join(taskDir, files[0]), "utf-8");
27
30
  const stripped = content.replace(/^---[\s\S]*?---\s*/, "");
28
31
  return stripped.trim() || void 0;
29
32
  }
@@ -141,29 +144,29 @@ var init_task = __esm({
141
144
  });
142
145
 
143
146
  // ../../packages/shared/dist/node/scanner.js
144
- import fs3 from "fs";
145
- import path3 from "path";
147
+ import fs2 from "fs";
148
+ import path2 from "path";
146
149
  function scanProject(projectPath, sourceName) {
147
- const absPath = path3.resolve(projectPath);
148
- const resolvedSourceName = sourceName ?? path3.basename(absPath);
150
+ const absPath = path2.resolve(projectPath);
151
+ const resolvedSourceName = sourceName ?? path2.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 = path2.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 = path2.join(absPath, projectConfig.tasks_dir);
162
+ if (!fs2.existsSync(tasksDir))
160
163
  return { tasks, errors: [`tasks_dir not found: ${tasksDir}`], repairs };
161
- const entries = fs3.readdirSync(tasksDir, { withFileTypes: true });
164
+ const entries = fs2.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 = path2.join(tasksDir, entry.name);
169
+ const taskYml = path2.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 = fs2.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 fs3 from "fs";
249
+ import path3 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 = path3.join(taskDir, file);
257
+ if (!fs3.existsSync(fullPath))
255
258
  continue;
256
- const md = fs4.readFileSync(fullPath, "utf-8");
259
+ const md = fs3.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";
282
+ import fs4 from "fs";
280
283
  import os from "os";
281
- import path5 from "path";
282
- import yaml3 from "js-yaml";
284
+ import path4 from "path";
285
+ import yaml2 from "js-yaml";
283
286
  function findProjectRoot(start = process.cwd()) {
284
- let dir = path5.resolve(start);
287
+ let dir = path4.resolve(start);
285
288
  while (true) {
286
- const ymlPath = path5.join(dir, "task0.yml");
287
- if (fs5.existsSync(ymlPath)) {
289
+ const ymlPath = path4.join(dir, "task0.yml");
290
+ if (fs4.existsSync(ymlPath)) {
288
291
  try {
289
- const raw = yaml3.load(fs5.readFileSync(ymlPath, "utf-8"));
292
+ const raw = yaml2.load(fs4.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 = path4.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 yaml2.load(fs4.readFileSync(path4.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 path4.isAbsolute(cfg.tasks_dir) ? cfg.tasks_dir : path4.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 = fs4.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 = path4.join(tasksDir, entry.name);
325
+ const taskYml = path4.join(taskDir, "task0.yml");
326
+ if (!fs4.existsSync(taskYml))
324
327
  continue;
325
328
  try {
326
- const raw = yaml3.load(fs5.readFileSync(taskYml, "utf-8"));
329
+ const raw = yaml2.load(fs4.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 (!fs4.existsSync(taskYml))
337
340
  throw new Error(`task0.yml not found: ${taskYml}`);
338
- return yaml3.load(fs5.readFileSync(taskYml, "utf-8")) || {};
341
+ return yaml2.load(fs4.readFileSync(taskYml, "utf-8")) || {};
339
342
  }
340
343
  function writeTaskYaml(taskYml, data) {
341
- fs5.writeFileSync(taskYml, yaml3.dump(data, { lineWidth: 120 }), "utf-8");
344
+ fs4.writeFileSync(taskYml, yaml2.dump(data, { lineWidth: 120 }), "utf-8");
342
345
  }
343
346
  function taskYamlLockPath(taskDir) {
344
- return path5.join(taskDir, TASK_YAML_LOCKFILE);
347
+ return path4.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(fs4.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 = fs4.openSync(file, "wx");
363
366
  try {
364
- fs5.writeFileSync(fd, JSON.stringify(info, null, 2), "utf-8");
367
+ fs4.writeFileSync(fd, JSON.stringify(info, null, 2), "utf-8");
365
368
  } finally {
366
- fs5.closeSync(fd);
369
+ fs4.closeSync(fd);
367
370
  }
368
371
  }
369
372
  function sleep(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
+ fs4.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
+ fs4.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 = path4.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 = path4.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";
586
+ import fs5 from "fs";
584
587
  import os2 from "os";
585
- import path6 from "path";
588
+ import path5 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 path5.join(os2.homedir(), ".task0");
593
596
  }
594
597
  function errorsRoot() {
595
- return path6.join(task0Home(), "errors");
598
+ return path5.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 = path5.join(root, dirName);
700
+ fs5.mkdirSync(dir, { recursive: true });
701
+ const finalPath = path5.join(dir, REPORT_FILENAME);
702
+ const tmpPath = path5.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
+ fs5.writeFileSync(tmpPath, json, "utf-8");
705
+ fs5.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 = fs5.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 = path5.join(root, entry.name);
735
+ const file = path5.join(dir, REPORT_FILENAME);
733
736
  let raw;
734
737
  try {
735
- raw = fs6.readFileSync(file, "utf-8");
738
+ raw = fs5.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 = fs5.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 = fs5.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
+ fs5.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";
847
+ import fs6 from "fs";
845
848
  import os3 from "os";
846
- import path7 from "path";
849
+ import path6 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 fs7 from "fs";
866
+ import path7 from "path";
867
+ import yaml3 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 = yaml3.load(match[1]);
877
+ return isRecord(parsed) ? parsed : null;
878
+ } catch {
879
+ return null;
880
+ }
881
+ }
882
+ function readTextIfExists(filePath) {
883
+ try {
884
+ if (!fs7.existsSync(filePath))
885
+ return null;
886
+ return fs7.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 = fs7.lstatSync(candidatePath);
895
+ if (!stat.isSymbolicLink())
896
+ continue;
897
+ const target = fs7.readlinkSync(candidatePath);
898
+ return {
899
+ isSymlink: true,
900
+ symlinkTarget: path7.isAbsolute(target) ? target : path7.resolve(path7.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 = path7.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 (!fs7.existsSync(skillsDir))
951
+ return;
952
+ entries = fs7.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 = path7.join(skillsDir, entry.name);
960
+ const skillFilePath = path7.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 = path7.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 (!fs7.existsSync(rulesDir))
998
+ return;
999
+ entries = fs7.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, path7.join(rulesDir, entry.name), scope);
1009
+ }
1010
+ }
1011
+ function getProjectAgentSkills(projectPath) {
1012
+ const absProjectPath = path7.resolve(projectPath);
1013
+ let projectStat;
1014
+ try {
1015
+ projectStat = fs7.statSync(absProjectPath);
1016
+ } catch {
1017
+ return [];
1018
+ }
1019
+ if (!projectStat.isDirectory())
1020
+ return [];
1021
+ const skills = [];
1022
+ scanClaudeSkillDir(skills, path7.join(absProjectPath, ".claude", "skills"), "project");
1023
+ pushInstructionIfExists(skills, "claude_code", "project", path7.join(absProjectPath, "CLAUDE.md"));
1024
+ pushInstructionIfExists(skills, "claude_code", "project", path7.join(absProjectPath, "AGENTS.md"));
1025
+ pushInstructionIfExists(skills, "codex", "project", path7.join(absProjectPath, "AGENTS.md"));
1026
+ scanCursorRulesDir(skills, path7.join(absProjectPath, ".cursor", "rules"), "project");
1027
+ pushInstructionIfExists(skills, "cursor", "project", path7.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, path7.join(homeDir, ".claude", "skills"), "global");
1036
+ scanCursorRulesDir(skills, path7.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,44 +1118,115 @@ 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
- import { Command as Command23 } from "commander";
925
-
926
- // src/commands/source.ts
927
- import { Command } from "commander";
928
- import path8 from "path";
929
- import chalk from "chalk";
1121
+ import { Command as Command24 } from "commander";
1122
+ import chalk24 from "chalk";
930
1123
 
931
1124
  // src/core/config.ts
932
- import fs from "fs";
933
- 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 });
1125
+ init_node();
1126
+ import fs8 from "fs";
1127
+ import os4 from "os";
1128
+ import path8 from "path";
1129
+ import yaml4 from "js-yaml";
1130
+ function registryDir() {
1131
+ return path8.join(
1132
+ process.env.HOME || process.env.USERPROFILE || os4.homedir(),
1133
+ ".config",
1134
+ "task0"
1135
+ );
1136
+ }
1137
+ function registryFile() {
1138
+ return path8.join(registryDir(), "config.yml");
1139
+ }
1140
+ function homeStateFile() {
1141
+ return path8.join(task0Home(), "config.yml");
1142
+ }
1143
+ function configFilePath() {
1144
+ return registryFile();
1145
+ }
1146
+ function readYamlFile(file) {
1147
+ if (!fs8.existsSync(file)) return null;
1148
+ try {
1149
+ const raw = fs8.readFileSync(file, "utf-8");
1150
+ const parsed = yaml4.load(raw);
1151
+ return parsed ?? null;
1152
+ } catch {
1153
+ return null;
1154
+ }
1155
+ }
1156
+ function writeYamlFile(file, data) {
1157
+ fs8.mkdirSync(path8.dirname(file), { recursive: true });
1158
+ fs8.writeFileSync(file, yaml4.dump(data, { lineWidth: 120 }), "utf-8");
1159
+ }
1160
+ function loadRegistry() {
1161
+ return readYamlFile(registryFile()) ?? {};
1162
+ }
1163
+ function saveRegistry(data) {
1164
+ writeYamlFile(registryFile(), data);
943
1165
  }
944
- function defaultConfig() {
945
- return { sources: [] };
1166
+ function loadHomeState() {
1167
+ return readYamlFile(homeStateFile()) ?? {};
1168
+ }
1169
+ function saveHomeState(data) {
1170
+ writeYamlFile(homeStateFile(), data);
946
1171
  }
947
1172
  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);
1173
+ const registry = loadRegistry();
1174
+ const home = loadHomeState();
1175
+ const profiles = registry.profiles && typeof registry.profiles === "object" && !Array.isArray(registry.profiles) ? registry.profiles : void 0;
1176
+ const current = typeof registry.current_profile === "string" && registry.current_profile.length > 0 ? registry.current_profile : void 0;
1177
+ const sources = Array.isArray(home.sources) ? home.sources : Array.isArray(registry.sources) ? registry.sources : [];
1178
+ const agentModels = home.agentModels !== void 0 ? home.agentModels : registry.agentModels;
951
1179
  return {
952
- ...data ?? {},
953
- sources: Array.isArray(data?.sources) ? data.sources : []
1180
+ sources,
1181
+ ...agentModels !== void 0 ? { agentModels } : {},
1182
+ ...profiles ? { profiles } : {},
1183
+ ...current ? { current_profile: current } : {}
954
1184
  };
955
1185
  }
956
1186
  function saveConfig(config) {
957
- ensureConfigDir();
958
- fs.writeFileSync(CONFIG_FILE, yaml.dump(config, { lineWidth: 120 }), "utf-8");
1187
+ const { sources, agentModels, ...registryFields } = config;
1188
+ saveRegistry(registryFields);
1189
+ saveHomeState({
1190
+ ...sources !== void 0 ? { sources } : {},
1191
+ ...agentModels !== void 0 ? { agentModels } : {}
1192
+ });
1193
+ }
1194
+ function listProfiles() {
1195
+ return loadConfig().profiles ?? {};
1196
+ }
1197
+ function getProfile(name) {
1198
+ return listProfiles()[name];
1199
+ }
1200
+ function getCurrentProfileName() {
1201
+ return loadConfig().current_profile;
1202
+ }
1203
+ function addProfile(name, entry) {
1204
+ const config = loadConfig();
1205
+ const profiles = { ...config.profiles ?? {} };
1206
+ profiles[name] = entry;
1207
+ config.profiles = profiles;
1208
+ saveConfig(config);
1209
+ }
1210
+ function removeProfile(name) {
1211
+ const config = loadConfig();
1212
+ if (!config.profiles || !(name in config.profiles)) return false;
1213
+ const profiles = { ...config.profiles };
1214
+ delete profiles[name];
1215
+ config.profiles = profiles;
1216
+ if (config.current_profile === name) {
1217
+ delete config.current_profile;
1218
+ }
1219
+ saveConfig(config);
1220
+ return true;
1221
+ }
1222
+ function setCurrentProfile(name) {
1223
+ const config = loadConfig();
1224
+ if (name === null) {
1225
+ delete config.current_profile;
1226
+ } else {
1227
+ config.current_profile = name;
1228
+ }
1229
+ saveConfig(config);
959
1230
  }
960
1231
  function addSource(entry) {
961
1232
  const config = loadConfig();
@@ -975,96 +1246,370 @@ function removeSource(name) {
975
1246
  saveConfig(config);
976
1247
  return true;
977
1248
  }
978
- function getSource(name) {
979
- return loadConfig().sources.find((s) => s.name === name);
1249
+
1250
+ // src/core/profile.ts
1251
+ var ProfileNotFoundError = class extends Error {
1252
+ constructor(name, available) {
1253
+ const list = available.length > 0 ? available.join(", ") : "(none)";
1254
+ super(`Profile "${name}" not found. Available: ${list}. Run "task0 profile list" to inspect.`);
1255
+ this.name = "ProfileNotFoundError";
1256
+ }
1257
+ };
1258
+ function parseProfileFlag(argv) {
1259
+ for (let i = 2; i < argv.length; i++) {
1260
+ const arg = argv[i];
1261
+ if (arg === "--") return null;
1262
+ if (arg === "--profile") {
1263
+ const next = argv[i + 1];
1264
+ if (typeof next === "string" && !next.startsWith("-")) return next;
1265
+ return null;
1266
+ }
1267
+ if (typeof arg === "string" && arg.startsWith("--profile=")) {
1268
+ const value = arg.slice("--profile=".length);
1269
+ return value.length > 0 ? value : null;
1270
+ }
1271
+ }
1272
+ return null;
1273
+ }
1274
+ function isProfileSubcommandInvocation(argv) {
1275
+ for (let i = 2; i < argv.length; i++) {
1276
+ const arg = argv[i];
1277
+ if (arg === "--") break;
1278
+ if (typeof arg !== "string") continue;
1279
+ if (arg.startsWith("-")) {
1280
+ if (arg === "--profile" || arg === "-C") i++;
1281
+ continue;
1282
+ }
1283
+ return arg === "profile";
1284
+ }
1285
+ return true;
1286
+ }
1287
+ function resolveActiveProfile(flagValue) {
1288
+ const requested = flagValue ?? getCurrentProfileName() ?? null;
1289
+ if (!requested) return null;
1290
+ const entry = getProfile(requested);
1291
+ if (!entry) {
1292
+ throw new ProfileNotFoundError(requested, Object.keys(listProfiles()));
1293
+ }
1294
+ return { name: requested, entry };
1295
+ }
1296
+ var activeProfileCache = null;
1297
+ function activateProfile(argv) {
1298
+ const flagValue = parseProfileFlag(argv);
1299
+ let active2;
1300
+ try {
1301
+ active2 = resolveActiveProfile(flagValue);
1302
+ } catch (error2) {
1303
+ if (isProfileSubcommandInvocation(argv) && error2 instanceof ProfileNotFoundError && flagValue === null) {
1304
+ active2 = null;
1305
+ } else {
1306
+ throw error2;
1307
+ }
1308
+ }
1309
+ if (active2) {
1310
+ activeProfileCache = active2;
1311
+ if (active2.entry.task0_home && active2.entry.task0_home.length > 0) {
1312
+ process.env.TASK0_HOME = active2.entry.task0_home;
1313
+ }
1314
+ if (active2.entry.api_url && !process.env.TASK0_API_URL) {
1315
+ process.env.TASK0_API_URL = active2.entry.api_url;
1316
+ }
1317
+ }
980
1318
  }
981
1319
 
982
1320
  // src/commands/source.ts
983
- init_node();
1321
+ import { Command } from "commander";
1322
+ import path11 from "path";
1323
+ import chalk from "chalk";
984
1324
 
985
1325
  // src/types.ts
986
1326
  init_task();
987
1327
 
1328
+ // src/core/admin-token.ts
1329
+ init_node();
1330
+ import fs9 from "fs";
1331
+ import path9 from "path";
1332
+ function tokenFile() {
1333
+ return path9.join(task0Home(), "admin.token");
1334
+ }
1335
+ var cached = null;
1336
+ var AdminTokenUnavailableError = class extends Error {
1337
+ constructor() {
1338
+ super(
1339
+ `Admin token not found.
1340
+ \u2022 If the task0 server runs on this host, run the task0-server binary once \u2014 the token will be generated at ${tokenFile()}.
1341
+ \u2022 Otherwise, copy the token from the server host and set TASK0_ADMIN_TOKEN.`
1342
+ );
1343
+ this.name = "AdminTokenUnavailableError";
1344
+ }
1345
+ };
1346
+ function readAdminToken() {
1347
+ if (cached) return cached;
1348
+ const fromEnv = process.env.TASK0_ADMIN_TOKEN?.trim();
1349
+ if (fromEnv) {
1350
+ cached = fromEnv;
1351
+ return cached;
1352
+ }
1353
+ const file = tokenFile();
1354
+ if (fs9.existsSync(file)) {
1355
+ const v = fs9.readFileSync(file, "utf-8").trim();
1356
+ if (v) {
1357
+ cached = v;
1358
+ return cached;
1359
+ }
1360
+ }
1361
+ throw new AdminTokenUnavailableError();
1362
+ }
1363
+ function adminAuthHeader() {
1364
+ return { authorization: `Bearer ${readAdminToken()}` };
1365
+ }
1366
+
1367
+ // src/core/daemon-config.ts
1368
+ init_node();
1369
+ import fs10 from "fs";
1370
+ import path10 from "path";
1371
+ function configDir() {
1372
+ return task0Home();
1373
+ }
1374
+ function configFile() {
1375
+ return path10.join(configDir(), "daemon.json");
1376
+ }
1377
+ function daemonConfigPath() {
1378
+ return configFile();
1379
+ }
1380
+ function readDaemonIdentity() {
1381
+ const file = configFile();
1382
+ if (!fs10.existsSync(file)) return null;
1383
+ try {
1384
+ const raw = fs10.readFileSync(file, "utf-8");
1385
+ return JSON.parse(raw);
1386
+ } catch {
1387
+ return null;
1388
+ }
1389
+ }
1390
+ function writeDaemonIdentity(identity) {
1391
+ const file = configFile();
1392
+ fs10.mkdirSync(configDir(), { recursive: true });
1393
+ fs10.writeFileSync(file, JSON.stringify(identity, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
1394
+ try {
1395
+ fs10.chmodSync(file, 384);
1396
+ } catch {
1397
+ }
1398
+ }
1399
+ function clearDaemonIdentity() {
1400
+ const file = configFile();
1401
+ if (!fs10.existsSync(file)) return false;
1402
+ fs10.unlinkSync(file);
1403
+ return true;
1404
+ }
1405
+
1406
+ // src/core/hub-client.ts
1407
+ var HubUnreachableError = class extends Error {
1408
+ constructor(url, cause) {
1409
+ super(`Cannot reach hub at ${url}: ${cause instanceof Error ? cause.message : String(cause)}`);
1410
+ this.name = "HubUnreachableError";
1411
+ }
1412
+ };
1413
+ var HubResponseError = class extends Error {
1414
+ status;
1415
+ code;
1416
+ body;
1417
+ constructor(status, body, code) {
1418
+ super(`Hub returned ${status}: ${body}`);
1419
+ this.name = "HubResponseError";
1420
+ this.status = status;
1421
+ this.code = code;
1422
+ this.body = body;
1423
+ }
1424
+ };
1425
+ function resolveHubUrl() {
1426
+ const fromEnv = process.env.TASK0_API_URL?.trim();
1427
+ if (fromEnv) return fromEnv.replace(/\/$/, "");
1428
+ const identity = readDaemonIdentity();
1429
+ if (identity?.server_url) return identity.server_url.replace(/\/$/, "");
1430
+ return "http://127.0.0.1:4318";
1431
+ }
1432
+ function resolveAuthHeader() {
1433
+ const apiToken = process.env.TASK0_API_TOKEN?.trim();
1434
+ if (apiToken) return { authorization: `Bearer ${apiToken}` };
1435
+ try {
1436
+ return adminAuthHeader();
1437
+ } catch (error2) {
1438
+ if (error2 instanceof AdminTokenUnavailableError) {
1439
+ throw new Error(
1440
+ `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" : "~/.task0/admin.token"}.`
1441
+ );
1442
+ }
1443
+ throw error2;
1444
+ }
1445
+ }
1446
+ async function callHub(pathname, opts = {}) {
1447
+ const base = resolveHubUrl();
1448
+ const url = new URL(pathname.startsWith("/") ? pathname : `/${pathname}`, base + "/");
1449
+ for (const [k, v] of Object.entries(opts.query ?? {})) {
1450
+ if (v !== void 0) url.searchParams.set(k, v);
1451
+ }
1452
+ const init = {
1453
+ method: opts.method ?? "GET",
1454
+ headers: {
1455
+ ...resolveAuthHeader(),
1456
+ ...opts.body !== void 0 ? { "content-type": "application/json" } : {}
1457
+ }
1458
+ };
1459
+ if (opts.body !== void 0) init.body = JSON.stringify(opts.body);
1460
+ if (opts.timeoutMs) init.signal = AbortSignal.timeout(opts.timeoutMs);
1461
+ let res;
1462
+ try {
1463
+ res = await fetch(url, init);
1464
+ } catch (error2) {
1465
+ throw new HubUnreachableError(url.toString(), error2);
1466
+ }
1467
+ if (!res.ok) {
1468
+ const body = await res.text().catch(() => "");
1469
+ let code = null;
1470
+ try {
1471
+ const parsed = JSON.parse(body);
1472
+ if (typeof parsed?.code === "string") code = parsed.code;
1473
+ } catch {
1474
+ }
1475
+ throw new HubResponseError(res.status, body, code);
1476
+ }
1477
+ if (res.status === 204) return void 0;
1478
+ const text = await res.text();
1479
+ if (!text) return void 0;
1480
+ return JSON.parse(text);
1481
+ }
1482
+ function localDaemonId() {
1483
+ const identity = readDaemonIdentity();
1484
+ return identity?.daemon_id ?? null;
1485
+ }
1486
+
988
1487
  // 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}`));
1488
+ function reportHubError(error2) {
1489
+ if (error2 instanceof HubUnreachableError) {
1490
+ console.error(chalk.red(error2.message));
1491
+ console.error(chalk.dim("Tip: ensure the hub is running and TASK0_API_URL points to it."));
1492
+ process.exit(1);
1493
+ }
1494
+ if (error2 instanceof HubResponseError) {
1495
+ console.error(chalk.red(`Hub error (${error2.status}): ${error2.body || "(empty body)"}`));
1496
+ process.exit(1);
1497
+ }
1498
+ throw error2;
1499
+ }
1500
+ function requireLocalDaemon() {
1501
+ const id = localDaemonId();
1502
+ if (!id) {
1503
+ console.error(chalk.red("This host is not registered as a daemon."));
1504
+ console.error(chalk.dim("Run `task0 daemon register --server <url>` first."));
996
1505
  process.exit(1);
997
1506
  }
998
- addSource({ name, type: "project", path: absPath, enabled: true });
1507
+ return id;
1508
+ }
1509
+ var source = new Command("source").description("Manage task sources");
1510
+ 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) => {
1511
+ const absPath = path11.resolve(inputPath);
1512
+ const name = opts.name || path11.basename(absPath);
1513
+ const daemonId = requireLocalDaemon();
1514
+ let resp;
1515
+ try {
1516
+ resp = await callHub(`/api/daemons/${encodeURIComponent(daemonId)}/projects`, {
1517
+ method: "POST",
1518
+ body: { path: absPath, name }
1519
+ });
1520
+ } catch (error2) {
1521
+ reportHubError(error2);
1522
+ }
999
1523
  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}`));
1524
+ if (typeof resp.taskCount === "number") {
1525
+ console.log(` ${resp.taskCount} tasks found`);
1003
1526
  }
1004
1527
  });
1005
- source.command("list").description("List registered task sources").action(() => {
1006
- const config = loadConfig();
1007
- if (config.sources.length === 0) {
1528
+ source.command("list").description("List registered task sources (queries the hub)").action(async () => {
1529
+ let data;
1530
+ try {
1531
+ data = await callHub("/api/config");
1532
+ } catch (error2) {
1533
+ if (error2 instanceof HubUnreachableError) {
1534
+ console.error(chalk.red(error2.message));
1535
+ console.error(chalk.dim("Tip: ensure the hub is running and TASK0_API_URL points to it."));
1536
+ process.exit(1);
1537
+ }
1538
+ if (error2 instanceof HubResponseError) {
1539
+ console.error(chalk.red(`Hub error (${error2.status}): ${error2.body || "(empty body)"}`));
1540
+ process.exit(1);
1541
+ }
1542
+ throw error2;
1543
+ }
1544
+ if (data.sources.length === 0) {
1008
1545
  console.log("No sources registered. Use `task0 source add <path>` to add one.");
1009
1546
  return;
1010
1547
  }
1011
- for (const s of config.sources) {
1548
+ for (const s of data.sources) {
1012
1549
  const status = s.enabled ? chalk.green("\u25CF") : chalk.dim("\u25CB");
1013
- console.log(`${status} ${s.name} ${chalk.dim(s.type)} ${s.path}`);
1014
- }
1015
- });
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);
1550
+ const where = s.path ?? (s.type === "github" ? "(github)" : s.type === "linear" ? "(linear)" : "");
1551
+ const owner = s.daemon_id ? chalk.dim(` @${s.daemon_id}`) : "";
1552
+ console.log(`${status} ${s.name} ${chalk.dim(s.type)} ${where}${owner}`);
1022
1553
  }
1023
1554
  });
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`));
1555
+ source.command("remove <name>").description("Remove a project source from this host's daemon (via hub)").action(async (name) => {
1556
+ const daemonId = requireLocalDaemon();
1557
+ try {
1558
+ await callHub(`/api/daemons/${encodeURIComponent(daemonId)}/projects/${encodeURIComponent(name)}`, {
1559
+ method: "DELETE"
1560
+ });
1561
+ } catch (error2) {
1562
+ if (error2 instanceof HubResponseError && error2.status === 404) {
1563
+ console.error(chalk.red(`Source "${name}" not found on this daemon.`));
1030
1564
  process.exit(1);
1031
1565
  }
1032
- return [s];
1033
- })() : config.sources.filter((s) => s.enabled);
1034
- if (sources.length === 0) {
1035
- console.log("No sources to scan.");
1566
+ reportHubError(error2);
1567
+ }
1568
+ console.log(chalk.green(`Removed source "${name}"`));
1569
+ });
1570
+ source.command("scan [name]").description("Scan source(s) via the hub and display tasks").option("--json", "Output as JSON").action(async (name, opts) => {
1571
+ let data;
1572
+ try {
1573
+ data = await callHub("/api/tasks", { query: { source: name } });
1574
+ } catch (error2) {
1575
+ reportHubError(error2);
1576
+ }
1577
+ if (data.tasks.length === 0) {
1578
+ console.log(name ? `No tasks in source "${name}".` : "No tasks.");
1036
1579
  return;
1037
1580
  }
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`));
1581
+ if (opts.json) {
1582
+ const out = data.tasks.map((t) => ({ ...t, _source: t.project }));
1583
+ console.log(JSON.stringify(out, null, 2));
1584
+ return;
1585
+ }
1586
+ const byProject = /* @__PURE__ */ new Map();
1587
+ for (const t of data.tasks) {
1588
+ const list = byProject.get(t.project) ?? [];
1589
+ list.push(t);
1590
+ byProject.set(t.project, list);
1591
+ }
1592
+ for (const [project2, tasks] of byProject) {
1593
+ console.log(chalk.bold(`
1594
+ ${project2}`));
1595
+ const objectIdWidth = Math.max(...tasks.map((t) => (t.object_id || "").length), 9);
1596
+ for (const t of tasks) {
1597
+ const statusColor2 = isActiveTaskStatus(t.status) ? chalk.green : t.status === "blocked" ? chalk.red : t.status === "todo" ? chalk.yellow : t.status === "done" ? chalk.dim : chalk.white;
1598
+ const objectId = chalk.cyan((t.object_id || "-").padEnd(objectIdWidth));
1599
+ console.log(` ${objectId} ${statusColor2(t.status.padEnd(8))} ${chalk.dim(t.id)} ${t.title}`);
1056
1600
  }
1601
+ console.log(chalk.dim(` ${tasks.length} tasks`));
1057
1602
  }
1058
- if (opts.json) {
1059
- console.log(JSON.stringify(allTasks, null, 2));
1603
+ if (data.errors.length > 0) {
1604
+ for (const err of data.errors) console.warn(chalk.yellow(`warn: ${err}`));
1060
1605
  }
1061
1606
  });
1062
1607
 
1063
1608
  // src/commands/project.ts
1064
1609
  import { Command as Command2 } from "commander";
1065
- import fs8 from "fs";
1066
- import path9 from "path";
1067
- import yaml4 from "js-yaml";
1610
+ import fs11 from "fs";
1611
+ import path12 from "path";
1612
+ import yaml5 from "js-yaml";
1068
1613
  import chalk2 from "chalk";
1069
1614
 
1070
1615
  // ../../packages/shared/dist/index.js
@@ -1072,44 +1617,45 @@ init_object_id();
1072
1617
 
1073
1618
  // src/commands/project.ts
1074
1619
  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 "-";
1620
+ project.command("list").description("List registered projects (queries the hub)").action(async () => {
1621
+ let data;
1078
1622
  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 "-";
1623
+ data = await callHub("/api/config");
1624
+ } catch (error2) {
1625
+ if (error2 instanceof HubUnreachableError) {
1626
+ console.error(chalk2.red(error2.message));
1627
+ process.exit(1);
1628
+ }
1629
+ if (error2 instanceof HubResponseError) {
1630
+ console.error(chalk2.red(`Hub error (${error2.status}): ${error2.body || "(empty body)"}`));
1631
+ process.exit(1);
1632
+ }
1633
+ throw error2;
1084
1634
  }
1085
- }
1086
- project.command("list").description("List registered projects").action(() => {
1087
- const projects = loadConfig().sources.filter((s) => s.type === "project");
1635
+ const projects = data.sources.filter((s) => s.type === "project");
1088
1636
  if (projects.length === 0) {
1089
1637
  console.log("No projects registered. Use `task0 source add <path>` to add one.");
1090
1638
  return;
1091
1639
  }
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) {
1640
+ const nameWidth = Math.max(...projects.map((p) => p.name.length), 4);
1641
+ for (const p of projects) {
1096
1642
  const status = p.enabled ? chalk2.green("\u25CF") : chalk2.dim("\u25CB");
1097
- const oid = chalk2.cyan(objectId.padEnd(objectIdWidth));
1098
1643
  const name = p.name.padEnd(nameWidth);
1099
- console.log(`${status} ${oid} ${name} ${chalk2.dim(p.path)}`);
1644
+ const owner = p.daemon_id ? chalk2.dim(` @${p.daemon_id}`) : "";
1645
+ console.log(`${status} ${name} ${chalk2.dim(p.path ?? "")}${owner}`);
1100
1646
  }
1101
1647
  });
1102
1648
  project.command("init").description("Initialize task0.yml in the current directory").option("-d, --tasks-dir <dir>", "Tasks directory", ".task0/tasks").action((opts) => {
1103
1649
  const cwd = process.cwd();
1104
- const ymlPath = path9.join(cwd, "task0.yml");
1105
- if (fs8.existsSync(ymlPath)) {
1650
+ const ymlPath = path12.join(cwd, "task0.yml");
1651
+ if (fs11.existsSync(ymlPath)) {
1106
1652
  console.error(chalk2.yellow("task0.yml already exists"));
1107
1653
  process.exit(1);
1108
1654
  }
1109
1655
  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 });
1656
+ fs11.writeFileSync(ymlPath, yaml5.dump(config), "utf-8");
1657
+ const tasksDir = path12.join(cwd, opts.tasksDir);
1658
+ fs11.mkdirSync(tasksDir, { recursive: true });
1113
1659
  console.log(chalk2.green("Initialized task0 project"));
1114
1660
  console.log(` ${ymlPath}`);
1115
1661
  console.log(` ${tasksDir}/`);
@@ -1118,61 +1664,57 @@ project.command("init").description("Initialize task0.yml in the current directo
1118
1664
  // src/commands/task.ts
1119
1665
  import { Command as Command8 } from "commander";
1120
1666
  import { execSync } from "child_process";
1121
- import fs14 from "fs";
1122
- import path12 from "path";
1123
- import yaml5 from "js-yaml";
1667
+ import fs17 from "fs";
1668
+ import path15 from "path";
1669
+ import yaml6 from "js-yaml";
1124
1670
  import chalk8 from "chalk";
1125
1671
 
1126
1672
  // src/lib/api.ts
1127
- var DEFAULT_BASE = "http://127.0.0.1:4318";
1128
1673
  function apiBaseUrl() {
1129
- return process.env.TASK0_API_URL || DEFAULT_BASE;
1674
+ return process.env.TASK0_API_URL || "http://127.0.0.1:4318";
1130
1675
  }
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) {
1676
+ function toApiError(method, pathname, error2) {
1677
+ if (error2 instanceof HubUnreachableError) {
1141
1678
  const err = new Error(
1142
1679
  `Cannot reach task0 API at ${apiBaseUrl()}. Start the task0-server binary (download from GitHub Releases) or repoint TASK0_API_URL.`
1143
1680
  );
1144
1681
  err.cause = error2;
1145
- throw err;
1682
+ return err;
1146
1683
  }
1147
- const text = await res.text();
1148
- let parsed = null;
1149
- try {
1150
- parsed = text ? JSON.parse(text) : null;
1151
- } catch {
1152
- parsed = text;
1684
+ if (error2 instanceof HubResponseError) {
1685
+ let parsedBody = error2.body;
1686
+ try {
1687
+ parsedBody = error2.body ? JSON.parse(error2.body) : null;
1688
+ } catch {
1689
+ }
1690
+ const message = parsedBody?.error || error2.body || error2.message;
1691
+ const err = new Error(`API ${method} ${pathname} failed (${error2.status}): ${message}`);
1692
+ err.status = error2.status;
1693
+ err.body = parsedBody;
1694
+ return err;
1153
1695
  }
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;
1696
+ return error2 instanceof Error ? error2 : new Error(String(error2));
1697
+ }
1698
+ async function request(method, pathname, body) {
1699
+ try {
1700
+ return await callHub(pathname, { method, body });
1701
+ } catch (error2) {
1702
+ throw toApiError(method, pathname, error2);
1160
1703
  }
1161
- return parsed;
1162
1704
  }
1163
1705
  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)
1706
+ get: (path31) => request("GET", path31),
1707
+ post: (path31, body) => request("POST", path31, body ?? {}),
1708
+ put: (path31, body) => request("PUT", path31, body ?? {}),
1709
+ patch: (path31, body) => request("PATCH", path31, body ?? {}),
1710
+ del: (path31) => request("DELETE", path31)
1169
1711
  };
1170
1712
 
1171
1713
  // src/commands/task/triage.ts
1172
1714
  import { Command as Command3 } from "commander";
1173
1715
  import chalk3 from "chalk";
1174
- import fs10 from "fs";
1175
- import path10 from "path";
1716
+ import fs13 from "fs";
1717
+ import path13 from "path";
1176
1718
 
1177
1719
  // src/core/agent-run-wait.ts
1178
1720
  async function getAgentRun(id) {
@@ -1223,8 +1765,8 @@ init_task_state2();
1223
1765
  var ISSUE_DETAIL_RE = /^ISSUE-\d+\.md$/;
1224
1766
  var TRIAGE_SKILL_NAME = "triage";
1225
1767
  function resolveSkillFilePath(projectRoot, skillName) {
1226
- const p = path10.join(projectRoot, ".claude", "skills", skillName, "SKILL.md");
1227
- return fs10.existsSync(p) ? p : null;
1768
+ const p = path13.join(projectRoot, ".claude", "skills", skillName, "SKILL.md");
1769
+ return fs13.existsSync(p) ? p : null;
1228
1770
  }
1229
1771
  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
1772
  try {
@@ -1243,7 +1785,7 @@ var triage = new Command3("triage").description("Decompose IDEA into ISSUE.md +
1243
1785
  process.exit(1);
1244
1786
  }
1245
1787
  for (const name of existingIssues) {
1246
- fs10.rmSync(path10.join(loc.taskDir, name), { force: true });
1788
+ fs13.rmSync(path13.join(loc.taskDir, name), { force: true });
1247
1789
  }
1248
1790
  }
1249
1791
  if (opts.model || opts.effort) {
@@ -1281,8 +1823,8 @@ var triage = new Command3("triage").description("Decompose IDEA into ISSUE.md +
1281
1823
  console.error(chalk3.red(`triage failed: ${final.error || "unknown"}`));
1282
1824
  process.exit(1);
1283
1825
  }
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();
1826
+ const hasOverview = fs13.existsSync(path13.join(loc.taskDir, "ISSUE.md"));
1827
+ const issueFiles = fs13.readdirSync(loc.taskDir).filter((name) => ISSUE_DETAIL_RE.test(name)).sort();
1286
1828
  if (!hasOverview || issueFiles.length === 0) {
1287
1829
  const missing = [];
1288
1830
  if (!hasOverview) missing.push("ISSUE.md");
@@ -1292,7 +1834,7 @@ var triage = new Command3("triage").description("Decompose IDEA into ISSUE.md +
1292
1834
  }
1293
1835
  let blockingQuestionCount = 0;
1294
1836
  for (const name of issueFiles) {
1295
- const content = fs10.readFileSync(path10.join(loc.taskDir, name), "utf-8");
1837
+ const content = fs13.readFileSync(path13.join(loc.taskDir, name), "utf-8");
1296
1838
  blockingQuestionCount += countBlockingQuestions(content);
1297
1839
  }
1298
1840
  await updateWorkflow(loc.taskYml, {
@@ -1320,8 +1862,8 @@ var triage = new Command3("triage").description("Decompose IDEA into ISSUE.md +
1320
1862
  }
1321
1863
  });
1322
1864
  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();
1865
+ if (!fs13.existsSync(taskDir)) return [];
1866
+ return fs13.readdirSync(taskDir).filter((name) => name === "ISSUE.md" || ISSUE_DETAIL_RE.test(name)).sort();
1325
1867
  }
1326
1868
  function countBlockingQuestions(md) {
1327
1869
  const match = md.match(/## Open Questions\s*\n([\s\S]*?)(\n## |\n*$)/i);
@@ -1335,13 +1877,13 @@ function countBlockingQuestions(md) {
1335
1877
  // src/commands/task/exec.ts
1336
1878
  import { Command as Command4 } from "commander";
1337
1879
  import chalk4 from "chalk";
1338
- import fs11 from "fs";
1339
- import path11 from "path";
1880
+ import fs14 from "fs";
1881
+ import path14 from "path";
1340
1882
  init_task_state2();
1341
1883
  var PLAN_EXECUTE_SKILL_NAME = "plan-execute";
1342
1884
  function resolveSkillFilePath2(projectRoot, skillName) {
1343
- const p = path11.join(projectRoot, ".claude", "skills", skillName, "SKILL.md");
1344
- return fs11.existsSync(p) ? p : null;
1885
+ const p = path14.join(projectRoot, ".claude", "skills", skillName, "SKILL.md");
1886
+ return fs14.existsSync(p) ? p : null;
1345
1887
  }
1346
1888
  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
1889
  try {
@@ -1434,7 +1976,7 @@ var summarize = new Command5("summarize").description("Generate a concise title
1434
1976
  });
1435
1977
 
1436
1978
  // src/commands/task/comment.ts
1437
- import fs12 from "fs";
1979
+ import fs15 from "fs";
1438
1980
  import { Command as Command6 } from "commander";
1439
1981
  import chalk6 from "chalk";
1440
1982
  var comment = new Command6("comment").description("Manage comments on a task");
@@ -1444,8 +1986,8 @@ function fail(err) {
1444
1986
  }
1445
1987
  function readBodyFromOpts(opts) {
1446
1988
  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");
1989
+ if (opts.file === "-") return fs15.readFileSync(0, "utf-8");
1990
+ if (opts.file) return fs15.readFileSync(opts.file, "utf-8");
1449
1991
  throw new Error("Provide --body or --file");
1450
1992
  }
1451
1993
  function preview(body, width = 60) {
@@ -1544,7 +2086,7 @@ comment.command("delete <cmtId>").description("Delete a comment by its cmt_ id")
1544
2086
  });
1545
2087
 
1546
2088
  // src/commands/task/description.ts
1547
- import fs13 from "fs";
2089
+ import fs16 from "fs";
1548
2090
  import { Command as Command7 } from "commander";
1549
2091
  import chalk7 from "chalk";
1550
2092
  var description = new Command7("description").description("Show or update the task description");
@@ -1554,8 +2096,8 @@ function fail2(err) {
1554
2096
  }
1555
2097
  function readBodyFromOpts2(opts) {
1556
2098
  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");
2099
+ if (opts.file === "-") return fs16.readFileSync(0, "utf-8");
2100
+ if (opts.file) return fs16.readFileSync(opts.file, "utf-8");
1559
2101
  throw new Error("Provide --body or --file (use --file - to read stdin)");
1560
2102
  }
1561
2103
  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 +2146,12 @@ task.addCommand(comment);
1604
2146
  task.addCommand(description);
1605
2147
  task.command("init <input>").description("Create a task from a description or Linear/GitHub issue URL").action(async (input) => {
1606
2148
  const cwd = process.cwd();
1607
- const projectYml = path12.join(cwd, "task0.yml");
1608
- if (!fs14.existsSync(projectYml)) {
2149
+ const projectYml = path15.join(cwd, "task0.yml");
2150
+ if (!fs17.existsSync(projectYml)) {
1609
2151
  console.error(chalk8.red("Not a task0 project (task0.yml not found). Run `task0 project init` first."));
1610
2152
  process.exit(1);
1611
2153
  }
1612
- const projectConfig = yaml5.load(fs14.readFileSync(projectYml, "utf-8"));
2154
+ const projectConfig = yaml6.load(fs17.readFileSync(projectYml, "utf-8"));
1613
2155
  if (projectConfig.kind !== "project") {
1614
2156
  console.error(chalk8.red('Invalid task0.yml: kind is not "project"'));
1615
2157
  process.exit(1);
@@ -1625,38 +2167,38 @@ task.command("init <input>").description("Create a task from a description or Li
1625
2167
  });
1626
2168
  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
2169
  const cwd = process.cwd();
1628
- const projectYml = path12.join(cwd, "task0.yml");
1629
- if (!fs14.existsSync(projectYml)) {
2170
+ const projectYml = path15.join(cwd, "task0.yml");
2171
+ if (!fs17.existsSync(projectYml)) {
1630
2172
  console.error(chalk8.red("Not a task0 project (task0.yml not found). Run `task0 project init` first."));
1631
2173
  process.exit(1);
1632
2174
  }
1633
- const projectConfig = yaml5.load(fs14.readFileSync(projectYml, "utf-8"));
2175
+ const projectConfig = yaml6.load(fs17.readFileSync(projectYml, "utf-8"));
1634
2176
  if (projectConfig.kind !== "project") {
1635
2177
  console.error(chalk8.red('Invalid task0.yml: kind is not "project"'));
1636
2178
  process.exit(1);
1637
2179
  }
1638
- const tasksDir = path12.join(cwd, projectConfig.tasks_dir);
1639
- if (!fs14.existsSync(tasksDir)) {
2180
+ const tasksDir = path15.join(cwd, projectConfig.tasks_dir);
2181
+ if (!fs17.existsSync(tasksDir)) {
1640
2182
  console.error(chalk8.red(`Tasks directory not found: ${tasksDir}`));
1641
2183
  process.exit(1);
1642
2184
  }
1643
- const entries = fs14.readdirSync(tasksDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
2185
+ const entries = fs17.readdirSync(tasksDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
1644
2186
  let migrated = 0;
1645
2187
  let skipped = 0;
1646
2188
  let seededObjectIds = 0;
1647
2189
  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)) {
2190
+ const taskDir = path15.join(tasksDir, name);
2191
+ const taskYml = path15.join(taskDir, "task0.yml");
2192
+ if (fs17.existsSync(taskYml)) {
1651
2193
  skipped++;
1652
2194
  try {
1653
- const raw = yaml5.load(fs14.readFileSync(taskYml, "utf-8"));
2195
+ const raw = yaml6.load(fs17.readFileSync(taskYml, "utf-8"));
1654
2196
  if (raw && raw.kind === "task" && !raw.object_id) {
1655
2197
  raw.object_id = generateObjectId("task");
1656
2198
  if (opts.dryRun) {
1657
2199
  console.log(chalk8.dim(`[dry-run] seed object_id: ${taskYml}`));
1658
2200
  } else {
1659
- fs14.writeFileSync(taskYml, yaml5.dump(raw, { lineWidth: 120 }), "utf-8");
2201
+ fs17.writeFileSync(taskYml, yaml6.dump(raw, { lineWidth: 120 }), "utf-8");
1660
2202
  console.log(chalk8.green(` seed object_id: ${taskYml}`));
1661
2203
  }
1662
2204
  seededObjectIds++;
@@ -1681,7 +2223,7 @@ task.command("migrate").description("Add task0.yml to legacy task directories th
1681
2223
  if (opts.dryRun) {
1682
2224
  console.log(chalk8.dim(`[dry-run] ${taskYml}`));
1683
2225
  } else {
1684
- fs14.writeFileSync(taskYml, yaml5.dump(taskConfig), "utf-8");
2226
+ fs17.writeFileSync(taskYml, yaml6.dump(taskConfig), "utf-8");
1685
2227
  console.log(chalk8.green(` ${taskYml}`));
1686
2228
  }
1687
2229
  migrated++;
@@ -1741,7 +2283,7 @@ task.command("done <id>").description("Write task lifecycle state to task0.yml \
1741
2283
  try {
1742
2284
  const { resolveTaskByObjectId: resolveTaskByObjectId2 } = await Promise.resolve().then(() => (init_task_state2(), task_state_exports));
1743
2285
  const loc = resolveTaskByObjectId2(id);
1744
- const raw = yaml5.load(fs14.readFileSync(loc.taskYml, "utf-8"));
2286
+ const raw = yaml6.load(fs17.readFileSync(loc.taskYml, "utf-8"));
1745
2287
  if (opts.phase) {
1746
2288
  const phase = opts.phase.trim();
1747
2289
  if (!phase) {
@@ -1754,7 +2296,7 @@ task.command("done <id>").description("Write task lifecycle state to task0.yml \
1754
2296
  return;
1755
2297
  }
1756
2298
  raw.workflow = { ...workflow2, phase };
1757
- fs14.writeFileSync(loc.taskYml, yaml5.dump(raw, { lineWidth: 120 }), "utf-8");
2299
+ fs17.writeFileSync(loc.taskYml, yaml6.dump(raw, { lineWidth: 120 }), "utf-8");
1758
2300
  console.log(chalk8.green(`${id} phase: ${phase}`));
1759
2301
  return;
1760
2302
  }
@@ -1765,7 +2307,7 @@ task.command("done <id>").description("Write task lifecycle state to task0.yml \
1765
2307
  raw.status = "done";
1766
2308
  const workflow = raw.workflow ?? {};
1767
2309
  raw.workflow = { ...workflow, phase: "completed" };
1768
- fs14.writeFileSync(loc.taskYml, yaml5.dump(raw, { lineWidth: 120 }), "utf-8");
2310
+ fs17.writeFileSync(loc.taskYml, yaml6.dump(raw, { lineWidth: 120 }), "utf-8");
1769
2311
  console.log(chalk8.green(`${id} marked as done`));
1770
2312
  } catch (err) {
1771
2313
  console.error(chalk8.red(err.message));
@@ -1774,33 +2316,33 @@ task.command("done <id>").description("Write task lifecycle state to task0.yml \
1774
2316
  });
1775
2317
  task.command("archive <id>").description("Archive a task (append to tasks.tar, remove from tasks/)").action((id) => {
1776
2318
  const cwd = process.cwd();
1777
- const projectYml = path12.join(cwd, "task0.yml");
1778
- if (!fs14.existsSync(projectYml)) {
2319
+ const projectYml = path15.join(cwd, "task0.yml");
2320
+ if (!fs17.existsSync(projectYml)) {
1779
2321
  console.error(chalk8.red("Not a task0 project (task0.yml not found)."));
1780
2322
  process.exit(1);
1781
2323
  }
1782
- const projectConfig = yaml5.load(fs14.readFileSync(projectYml, "utf-8"));
2324
+ const projectConfig = yaml6.load(fs17.readFileSync(projectYml, "utf-8"));
1783
2325
  if (projectConfig.kind !== "project") {
1784
2326
  console.error(chalk8.red('Invalid task0.yml: kind is not "project"'));
1785
2327
  process.exit(1);
1786
2328
  }
1787
- const tasksDir = path12.join(cwd, projectConfig.tasks_dir);
1788
- const taskDir = path12.join(tasksDir, id);
1789
- if (!fs14.existsSync(taskDir)) {
2329
+ const tasksDir = path15.join(cwd, projectConfig.tasks_dir);
2330
+ const taskDir = path15.join(tasksDir, id);
2331
+ if (!fs17.existsSync(taskDir)) {
1790
2332
  console.error(chalk8.red(`Task "${id}" not found at ${taskDir}`));
1791
2333
  process.exit(1);
1792
2334
  }
1793
2335
  const tarFile = tasksDir + ".tar";
1794
2336
  archiveTaskToTar(tasksDir, id, tarFile);
1795
- fs14.rmSync(taskDir, { recursive: true });
2337
+ fs17.rmSync(taskDir, { recursive: true });
1796
2338
  console.log(chalk8.green(`Archived "${id}" \u2192 ${tarFile}`));
1797
2339
  });
1798
2340
  function archiveTaskToTar(tasksDir, taskId, tarFile) {
1799
- if (fs14.existsSync(tarFile) && !fs14.statSync(tarFile).isFile()) {
2341
+ if (fs17.existsSync(tarFile) && !fs17.statSync(tarFile).isFile()) {
1800
2342
  console.error(chalk8.red(`${tarFile} exists but is not a file (likely a leftover directory). Remove it manually first.`));
1801
2343
  process.exit(1);
1802
2344
  }
1803
- if (fs14.existsSync(tarFile)) {
2345
+ if (fs17.existsSync(tarFile)) {
1804
2346
  execSync(`tar -rf ${JSON.stringify(tarFile)} -C ${JSON.stringify(tasksDir)} ${JSON.stringify(taskId)}`);
1805
2347
  } else {
1806
2348
  execSync(`tar -cf ${JSON.stringify(tarFile)} -C ${JSON.stringify(tasksDir)} ${JSON.stringify(taskId)}`);
@@ -1810,9 +2352,9 @@ function archiveTaskToTar(tasksDir, taskId, tarFile) {
1810
2352
  // src/commands/ui.ts
1811
2353
  import { Command as Command9 } from "commander";
1812
2354
  import { spawn } from "child_process";
1813
- import path13 from "path";
2355
+ import path16 from "path";
1814
2356
  import chalk9 from "chalk";
1815
- var DASHBOARD_DIR = path13.resolve(
2357
+ var DASHBOARD_DIR = path16.resolve(
1816
2358
  import.meta.dirname,
1817
2359
  "..",
1818
2360
  "..",
@@ -1846,17 +2388,17 @@ import chalk10 from "chalk";
1846
2388
  // ../../packages/shared/dist/types/agent.js
1847
2389
  var AGENT_KINDS = ["coding", "llm_api", "workflow"];
1848
2390
  var AGENT_KIND_SET = new Set(AGENT_KINDS);
1849
- var CODING_RUNTIME_AGENTS = ["claude-code", "codex", "cursor"];
1850
- var CODING_RUNTIME_AGENT_SET = new Set(CODING_RUNTIME_AGENTS);
1851
- function isCodingRuntimeAgent(value) {
1852
- return typeof value === "string" && CODING_RUNTIME_AGENT_SET.has(value);
2391
+ var AGENT_PROVIDERS = ["claude-code", "codex", "cursor-agent"];
2392
+ var AGENT_PROVIDER_SET = new Set(AGENT_PROVIDERS);
2393
+ function isAgentProvider(value) {
2394
+ return typeof value === "string" && AGENT_PROVIDER_SET.has(value);
1853
2395
  }
1854
2396
 
1855
2397
  // ../../packages/shared/dist/types/runtime.js
1856
2398
  var AGENT_RUN_STATUS_VALUES = ["starting", "running", "done", "error"];
1857
2399
  var AGENT_RUN_STATUS_SET = new Set(AGENT_RUN_STATUS_VALUES);
1858
- var RUNTIME_AGENTS = CODING_RUNTIME_AGENTS;
1859
- var isRuntimeAgent = isCodingRuntimeAgent;
2400
+ var RUNTIME_AGENTS = AGENT_PROVIDERS;
2401
+ var isRuntimeAgent = isAgentProvider;
1860
2402
  var AGENT_MODEL_DEFAULTS = {
1861
2403
  "claude-code": [
1862
2404
  { id: "opus", label: "Opus" },
@@ -1868,20 +2410,45 @@ var AGENT_MODEL_DEFAULTS = {
1868
2410
  { id: "o3", label: "o3" },
1869
2411
  { id: "o4-mini", label: "o4-mini" }
1870
2412
  ],
1871
- cursor: [
2413
+ "cursor-agent": [
1872
2414
  { id: "", label: "Default" },
1873
2415
  { id: "gpt-5", label: "GPT-5" },
1874
2416
  { id: "sonnet-4", label: "Sonnet 4" },
1875
2417
  { id: "sonnet-4-thinking", label: "Sonnet 4 Thinking" }
1876
2418
  ]
1877
2419
  };
2420
+ var AGENT_EFFORT_DEFAULTS = {
2421
+ "claude-code": [
2422
+ { id: "high", label: "High" },
2423
+ { id: "max", label: "Max" },
2424
+ { id: "medium", label: "Medium" },
2425
+ { id: "low", label: "Low" }
2426
+ ],
2427
+ codex: [
2428
+ { id: "xhigh", label: "Extra High" },
2429
+ { id: "high", label: "High" },
2430
+ { id: "medium", label: "Medium" },
2431
+ { id: "low", label: "Low" }
2432
+ ],
2433
+ "cursor-agent": []
2434
+ };
2435
+ var AGENT_DEFAULT_MODEL = {
2436
+ "claude-code": "opus",
2437
+ codex: "gpt-5.4",
2438
+ "cursor-agent": ""
2439
+ };
2440
+ var AGENT_DEFAULT_EFFORT = {
2441
+ "claude-code": "high",
2442
+ codex: "xhigh",
2443
+ "cursor-agent": ""
2444
+ };
1878
2445
  function defaultAgentModelFetchCommand(agent2) {
1879
- if (agent2 === "cursor")
2446
+ if (agent2 === "cursor-agent")
1880
2447
  return "cursor-agent models";
1881
2448
  return void 0;
1882
2449
  }
1883
2450
  function defaultAgentModelOutputFormat(agent2) {
1884
- if (agent2 === "cursor")
2451
+ if (agent2 === "cursor-agent")
1885
2452
  return "lines";
1886
2453
  return void 0;
1887
2454
  }
@@ -2093,12 +2660,12 @@ models.command("default <agent>").description("Get or set default model / effort
2093
2660
  });
2094
2661
 
2095
2662
  // src/commands/agent-run.ts
2096
- import fs15 from "fs";
2097
- import path14 from "path";
2663
+ import fs18 from "fs";
2664
+ import path17 from "path";
2098
2665
  import { Command as Command11 } from "commander";
2099
2666
  import chalk11 from "chalk";
2100
2667
  import { spawn as spawn2, spawnSync as spawnSync3 } from "child_process";
2101
- import yaml6 from "js-yaml";
2668
+ import yaml7 from "js-yaml";
2102
2669
  var agentRun = new Command11("agent-run").description("Inspect and control agent runs");
2103
2670
  agentRun.command("list").description("List active agent runs").option("--json", "Output JSON").option("--task <id>", "Filter by task id").action(async (opts) => {
2104
2671
  try {
@@ -2222,7 +2789,7 @@ profile.command("get <ref>").description("Show one runtime profile by slug or ob
2222
2789
  console.log(formatProfile(result.runtime));
2223
2790
  console.log();
2224
2791
  console.log(chalk11.dim("--- exec ---"));
2225
- console.log(yaml6.dump(result.runtime.exec, { lineWidth: 100 }));
2792
+ console.log(yaml7.dump(result.runtime.exec, { lineWidth: 100 }));
2226
2793
  } catch (err) {
2227
2794
  const apiErr = err;
2228
2795
  if (apiErr.status === 404) failProfile(`not found: ${ref}`);
@@ -2232,7 +2799,7 @@ profile.command("get <ref>").description("Show one runtime profile by slug or ob
2232
2799
  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
2800
  let parsed;
2234
2801
  try {
2235
- parsed = yaml6.load(fs15.readFileSync(path14.resolve(opts.fromFile), "utf-8"));
2802
+ parsed = yaml7.load(fs18.readFileSync(path17.resolve(opts.fromFile), "utf-8"));
2236
2803
  } catch (err) {
2237
2804
  failProfile(`cannot read ${opts.fromFile}: ${err.message}`);
2238
2805
  }
@@ -2248,14 +2815,14 @@ profile.command("edit <ref>").description("Open the runtime profile YAML in $EDI
2248
2815
  try {
2249
2816
  const result = await api.get(`/api/runtime-profiles/${encodeURIComponent(ref)}`);
2250
2817
  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");
2818
+ const tmp = path17.join(process.env.TMPDIR || "/tmp", `task0-runtime-${result.runtime.slug}-${Date.now()}.yml`);
2819
+ fs18.writeFileSync(tmp, yaml7.dump(result.runtime, { lineWidth: 100 }), "utf-8");
2253
2820
  const editor = process.env.EDITOR || "vi";
2254
2821
  const r = spawnSync3(editor, [tmp], { stdio: "inherit" });
2255
2822
  if (r.status !== 0) failProfile(`editor exited with status ${r.status}`);
2256
- const updated = yaml6.load(fs15.readFileSync(tmp, "utf-8"));
2823
+ const updated = yaml7.load(fs18.readFileSync(tmp, "utf-8"));
2257
2824
  await api.put(`/api/runtime-profiles/${encodeURIComponent(ref)}`, updated);
2258
- fs15.unlinkSync(tmp);
2825
+ fs18.unlinkSync(tmp);
2259
2826
  console.log(chalk11.green(`updated ${ref}`));
2260
2827
  } catch (err) {
2261
2828
  failProfile(err.message);
@@ -2275,13 +2842,13 @@ agentRun.addCommand(profile);
2275
2842
  // src/commands/plan.ts
2276
2843
  import { Command as Command12 } from "commander";
2277
2844
  import chalk12 from "chalk";
2278
- import fs16 from "fs";
2279
- import path15 from "path";
2845
+ import fs19 from "fs";
2846
+ import path18 from "path";
2280
2847
  init_task_state2();
2281
2848
  var PLAN_GENERATE_SKILL_NAME = "plan-generate";
2282
2849
  function resolveSkillFilePath3(projectRoot, skillName) {
2283
- const p = path15.join(projectRoot, ".claude", "skills", skillName, "SKILL.md");
2284
- return fs16.existsSync(p) ? p : null;
2850
+ const p = path18.join(projectRoot, ".claude", "skills", skillName, "SKILL.md");
2851
+ return fs19.existsSync(p) ? p : null;
2285
2852
  }
2286
2853
  var plan = new Command12("plan").description("Generate and refine plans");
2287
2854
  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 +2980,7 @@ var PLAN_REFINE_SKILL_NAME = "plan-refine";
2413
2980
  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
2981
  try {
2415
2982
  const loc = resolveTaskByObjectId(objectId);
2416
- const files = fs16.readdirSync(loc.taskDir);
2983
+ const files = fs19.readdirSync(loc.taskDir);
2417
2984
  const planFiles = files.filter((f) => /^PLAN-\d+-(codex|claude-code|cursor)\.md$/.test(f));
2418
2985
  if (planFiles.length === 0) {
2419
2986
  console.error(chalk12.red("No PLAN-NN-<agent>.md files found. Run `task0 plan generate` first."));
@@ -2455,8 +3022,8 @@ plan.command("refine <objectId>").description("Synthesize plan files + ISSUE fil
2455
3022
  if (!opts.json) console.log(chalk12.dim(`[refine] ${s.status}${s.phase ? " " + s.phase : ""}`));
2456
3023
  }
2457
3024
  });
2458
- const refinedPath = path15.join(loc.taskDir, refinedFile);
2459
- const wrote = fs16.existsSync(refinedPath);
3025
+ const refinedPath = path18.join(loc.taskDir, refinedFile);
3026
+ const wrote = fs19.existsSync(refinedPath);
2460
3027
  if (final.status === "done" && wrote) {
2461
3028
  await updateWorkflow(loc.taskYml, {
2462
3029
  phase: "refined",
@@ -2608,8 +3175,8 @@ import { Command as Command14 } from "commander";
2608
3175
  import chalk14 from "chalk";
2609
3176
 
2610
3177
  // src/lib/project.ts
2611
- import path16 from "path";
2612
- import fs17 from "fs";
3178
+ import path19 from "path";
3179
+ import fs20 from "fs";
2613
3180
  function resolveProjectName(opts) {
2614
3181
  if (opts.project && opts.project.length > 0) return opts.project;
2615
3182
  const config = loadConfig();
@@ -2619,15 +3186,15 @@ function resolveProjectName(opts) {
2619
3186
  "Cannot resolve project: no registered projects. Use `task0 source add <path>` first, or pass --project <name>."
2620
3187
  );
2621
3188
  }
2622
- const cwd = fs17.realpathSync(process.cwd());
3189
+ const cwd = fs20.realpathSync(process.cwd());
2623
3190
  for (const source2 of projects) {
2624
3191
  let sourceAbs;
2625
3192
  try {
2626
- sourceAbs = fs17.realpathSync(path16.resolve(source2.path));
3193
+ sourceAbs = fs20.realpathSync(path19.resolve(source2.path));
2627
3194
  } catch {
2628
- sourceAbs = path16.resolve(source2.path);
3195
+ sourceAbs = path19.resolve(source2.path);
2629
3196
  }
2630
- if (cwd === sourceAbs || cwd.startsWith(sourceAbs + path16.sep)) {
3197
+ if (cwd === sourceAbs || cwd.startsWith(sourceAbs + path19.sep)) {
2631
3198
  return source2.name;
2632
3199
  }
2633
3200
  }
@@ -3267,12 +3834,12 @@ import chalk17 from "chalk";
3267
3834
 
3268
3835
  // src/core/issue/decision.ts
3269
3836
  init_node();
3270
- import fs18 from "fs";
3271
- import path17 from "path";
3837
+ import fs21 from "fs";
3838
+ import path20 from "path";
3272
3839
  init_task_state2();
3273
3840
  var DECISION_FILE_RE = /^DECISION-(\d+)-([a-z0-9-]+)\.md$/;
3274
3841
  function selectBlockingIssues(taskDir, explicitIssue) {
3275
- const files = fs18.existsSync(taskDir) ? fs18.readdirSync(taskDir) : [];
3842
+ const files = fs21.existsSync(taskDir) ? fs21.readdirSync(taskDir) : [];
3276
3843
  const issueFiles = files.filter((f) => /^ISSUE-\d+\.md$/.test(f)).sort();
3277
3844
  const all = readOpenQuestions(taskDir, issueFiles);
3278
3845
  if (!explicitIssue) return all;
@@ -3376,12 +3943,12 @@ function parseConsolidatedAnswers(md) {
3376
3943
  return sections;
3377
3944
  }
3378
3945
  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");
3946
+ const issuePath = path20.join(taskDir, issueFile);
3947
+ const consolidatedPath = path20.join(taskDir, consolidatedFile);
3948
+ if (!fs21.existsSync(issuePath)) throw new Error(`${issueFile} not found in ${taskDir}`);
3949
+ if (!fs21.existsSync(consolidatedPath)) throw new Error(`${consolidatedFile} not found in ${taskDir}`);
3950
+ const issueMd = fs21.readFileSync(issuePath, "utf-8");
3951
+ const consolidatedMd = fs21.readFileSync(consolidatedPath, "utf-8");
3385
3952
  const answers = parseConsolidatedAnswers(consolidatedMd);
3386
3953
  if (answers.length === 0) {
3387
3954
  throw new Error(`${consolidatedFile} has no "## Qn" sections; cannot derive Decisions`);
@@ -3403,7 +3970,7 @@ _Resolved from [${consolidatedFile}](${consolidatedFile}); see that file for rea
3403
3970
  throw new Error(`${issueFile} has no "## Open Questions" section to replace`);
3404
3971
  }
3405
3972
  const next = issueMd.replace(openQRe, decisionsSection.trimEnd() + "\n");
3406
- fs18.writeFileSync(issuePath, next, "utf-8");
3973
+ fs21.writeFileSync(issuePath, next, "utf-8");
3407
3974
  return { replaced: answers.length };
3408
3975
  }
3409
3976
  async function propose(opts) {
@@ -3431,8 +3998,8 @@ async function propose(opts) {
3431
3998
  const kicks = [];
3432
3999
  for (const agent2 of opts.agents) {
3433
4000
  const decisionFile = decisionFileName(issue2, agent2);
3434
- const decisionPath = path17.join(loc.taskDir, decisionFile);
3435
- if (fs18.existsSync(decisionPath) && !opts.force) {
4001
+ const decisionPath = path20.join(loc.taskDir, decisionFile);
4002
+ if (fs21.existsSync(decisionPath) && !opts.force) {
3436
4003
  if (opts.ifNeeded) {
3437
4004
  kicks.push({
3438
4005
  issue: issue2.file,
@@ -3507,7 +4074,7 @@ async function propose(opts) {
3507
4074
  const final = await waitForAgentRun(k.agentRunId);
3508
4075
  if (final.status !== "done") {
3509
4076
  k.error = final.error || `runtime ${k.agentRunId} ended with ${final.status}`;
3510
- } else if (!fs18.existsSync(path17.join(loc.taskDir, k.decisionFile))) {
4077
+ } else if (!fs21.existsSync(path20.join(loc.taskDir, k.decisionFile))) {
3511
4078
  k.error = `runtime completed but ${k.decisionFile} was not written`;
3512
4079
  }
3513
4080
  } catch (err) {
@@ -3528,7 +4095,7 @@ async function consolidate(opts) {
3528
4095
  const kicks = [];
3529
4096
  const modelOpts = resolveModelOptions(opts.agent, { model: opts.model, effort: opts.effort }, opts.warn);
3530
4097
  for (const issue2 of issues) {
3531
- const files = fs18.readdirSync(loc.taskDir);
4098
+ const files = fs21.readdirSync(loc.taskDir);
3532
4099
  const proposalFiles = files.filter((f) => {
3533
4100
  const m = f.match(DECISION_FILE_RE);
3534
4101
  return !!m && m[1] === issue2.index && m[2] !== "consolidated";
@@ -3574,7 +4141,7 @@ async function consolidate(opts) {
3574
4141
  if (opts.wait) {
3575
4142
  try {
3576
4143
  const final = await waitForAgentRun(agentRunId);
3577
- const wrote = fs18.existsSync(path17.join(loc.taskDir, consolidatedFile));
4144
+ const wrote = fs21.existsSync(path20.join(loc.taskDir, consolidatedFile));
3578
4145
  if (final.status !== "done") {
3579
4146
  kick.error = final.error || `runtime ${agentRunId} ended with ${final.status}`;
3580
4147
  } else if (!wrote) {
@@ -3640,7 +4207,7 @@ async function approve(opts) {
3640
4207
  return { updated };
3641
4208
  }
3642
4209
  function buildReferenceFiles(taskDir, issue2) {
3643
- const names = fs18.readdirSync(taskDir);
4210
+ const names = fs21.readdirSync(taskDir);
3644
4211
  const refs = [issue2.file];
3645
4212
  if (names.includes("ISSUE.md")) refs.push("ISSUE.md");
3646
4213
  const ideaFile = `IDEA-${issue2.index}.md`;
@@ -3748,12 +4315,12 @@ issue.command("approve <taskId>").description("Apply the consolidated decisions
3748
4315
  });
3749
4316
 
3750
4317
  // src/commands/agent.ts
3751
- import fs19 from "fs";
3752
- import path18 from "path";
4318
+ import fs22 from "fs";
4319
+ import path21 from "path";
3753
4320
  import { spawnSync as spawnSync5 } from "child_process";
3754
4321
  import { Command as Command18 } from "commander";
3755
4322
  import chalk18 from "chalk";
3756
- import yaml7 from "js-yaml";
4323
+ import yaml8 from "js-yaml";
3757
4324
  function statusBadge(s) {
3758
4325
  if (s === "working") return chalk18.cyan("\u25CF");
3759
4326
  if (s === "error") return chalk18.red("\u25CF");
@@ -3772,7 +4339,7 @@ function fail5(message, code = 1) {
3772
4339
  process.exit(code);
3773
4340
  }
3774
4341
  function formatAgent(a, withDetails = false) {
3775
- const tag = a.system ? chalk18.dim("(system)") : a.scope ? chalk18.dim(`(${a.scope})`) : "";
4342
+ const tag = a.scope ? chalk18.dim(`(${a.scope})`) : "";
3776
4343
  const dot = statusBadge(a.status);
3777
4344
  const head = `${dot} ${chalk18.bold(a.slug.padEnd(24))} ${chalk18.dim(a.object_id.padEnd(16))} ${a.kind.padEnd(10)} ${tag}`;
3778
4345
  if (!withDetails) return head;
@@ -3813,10 +4380,10 @@ agent.command("get <ref>").description("Show one agent by object_id or slug").op
3813
4380
  }
3814
4381
  console.log();
3815
4382
  console.log(chalk18.dim("--- spec ---"));
3816
- console.log(yaml7.dump(result.agent.spec, { lineWidth: 100 }));
4383
+ console.log(yaml8.dump(result.agent.spec, { lineWidth: 100 }));
3817
4384
  if (result.agent.mcp_config) {
3818
4385
  console.log(chalk18.dim("--- mcp_config ---"));
3819
- console.log(yaml7.dump(result.agent.mcp_config, { lineWidth: 100 }));
4386
+ console.log(yaml8.dump(result.agent.mcp_config, { lineWidth: 100 }));
3820
4387
  }
3821
4388
  } catch (err) {
3822
4389
  const apiErr = err;
@@ -3827,8 +4394,8 @@ agent.command("get <ref>").description("Show one agent by object_id or slug").op
3827
4394
  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
4395
  let parsed;
3829
4396
  try {
3830
- const raw = fs19.readFileSync(path18.resolve(opts.fromFile), "utf-8");
3831
- parsed = yaml7.load(raw);
4397
+ const raw = fs22.readFileSync(path21.resolve(opts.fromFile), "utf-8");
4398
+ parsed = yaml8.load(raw);
3832
4399
  } catch (err) {
3833
4400
  fail5(`cannot read ${opts.fromFile}: ${err.message}`);
3834
4401
  }
@@ -3843,18 +4410,17 @@ agent.command("create").description("Create an agent from a YAML spec file").req
3843
4410
  agent.command("edit <ref>").description("Open the agent YAML in $EDITOR and save changes").action(async (ref) => {
3844
4411
  try {
3845
4412
  const result = await api.get(`/api/agents/${encodeURIComponent(ref)}`);
3846
- if (result.agent.system) fail5("cannot edit a system agent");
3847
- const tmp = path18.join(
4413
+ const tmp = path21.join(
3848
4414
  process.env.TMPDIR || "/tmp",
3849
4415
  `task0-agent-${result.agent.slug}-${Date.now()}.yml`
3850
4416
  );
3851
- fs19.writeFileSync(tmp, yaml7.dump(result.agent, { lineWidth: 100 }), "utf-8");
4417
+ fs22.writeFileSync(tmp, yaml8.dump(result.agent, { lineWidth: 100 }), "utf-8");
3852
4418
  const editor = process.env.EDITOR || "vi";
3853
4419
  const r = spawnSync5(editor, [tmp], { stdio: "inherit" });
3854
4420
  if (r.status !== 0) fail5(`editor exited with status ${r.status}`);
3855
- const updated = yaml7.load(fs19.readFileSync(tmp, "utf-8"));
4421
+ const updated = yaml8.load(fs22.readFileSync(tmp, "utf-8"));
3856
4422
  await api.put(`/api/agents/${encodeURIComponent(ref)}`, updated);
3857
- fs19.unlinkSync(tmp);
4423
+ fs22.unlinkSync(tmp);
3858
4424
  console.log(chalk18.green(`updated ${ref}`));
3859
4425
  } catch (err) {
3860
4426
  fail5(err.message);
@@ -3925,7 +4491,7 @@ var mcp = new Command18("mcp").description("Manage per-agent MCP config (mcp_con
3925
4491
  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
4492
  let parsed;
3927
4493
  try {
3928
- parsed = JSON.parse(fs19.readFileSync(path18.resolve(opts.fromFile), "utf-8"));
4494
+ parsed = JSON.parse(fs22.readFileSync(path21.resolve(opts.fromFile), "utf-8"));
3929
4495
  } catch (err) {
3930
4496
  fail5(`cannot parse ${opts.fromFile}: ${err.message}`);
3931
4497
  }
@@ -3968,78 +4534,77 @@ async function streamOutput(ref, agentRunId) {
3968
4534
  }
3969
4535
 
3970
4536
  // src/commands/daemon.ts
3971
- import os7 from "os";
4537
+ import os6 from "os";
4538
+ import { execFileSync as execFileSync2 } from "child_process";
3972
4539
  import { Command as Command19 } from "commander";
3973
4540
  import chalk19 from "chalk";
3974
4541
  import WebSocket from "ws";
3975
4542
 
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;
4543
+ // src/core/daemon-agent-run-sink.ts
4544
+ var BUFFER_CAP = 5e3;
4545
+ var bound = null;
4546
+ var seqByRun = /* @__PURE__ */ new Map();
4547
+ var buffer = [];
4548
+ var logHistoryByRun = /* @__PURE__ */ new Map();
4549
+ function bindAgentRunFrameSink(sink) {
4550
+ bound = sink;
4551
+ if (!sink) return;
4552
+ while (buffer.length > 0) {
4553
+ const frame = buffer.shift();
4554
+ try {
4555
+ sink.send(frame);
4556
+ } catch {
4005
4557
  }
4006
4558
  }
4007
- throw new AdminTokenUnavailableError();
4008
- }
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
4559
  }
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;
4560
+ function deliverOrBuffer(frame) {
4561
+ if (bound) {
4562
+ try {
4563
+ bound.send(frame);
4564
+ return;
4565
+ } catch {
4566
+ }
4029
4567
  }
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 {
4568
+ if (buffer.length >= BUFFER_CAP) {
4569
+ buffer.shift();
4037
4570
  }
4571
+ buffer.push(frame);
4572
+ }
4573
+ function pruneRunHistory(runId) {
4574
+ logHistoryByRun.delete(runId);
4575
+ seqByRun.delete(runId);
4038
4576
  }
4039
- function clearDaemonIdentity() {
4040
- if (!fs21.existsSync(CONFIG_FILE2)) return false;
4041
- fs21.unlinkSync(CONFIG_FILE2);
4042
- return true;
4577
+ function emitAgentRunStatus(runId, status, opts = {}) {
4578
+ deliverOrBuffer({
4579
+ type: "agent_run_status",
4580
+ run_id: runId,
4581
+ status,
4582
+ phase: opts.phase ?? null,
4583
+ exit_code: opts.exitCode ?? null,
4584
+ error: opts.error ?? null,
4585
+ ts: (/* @__PURE__ */ new Date()).toISOString()
4586
+ });
4587
+ if (status === "completed" || status === "failed" || status === "killed") {
4588
+ pruneRunHistory(runId);
4589
+ }
4590
+ }
4591
+ function replayAfterRanges(ranges) {
4592
+ if (!bound) return 0;
4593
+ let sent = 0;
4594
+ for (const { run_id, after_seq } of ranges) {
4595
+ const ring = logHistoryByRun.get(run_id);
4596
+ if (!ring) continue;
4597
+ for (const frame of ring) {
4598
+ if (frame.seq <= after_seq) continue;
4599
+ try {
4600
+ bound.send(frame);
4601
+ sent += 1;
4602
+ } catch {
4603
+ return sent;
4604
+ }
4605
+ }
4606
+ }
4607
+ return sent;
4043
4608
  }
4044
4609
 
4045
4610
  // src/core/register-auth.ts
@@ -4049,7 +4614,7 @@ var RegisterAuthUnavailableError = class extends Error {
4049
4614
  `No registration credential available.
4050
4615
  \u2022 Pass --token <apit_...> with an API token created in the dashboard, or
4051
4616
  \u2022 Set TASK0_API_TOKEN in the environment, or
4052
- \u2022 Fall back to the server's admin token (TASK0_ADMIN_TOKEN or ~/.config/task0/admin.token).`
4617
+ \u2022 Fall back to the server's admin token (TASK0_ADMIN_TOKEN or ~/.task0/admin.token).`
4053
4618
  );
4054
4619
  this.name = "RegisterAuthUnavailableError";
4055
4620
  }
@@ -4075,8 +4640,191 @@ function pickRegisterAuth(flagToken) {
4075
4640
 
4076
4641
  // src/core/daemon-rpc-handlers.ts
4077
4642
  init_node();
4078
- import fs22 from "fs";
4079
- import path21 from "path";
4643
+ import fs25 from "fs";
4644
+ import path24 from "path";
4645
+
4646
+ // src/core/daemon-agent-run-runner.ts
4647
+ import { execFileSync } from "child_process";
4648
+ import fs24 from "fs";
4649
+ import path23 from "path";
4650
+
4651
+ // src/core/daemon-agent-run-dir.ts
4652
+ init_node();
4653
+ import fs23 from "fs";
4654
+ import path22 from "path";
4655
+ function agentRunRoot() {
4656
+ return process.env.TASK0_DAEMON_AGENT_RUN_DIR || path22.join(task0Home(), "agent-run");
4657
+ }
4658
+ function agentRunDir(runId) {
4659
+ return path22.join(agentRunRoot(), runId);
4660
+ }
4661
+ function agentRunStatusPath(runId) {
4662
+ return path22.join(agentRunDir(runId), "status.json");
4663
+ }
4664
+ function ensureAgentRunDir(runId) {
4665
+ const dir = agentRunDir(runId);
4666
+ fs23.mkdirSync(dir, { recursive: true });
4667
+ return dir;
4668
+ }
4669
+ function removeAgentRunDir(runId) {
4670
+ const dir = agentRunDir(runId);
4671
+ if (!fs23.existsSync(dir)) return;
4672
+ fs23.rmSync(dir, { recursive: true, force: true });
4673
+ }
4674
+
4675
+ // src/core/daemon-agent-run-runner.ts
4676
+ var active = /* @__PURE__ */ new Map();
4677
+ var STATUS_POLL_MS = 500;
4678
+ var SESSION_PROBE_MS = 3e3;
4679
+ function getTmuxBin() {
4680
+ const fromEnv = process.env.TASK0_TMUX_BIN;
4681
+ if (fromEnv) return fromEnv;
4682
+ return "tmux";
4683
+ }
4684
+ function checkTmuxAvailable() {
4685
+ try {
4686
+ execFileSync(getTmuxBin(), ["-V"], { stdio: ["ignore", "pipe", "pipe"] });
4687
+ return { ok: true };
4688
+ } catch (err) {
4689
+ return { ok: false, reason: `tmux not available: ${err instanceof Error ? err.message : String(err)}` };
4690
+ }
4691
+ }
4692
+ function isSessionAlive2(sessionName) {
4693
+ try {
4694
+ execFileSync(getTmuxBin(), ["has-session", "-t", sessionName], {
4695
+ stdio: ["ignore", "pipe", "pipe"]
4696
+ });
4697
+ return true;
4698
+ } catch {
4699
+ return false;
4700
+ }
4701
+ }
4702
+ function killSession2(sessionName) {
4703
+ try {
4704
+ execFileSync(getTmuxBin(), ["kill-session", "-t", sessionName], {
4705
+ stdio: ["ignore", "pipe", "pipe"]
4706
+ });
4707
+ return true;
4708
+ } catch {
4709
+ return false;
4710
+ }
4711
+ }
4712
+ function readStatusFile(runId) {
4713
+ const filePath = agentRunStatusPath(runId);
4714
+ if (!fs24.existsSync(filePath)) return null;
4715
+ try {
4716
+ const raw = fs24.readFileSync(filePath, "utf-8");
4717
+ const parsed = JSON.parse(raw);
4718
+ return { raw, parsed };
4719
+ } catch {
4720
+ return null;
4721
+ }
4722
+ }
4723
+ function deriveStatus(parsed) {
4724
+ const raw = typeof parsed.status === "string" ? parsed.status : "starting";
4725
+ let status = "starting";
4726
+ if (raw === "running") status = "running";
4727
+ else if (raw === "done" || raw === "completed") status = "completed";
4728
+ else if (raw === "error" || raw === "failed") status = "failed";
4729
+ const phase = typeof parsed.phase === "string" ? parsed.phase : null;
4730
+ const error2 = typeof parsed.error === "string" && parsed.error ? parsed.error : null;
4731
+ return { status, phase, error: error2 };
4732
+ }
4733
+ function launchAgentRun(params) {
4734
+ const tmuxCheck = checkTmuxAvailable();
4735
+ if (!tmuxCheck.ok) {
4736
+ throw Object.assign(new Error(tmuxCheck.reason ?? "tmux unavailable"), { code: "tmux_unavailable" });
4737
+ }
4738
+ if (active.has(params.runId)) {
4739
+ throw Object.assign(new Error(`agent run already active: ${params.runId}`), { code: "already_running" });
4740
+ }
4741
+ if (!fs24.existsSync(params.workspace)) {
4742
+ throw Object.assign(new Error(`workspace not found: ${params.workspace}`), { code: "workspace_missing" });
4743
+ }
4744
+ const runDir = ensureAgentRunDir(params.runId);
4745
+ const scriptPath = path23.join(runDir, "script.sh");
4746
+ fs24.writeFileSync(scriptPath, params.scriptContent, { mode: 493 });
4747
+ if (params.promptContent !== void 0) {
4748
+ fs24.writeFileSync(path23.join(runDir, "prompt.txt"), params.promptContent, "utf-8");
4749
+ }
4750
+ for (const [name, content] of Object.entries(params.auxFiles ?? {})) {
4751
+ if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
4752
+ throw Object.assign(new Error(`invalid aux file name: ${name}`), { code: "invalid_params" });
4753
+ }
4754
+ fs24.writeFileSync(path23.join(runDir, name), content, "utf-8");
4755
+ }
4756
+ fs24.writeFileSync(
4757
+ agentRunStatusPath(params.runId),
4758
+ JSON.stringify({ status: "starting", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
4759
+ "utf-8"
4760
+ );
4761
+ try {
4762
+ execFileSync(
4763
+ getTmuxBin(),
4764
+ ["new-session", "-d", "-s", params.sessionName, "-c", params.workspace, `bash ${JSON.stringify(scriptPath)}`],
4765
+ { encoding: "utf-8", timeout: 5e3, stdio: ["ignore", "pipe", "pipe"] }
4766
+ );
4767
+ } catch (err) {
4768
+ removeAgentRunDir(params.runId);
4769
+ throw Object.assign(
4770
+ new Error(`failed to start tmux session: ${err instanceof Error ? err.message : String(err)}`),
4771
+ { code: "spawn_failed" }
4772
+ );
4773
+ }
4774
+ emitAgentRunStatus(params.runId, "starting", { phase: null });
4775
+ const entry = {
4776
+ runId: params.runId,
4777
+ sessionName: params.sessionName,
4778
+ lastStatusJson: null,
4779
+ finished: false,
4780
+ pollInterval: setInterval(() => pollOne(params.runId), STATUS_POLL_MS)
4781
+ };
4782
+ active.set(params.runId, entry);
4783
+ const sessionProbe = setInterval(() => {
4784
+ const cur = active.get(params.runId);
4785
+ if (!cur || cur.finished) {
4786
+ clearInterval(sessionProbe);
4787
+ return;
4788
+ }
4789
+ if (!isSessionAlive2(cur.sessionName)) {
4790
+ finishRun(params.runId, "killed", { error: "tmux session disappeared" });
4791
+ clearInterval(sessionProbe);
4792
+ }
4793
+ }, SESSION_PROBE_MS);
4794
+ return { agentRunDir: runDir, sessionName: params.sessionName };
4795
+ }
4796
+ function pollOne(runId) {
4797
+ const entry = active.get(runId);
4798
+ if (!entry || entry.finished) return;
4799
+ const current = readStatusFile(runId);
4800
+ if (!current) return;
4801
+ if (current.raw === entry.lastStatusJson) return;
4802
+ entry.lastStatusJson = current.raw;
4803
+ const { status, phase, error: error2 } = deriveStatus(current.parsed);
4804
+ emitAgentRunStatus(runId, status, { phase, error: error2 });
4805
+ if (status === "completed" || status === "failed") {
4806
+ finishRun(runId, status, { phase, error: error2 });
4807
+ }
4808
+ }
4809
+ function finishRun(runId, status, opts = {}) {
4810
+ const entry = active.get(runId);
4811
+ if (!entry || entry.finished) return;
4812
+ entry.finished = true;
4813
+ clearInterval(entry.pollInterval);
4814
+ emitAgentRunStatus(runId, status, opts);
4815
+ active.delete(runId);
4816
+ }
4817
+ function cancelAgentRun(runId) {
4818
+ const entry = active.get(runId);
4819
+ if (!entry) {
4820
+ return { ok: false, sessionName: null };
4821
+ }
4822
+ killSession2(entry.sessionName);
4823
+ finishRun(runId, "killed", { error: "cancelled by hub" });
4824
+ return { ok: true, sessionName: entry.sessionName };
4825
+ }
4826
+
4827
+ // src/core/daemon-rpc-handlers.ts
4080
4828
  var MAX_FILE_BYTES = 1 * 1024 * 1024;
4081
4829
  function ensureString(value, name) {
4082
4830
  if (typeof value !== "string" || value.length === 0) {
@@ -4084,13 +4832,61 @@ function ensureString(value, name) {
4084
4832
  }
4085
4833
  return value;
4086
4834
  }
4835
+ function ensureSafeTaskId(value) {
4836
+ const id = ensureString(value, "taskId");
4837
+ if (id.includes("/") || id.includes("\\") || id.includes("\0") || id === "." || id === ".." || path24.isAbsolute(id)) {
4838
+ throw Object.assign(new Error(`invalid taskId: ${id}`), { code: "invalid_params" });
4839
+ }
4840
+ return id;
4841
+ }
4842
+ function assertContained(rootAbs, resolvedAbs) {
4843
+ const root = path24.resolve(rootAbs);
4844
+ const target = path24.resolve(resolvedAbs);
4845
+ if (target !== root && !target.startsWith(root + path24.sep)) {
4846
+ throw Object.assign(new Error("path escapes containment root"), { code: "invalid_params" });
4847
+ }
4848
+ }
4849
+ function optionalString(value) {
4850
+ if (value === void 0 || value === null) return void 0;
4851
+ if (typeof value !== "string") {
4852
+ throw Object.assign(new Error("expected string"), { code: "invalid_params" });
4853
+ }
4854
+ return value;
4855
+ }
4856
+ function optionalBoolean(value) {
4857
+ if (value === void 0 || value === null) return void 0;
4858
+ if (typeof value !== "boolean") {
4859
+ throw Object.assign(new Error("expected boolean"), { code: "invalid_params" });
4860
+ }
4861
+ return value;
4862
+ }
4863
+ function listProjects() {
4864
+ return loadConfig().sources.filter((source2) => source2.type === "project");
4865
+ }
4866
+ function applyRepair(r) {
4867
+ const raw = readYaml(r.taskYml);
4868
+ if (!raw) return;
4869
+ if (r.reason === "missing_object_id") raw.object_id = generateObjectId("task");
4870
+ if (r.reason === "id_mismatch") raw.id = r.dirName;
4871
+ writeYaml(r.taskYml, raw);
4872
+ }
4873
+ function scanProjectWithRepair(projectPath, sourceName) {
4874
+ let result = scanProject(projectPath, sourceName);
4875
+ if (result.repairs.length > 0) {
4876
+ for (const r of result.repairs) applyRepair(r);
4877
+ result = scanProject(projectPath, sourceName);
4878
+ }
4879
+ return result;
4880
+ }
4087
4881
  var rpcHandlers = {
4088
4882
  // Scan a local project for its task manifest. Returns the same shape the
4089
- // in-process server's /api/tasks scanProject() returns.
4883
+ // hub's in-process scanProject() used to return — with auto-repair of
4884
+ // missing object_id / id mismatches applied in place on the daemon's FS.
4090
4885
  async scan_project(params) {
4091
4886
  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);
4887
+ const name = typeof params.name === "string" && params.name ? params.name : path24.basename(rootPath);
4888
+ const result = scanProjectWithRepair(rootPath, name);
4889
+ return { tasks: result.tasks, errors: result.errors };
4094
4890
  },
4095
4891
  // Read a file from disk on the daemon's host. The dashboard uses this to
4096
4892
  // peek into archived task content, runtime logs, etc. Cap is 1 MiB to
@@ -4099,7 +4895,7 @@ var rpcHandlers = {
4099
4895
  const filePath = ensureString(params.path, "path");
4100
4896
  let stat;
4101
4897
  try {
4102
- stat = fs22.statSync(filePath);
4898
+ stat = fs25.statSync(filePath);
4103
4899
  } catch {
4104
4900
  throw Object.assign(new Error("file not found"), { code: "not_found" });
4105
4901
  }
@@ -4109,23 +4905,261 @@ var rpcHandlers = {
4109
4905
  if (stat.size > MAX_FILE_BYTES) {
4110
4906
  throw Object.assign(new Error(`file too large (${stat.size} bytes > ${MAX_FILE_BYTES})`), { code: "too_large" });
4111
4907
  }
4112
- const content = fs22.readFileSync(filePath, "utf-8");
4908
+ const content = fs25.readFileSync(filePath, "utf-8");
4113
4909
  return { content, size: stat.size, modifiedAt: stat.mtime.toISOString() };
4910
+ },
4911
+ // ---------------------------------------------------------------------
4912
+ // Project source CRUD. The daemon owns its host's project list; the hub
4913
+ // routes /api/daemons/:id/projects through these handlers instead of
4914
+ // writing config.yml directly. After every mutation we re-push manifest
4915
+ // so the hub's daemon_projects table catches up without waiting for a
4916
+ // restart.
4917
+ // ---------------------------------------------------------------------
4918
+ async project_list() {
4919
+ return { projects: listProjects() };
4920
+ },
4921
+ async project_add(params, ctx) {
4922
+ const rawPath = ensureString(params.path, "path");
4923
+ const absPath = path24.resolve(rawPath);
4924
+ if (!fs25.existsSync(absPath)) {
4925
+ throw Object.assign(new Error(`path does not exist: ${absPath}`), { code: "not_found" });
4926
+ }
4927
+ const stat = fs25.statSync(absPath);
4928
+ if (!stat.isDirectory()) {
4929
+ throw Object.assign(new Error(`path is not a directory: ${absPath}`), { code: "invalid_target" });
4930
+ }
4931
+ const name = optionalString(params.name)?.trim() || path24.basename(absPath);
4932
+ const result = scanProjectWithRepair(absPath, name);
4933
+ addSource({ name, type: "project", path: absPath, enabled: true });
4934
+ ctx.notifyManifestChanged();
4935
+ return {
4936
+ project: { name, type: "project", path: absPath, enabled: true },
4937
+ taskCount: result.tasks.length
4938
+ };
4939
+ },
4940
+ async project_remove(params, ctx) {
4941
+ const name = ensureString(params.name, "name");
4942
+ const ok = removeSource(name);
4943
+ if (!ok) {
4944
+ throw Object.assign(new Error(`project not found: ${name}`), { code: "not_found" });
4945
+ }
4946
+ ctx.notifyManifestChanged();
4947
+ return { ok: true };
4948
+ },
4949
+ async project_set_enabled(params, ctx) {
4950
+ const name = ensureString(params.name, "name");
4951
+ const enabled = optionalBoolean(params.enabled);
4952
+ if (enabled === void 0) {
4953
+ throw Object.assign(new Error("enabled is required (boolean)"), { code: "invalid_params" });
4954
+ }
4955
+ const config = loadConfig();
4956
+ const source2 = config.sources.find((s) => s.name === name && s.type === "project");
4957
+ if (!source2) {
4958
+ throw Object.assign(new Error(`project not found: ${name}`), { code: "not_found" });
4959
+ }
4960
+ source2.enabled = enabled;
4961
+ saveConfig(config);
4962
+ ctx.notifyManifestChanged();
4963
+ return { project: source2 };
4964
+ },
4965
+ // ---------------------------------------------------------------------
4966
+ // Task content R/W. These are the deep-pull operations from Q5(c): the
4967
+ // hub asks the daemon to read/write/create/delete a task's yaml + sibling
4968
+ // files on the daemon's FS. After every mutation we re-push manifest so
4969
+ // hub's daemon_tasks index reflects the new state without a restart.
4970
+ // ---------------------------------------------------------------------
4971
+ // Read a task's task0.yml (parsed) plus an optional list of sibling files
4972
+ // in the same task directory. Path is resolved from (projectName, taskId).
4973
+ async task_read(params) {
4974
+ const projectName = ensureString(params.projectName, "projectName");
4975
+ const taskId = ensureSafeTaskId(params.taskId);
4976
+ const project2 = listProjects().find((p) => p.name === projectName);
4977
+ if (!project2) {
4978
+ throw Object.assign(new Error(`project not found: ${projectName}`), { code: "not_found" });
4979
+ }
4980
+ const projectAbs = path24.resolve(project2.path);
4981
+ const projectConfig = readYaml(path24.join(projectAbs, "task0.yml"));
4982
+ if (!projectConfig) {
4983
+ throw Object.assign(new Error(`invalid project: missing task0.yml at ${projectAbs}`), { code: "invalid_project" });
4984
+ }
4985
+ const tasksRoot = path24.resolve(projectAbs, projectConfig.tasks_dir);
4986
+ const taskDir = path24.resolve(tasksRoot, taskId);
4987
+ assertContained(tasksRoot, taskDir);
4988
+ if (!fs25.existsSync(taskDir)) {
4989
+ throw Object.assign(new Error(`task not found: ${taskId}`), { code: "not_found" });
4990
+ }
4991
+ const taskYml = path24.join(taskDir, "task0.yml");
4992
+ const yaml10 = readYaml(taskYml);
4993
+ if (!yaml10) {
4994
+ throw Object.assign(new Error(`task yaml missing or unreadable: ${taskYml}`), { code: "not_found" });
4995
+ }
4996
+ const files = fs25.readdirSync(taskDir).filter((name) => name !== "task0.yml");
4997
+ return { task_dir: taskDir, yaml: yaml10, files };
4998
+ },
4999
+ // Create a task directory + write task0.yml + write any additional named
5000
+ // files. Hub decides the taskId and yaml content (slug/object_id are
5001
+ // generated server-side); daemon just commits to disk.
5002
+ async task_create(params, ctx) {
5003
+ const projectName = ensureString(params.projectName, "projectName");
5004
+ const taskId = ensureSafeTaskId(params.taskId);
5005
+ const yaml10 = params.yaml;
5006
+ if (!yaml10 || typeof yaml10 !== "object") {
5007
+ throw Object.assign(new Error("yaml (object) is required"), { code: "invalid_params" });
5008
+ }
5009
+ const files = params.files ?? {};
5010
+ const project2 = listProjects().find((p) => p.name === projectName);
5011
+ if (!project2) {
5012
+ throw Object.assign(new Error(`project not found: ${projectName}`), { code: "not_found" });
5013
+ }
5014
+ const projectAbs = path24.resolve(project2.path);
5015
+ const projectConfig = readYaml(path24.join(projectAbs, "task0.yml"));
5016
+ if (!projectConfig) {
5017
+ throw Object.assign(new Error(`invalid project: missing task0.yml at ${projectAbs}`), { code: "invalid_project" });
5018
+ }
5019
+ const tasksRoot = path24.resolve(projectAbs, projectConfig.tasks_dir);
5020
+ const taskDir = path24.resolve(tasksRoot, taskId);
5021
+ assertContained(tasksRoot, taskDir);
5022
+ if (fs25.existsSync(taskDir)) {
5023
+ throw Object.assign(new Error(`task already exists: ${taskId}`), { code: "already_exists" });
5024
+ }
5025
+ fs25.mkdirSync(taskDir, { recursive: true });
5026
+ writeYaml(path24.join(taskDir, "task0.yml"), yaml10);
5027
+ for (const [name, content] of Object.entries(files)) {
5028
+ if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
5029
+ throw Object.assign(new Error(`invalid file name: ${name}`), { code: "invalid_params" });
5030
+ }
5031
+ fs25.writeFileSync(path24.join(taskDir, name), content, "utf-8");
5032
+ }
5033
+ ctx.notifyManifestChanged();
5034
+ return { task_dir: taskDir };
5035
+ },
5036
+ // Patch a task's task0.yml in place. The hub computes the desired yaml
5037
+ // (typically by loading + applying its own metadata patch), then sends
5038
+ // the full replacement object back. Atomic at the file level — no
5039
+ // partial writes.
5040
+ async task_write_yaml(params, ctx) {
5041
+ const projectName = ensureString(params.projectName, "projectName");
5042
+ const taskId = ensureSafeTaskId(params.taskId);
5043
+ const yaml10 = params.yaml;
5044
+ if (!yaml10 || typeof yaml10 !== "object") {
5045
+ throw Object.assign(new Error("yaml (object) is required"), { code: "invalid_params" });
5046
+ }
5047
+ const project2 = listProjects().find((p) => p.name === projectName);
5048
+ if (!project2) {
5049
+ throw Object.assign(new Error(`project not found: ${projectName}`), { code: "not_found" });
5050
+ }
5051
+ const projectAbs = path24.resolve(project2.path);
5052
+ const projectConfig = readYaml(path24.join(projectAbs, "task0.yml"));
5053
+ if (!projectConfig) {
5054
+ throw Object.assign(new Error(`invalid project: missing task0.yml at ${projectAbs}`), { code: "invalid_project" });
5055
+ }
5056
+ const tasksRoot = path24.resolve(projectAbs, projectConfig.tasks_dir);
5057
+ const taskDir = path24.resolve(tasksRoot, taskId);
5058
+ assertContained(tasksRoot, taskDir);
5059
+ const taskYml = path24.join(taskDir, "task0.yml");
5060
+ if (!fs25.existsSync(taskYml)) {
5061
+ throw Object.assign(new Error(`task yaml not found: ${taskYml}`), { code: "not_found" });
5062
+ }
5063
+ writeYaml(taskYml, yaml10);
5064
+ ctx.notifyManifestChanged();
5065
+ return { task_dir: taskDir };
5066
+ },
5067
+ // Delete an entire task directory (use sparingly; mostly for archive
5068
+ // workflows). Returns the deleted path so the hub can emit an event.
5069
+ async task_delete(params, ctx) {
5070
+ const projectName = ensureString(params.projectName, "projectName");
5071
+ const taskId = ensureSafeTaskId(params.taskId);
5072
+ const project2 = listProjects().find((p) => p.name === projectName);
5073
+ if (!project2) {
5074
+ throw Object.assign(new Error(`project not found: ${projectName}`), { code: "not_found" });
5075
+ }
5076
+ const projectAbs = path24.resolve(project2.path);
5077
+ const projectConfig = readYaml(path24.join(projectAbs, "task0.yml"));
5078
+ if (!projectConfig) {
5079
+ throw Object.assign(new Error(`invalid project: missing task0.yml at ${projectAbs}`), { code: "invalid_project" });
5080
+ }
5081
+ const tasksRoot = path24.resolve(projectAbs, projectConfig.tasks_dir);
5082
+ const taskDir = path24.resolve(tasksRoot, taskId);
5083
+ assertContained(tasksRoot, taskDir);
5084
+ if (!fs25.existsSync(taskDir)) {
5085
+ throw Object.assign(new Error(`task not found: ${taskId}`), { code: "not_found" });
5086
+ }
5087
+ fs25.rmSync(taskDir, { recursive: true, force: true });
5088
+ ctx.notifyManifestChanged();
5089
+ return { task_dir: taskDir, deleted: true };
5090
+ },
5091
+ // ---------------------------------------------------------------------
5092
+ // Agent run lifecycle (P3.A scaffolding — handlers reject until P3.B
5093
+ // moves the actual spawn / tmux / supervise logic from hub's
5094
+ // agent-run-launcher.ts into the daemon).
5095
+ //
5096
+ // Wire shape (subject to refinement in P3.B):
5097
+ // agent_run_launch → {projectName, runId, runType, agentSlug,
5098
+ // workspace, prompt, ...} → {tmuxSession}
5099
+ // Daemon then streams agent_run_log /
5100
+ // agent_run_status frames over WS.
5101
+ // agent_run_cancel → {runId} → {ok: true}
5102
+ // ---------------------------------------------------------------------
5103
+ // Skill discovery on the daemon's host. Mirrors hub-side
5104
+ // resolveAgentSkill() — given a workspace + agent + filePath, returns
5105
+ // the matching AgentSkill record or null. P3.C uses this so the hub
5106
+ // can dispatch agent runs to remote daemons without itself needing FS
5107
+ // access to the workspace.
5108
+ async agent_skill_resolve(params) {
5109
+ const workspace = ensureString(params.workspace, "workspace");
5110
+ const agent2 = ensureString(params.agent, "agent");
5111
+ const filePath = ensureString(params.filePath, "filePath");
5112
+ if (agent2 !== "claude_code" && agent2 !== "codex" && agent2 !== "cursor") {
5113
+ throw Object.assign(new Error(`unknown skill agent: ${agent2}`), { code: "invalid_params" });
5114
+ }
5115
+ const projectSkills = getProjectAgentSkills(workspace);
5116
+ const globalSkills = getGlobalAgentSkills();
5117
+ const all = [...projectSkills, ...globalSkills];
5118
+ const resolved = path24.resolve(filePath);
5119
+ const skill = all.find(
5120
+ (s) => s.agent === agent2 && path24.resolve(s.filePath) === resolved
5121
+ ) ?? null;
5122
+ return { skill };
5123
+ },
5124
+ async agent_run_launch(params) {
5125
+ const runId = ensureString(params.runId, "runId");
5126
+ const sessionName = ensureString(params.sessionName, "sessionName");
5127
+ const workspace = ensureString(params.workspace, "workspace");
5128
+ const scriptContent = ensureString(params.scriptContent, "scriptContent");
5129
+ const auxFiles = params.auxFiles && typeof params.auxFiles === "object" ? params.auxFiles : {};
5130
+ const promptContent = typeof params.promptContent === "string" ? params.promptContent : void 0;
5131
+ const { agentRunDir: agentRunDir2, sessionName: actualSessionName } = launchAgentRun({
5132
+ runId,
5133
+ sessionName,
5134
+ workspace,
5135
+ scriptContent,
5136
+ auxFiles,
5137
+ promptContent
5138
+ });
5139
+ return { agent_run_dir: agentRunDir2, tmux_session: actualSessionName };
5140
+ },
5141
+ async agent_run_cancel(params) {
5142
+ const runId = ensureString(params.runId, "runId");
5143
+ const result = cancelAgentRun(runId);
5144
+ if (!result.ok) {
5145
+ throw Object.assign(new Error(`no active agent run: ${runId}`), { code: "not_found" });
5146
+ }
5147
+ return { ok: true, tmux_session: result.sessionName };
4114
5148
  }
4115
5149
  };
4116
5150
 
4117
5151
  // src/core/daemon-service/launchd.ts
4118
5152
  import { spawnSync as spawnSync6 } from "child_process";
4119
- import fs24 from "fs";
4120
- import path23 from "path";
5153
+ import fs27 from "fs";
5154
+ import path26 from "path";
4121
5155
 
4122
5156
  // src/core/daemon-service/binary.ts
4123
- import fs23 from "fs";
5157
+ import fs26 from "fs";
4124
5158
  import { fileURLToPath } from "url";
4125
5159
  function resolveTask0Invocation() {
4126
5160
  const node = process.execPath;
4127
5161
  const argv1 = process.argv[1] ?? fileURLToPath(import.meta.url);
4128
- const main2 = fs23.realpathSync(argv1);
5162
+ const main2 = fs26.realpathSync(argv1);
4129
5163
  if (!isInstalledBuild(main2) && process.env.TASK0_ALLOW_DEV_SERVICE !== "1") {
4130
5164
  throw new Error(
4131
5165
  `Refusing to install autostart service pointing at ${main2}.
@@ -4141,31 +5175,40 @@ function isInstalledBuild(p) {
4141
5175
  }
4142
5176
 
4143
5177
  // src/core/daemon-service/paths.ts
4144
- import os6 from "os";
4145
- import path22 from "path";
5178
+ init_node();
5179
+ import os5 from "os";
5180
+ import path25 from "path";
4146
5181
 
4147
5182
  // src/core/daemon-service/types.ts
4148
- var SERVICE_LABEL = "cc.cy0.task0";
5183
+ import crypto2 from "crypto";
5184
+ var BASE_SERVICE_LABEL = "cc.cy0.task0";
5185
+ function serviceLabel() {
5186
+ const home = process.env.TASK0_HOME;
5187
+ if (!home || home.length === 0) return BASE_SERVICE_LABEL;
5188
+ const hash = crypto2.createHash("sha256").update(home).digest("hex").slice(0, 8);
5189
+ return `${BASE_SERVICE_LABEL}.${hash}`;
5190
+ }
4149
5191
 
4150
5192
  // src/core/daemon-service/paths.ts
4151
5193
  function unitPath(scope) {
4152
5194
  const platform = process.platform;
5195
+ const label = serviceLabel();
4153
5196
  if (platform === "darwin") {
4154
- return scope === "user" ? path22.join(os6.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`) : path22.join("/", "Library", "LaunchDaemons", `${SERVICE_LABEL}.plist`);
5197
+ return scope === "user" ? path25.join(os5.homedir(), "Library", "LaunchAgents", `${label}.plist`) : path25.join("/", "Library", "LaunchDaemons", `${label}.plist`);
4155
5198
  }
4156
5199
  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`);
5200
+ return scope === "user" ? path25.join(os5.homedir(), ".config", "systemd", "user", `${label}.service`) : path25.join("/", "etc", "systemd", "system", `${label}.service`);
4158
5201
  }
4159
5202
  throw new Error(`Unsupported platform for service install: ${platform}`);
4160
5203
  }
4161
5204
  function logDir() {
4162
- return path22.join(os6.homedir(), ".task0", "logs");
5205
+ return path25.join(task0Home(), "logs");
4163
5206
  }
4164
5207
  function logPaths() {
4165
5208
  const dir = logDir();
4166
5209
  return {
4167
- out: path22.join(dir, "daemon.out.log"),
4168
- err: path22.join(dir, "daemon.err.log")
5210
+ out: path25.join(dir, "daemon.out.log"),
5211
+ err: path25.join(dir, "daemon.err.log")
4169
5212
  };
4170
5213
  }
4171
5214
 
@@ -4173,14 +5216,29 @@ function logPaths() {
4173
5216
  function escapeXml(s) {
4174
5217
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
4175
5218
  }
5219
+ function collectTask0Env() {
5220
+ const out = {};
5221
+ for (const [k, v] of Object.entries(process.env)) {
5222
+ if (k.startsWith("TASK0_") && typeof v === "string") out[k] = v;
5223
+ }
5224
+ return out;
5225
+ }
4176
5226
  function renderPlist(opts) {
4177
5227
  const programArgs = [opts.node, opts.main, ...opts.args].map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
5228
+ const envLines = [
5229
+ ` <key>PATH</key>`,
5230
+ ` <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>`,
5231
+ ...Object.entries(opts.task0Env ?? {}).flatMap(([k, v]) => [
5232
+ ` <key>${escapeXml(k)}</key>`,
5233
+ ` <string>${escapeXml(v)}</string>`
5234
+ ])
5235
+ ].join("\n");
4178
5236
  return `<?xml version="1.0" encoding="UTF-8"?>
4179
5237
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4180
5238
  <plist version="1.0">
4181
5239
  <dict>
4182
5240
  <key>Label</key>
4183
- <string>${SERVICE_LABEL}</string>
5241
+ <string>${serviceLabel()}</string>
4184
5242
  <key>ProgramArguments</key>
4185
5243
  <array>
4186
5244
  ${programArgs}
@@ -4202,8 +5260,7 @@ ${programArgs}
4202
5260
  <string>${escapeXml(opts.home)}</string>
4203
5261
  <key>EnvironmentVariables</key>
4204
5262
  <dict>
4205
- <key>PATH</key>
4206
- <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
5263
+ ${envLines}
4207
5264
  </dict>
4208
5265
  </dict>
4209
5266
  </plist>
@@ -4215,7 +5272,7 @@ function domainTarget(scope) {
4215
5272
  return `gui/${uid}`;
4216
5273
  }
4217
5274
  function serviceTarget(scope) {
4218
- return `${domainTarget(scope)}/${SERVICE_LABEL}`;
5275
+ return `${domainTarget(scope)}/${serviceLabel()}`;
4219
5276
  }
4220
5277
  function run2(cmd, args) {
4221
5278
  const res = spawnSync6(cmd, args, { encoding: "utf-8" });
@@ -4226,17 +5283,18 @@ function createLaunchdManager(scope) {
4226
5283
  const logs = logPaths();
4227
5284
  async function install() {
4228
5285
  const inv = resolveTask0Invocation();
4229
- fs24.mkdirSync(logDir(), { recursive: true });
4230
- fs24.mkdirSync(path23.dirname(file), { recursive: true });
5286
+ fs27.mkdirSync(logDir(), { recursive: true });
5287
+ fs27.mkdirSync(path26.dirname(file), { recursive: true });
4231
5288
  const body = renderPlist({
4232
5289
  node: inv.node,
4233
5290
  main: inv.main,
4234
5291
  args: inv.args,
4235
5292
  home: process.env.HOME ?? "/",
4236
5293
  out: logs.out,
4237
- err: logs.err
5294
+ err: logs.err,
5295
+ task0Env: collectTask0Env()
4238
5296
  });
4239
- fs24.writeFileSync(file, body, { mode: 420 });
5297
+ fs27.writeFileSync(file, body, { mode: 420 });
4240
5298
  const bootstrap = run2("launchctl", ["bootstrap", domainTarget(scope), file]);
4241
5299
  if (bootstrap.code !== 0) {
4242
5300
  const already = /already loaded|service already bootstrapped/i.test(bootstrap.stderr);
@@ -4262,9 +5320,9 @@ function createLaunchdManager(scope) {
4262
5320
  }
4263
5321
  async function uninstall() {
4264
5322
  run2("launchctl", ["bootout", serviceTarget(scope)]);
4265
- if (fs24.existsSync(file)) {
5323
+ if (fs27.existsSync(file)) {
4266
5324
  run2("launchctl", ["unload", file]);
4267
- fs24.unlinkSync(file);
5325
+ fs27.unlinkSync(file);
4268
5326
  }
4269
5327
  }
4270
5328
  async function start() {
@@ -4283,7 +5341,7 @@ function createLaunchdManager(scope) {
4283
5341
  }
4284
5342
  }
4285
5343
  async function status() {
4286
- if (!fs24.existsSync(file)) return "absent";
5344
+ if (!fs27.existsSync(file)) return "absent";
4287
5345
  const printed = run2("launchctl", ["print", serviceTarget(scope)]);
4288
5346
  if (printed.code !== 0) return "installed";
4289
5347
  const out = printed.stdout;
@@ -4306,15 +5364,26 @@ function createLaunchdManager(scope) {
4306
5364
 
4307
5365
  // src/core/daemon-service/systemd.ts
4308
5366
  import { spawnSync as spawnSync7 } from "child_process";
4309
- import fs25 from "fs";
4310
- import path24 from "path";
5367
+ import fs28 from "fs";
5368
+ import path27 from "path";
4311
5369
  function shellEscape(s) {
4312
5370
  if (!/[\s"\\$]/.test(s)) return s;
4313
5371
  return `"${s.replace(/[\\"]/g, (m) => `\\${m}`)}"`;
4314
5372
  }
5373
+ function collectTask0Env2() {
5374
+ const out = {};
5375
+ for (const [k, v] of Object.entries(process.env)) {
5376
+ if (k.startsWith("TASK0_") && typeof v === "string") out[k] = v;
5377
+ }
5378
+ return out;
5379
+ }
4315
5380
  function renderUnit(opts) {
4316
5381
  const execStart = [opts.node, opts.main, ...opts.args].map(shellEscape).join(" ");
4317
5382
  const wantedBy = opts.scope === "user" ? "default.target" : "multi-user.target";
5383
+ const envLines = ["Environment=NODE_ENV=production"];
5384
+ for (const [k, v] of Object.entries(opts.task0Env ?? {})) {
5385
+ envLines.push(`Environment=${k}=${shellEscape(v)}`);
5386
+ }
4318
5387
  return `[Unit]
4319
5388
  Description=task0 daemon \u2014 central-server bridge
4320
5389
  After=network-online.target
@@ -4327,7 +5396,7 @@ Restart=on-failure
4327
5396
  RestartSec=5s
4328
5397
  StandardOutput=append:${opts.out}
4329
5398
  StandardError=append:${opts.err}
4330
- Environment=NODE_ENV=production
5399
+ ${envLines.join("\n")}
4331
5400
 
4332
5401
  [Install]
4333
5402
  WantedBy=${wantedBy}
@@ -4343,20 +5412,21 @@ function run3(cmd, args) {
4343
5412
  function createSystemdManager(scope) {
4344
5413
  const file = unitPath(scope);
4345
5414
  const logs = logPaths();
4346
- const unitName = `${SERVICE_LABEL}.service`;
5415
+ const unitName = `${serviceLabel()}.service`;
4347
5416
  async function install() {
4348
5417
  const inv = resolveTask0Invocation();
4349
- fs25.mkdirSync(logDir(), { recursive: true });
4350
- fs25.mkdirSync(path24.dirname(file), { recursive: true });
5418
+ fs28.mkdirSync(logDir(), { recursive: true });
5419
+ fs28.mkdirSync(path27.dirname(file), { recursive: true });
4351
5420
  const body = renderUnit({
4352
5421
  node: inv.node,
4353
5422
  main: inv.main,
4354
5423
  args: inv.args,
4355
5424
  out: logs.out,
4356
5425
  err: logs.err,
4357
- scope
5426
+ scope,
5427
+ task0Env: collectTask0Env2()
4358
5428
  });
4359
- fs25.writeFileSync(file, body, { mode: 420 });
5429
+ fs28.writeFileSync(file, body, { mode: 420 });
4360
5430
  const reload = run3("systemctl", [...scopeFlag(scope), "daemon-reload"]);
4361
5431
  if (reload.code !== 0) {
4362
5432
  throw new Error(`systemctl daemon-reload failed: ${reload.stderr}`);
@@ -4365,8 +5435,8 @@ function createSystemdManager(scope) {
4365
5435
  }
4366
5436
  async function uninstall() {
4367
5437
  run3("systemctl", [...scopeFlag(scope), "disable", "--now", unitName]);
4368
- if (fs25.existsSync(file)) {
4369
- fs25.unlinkSync(file);
5438
+ if (fs28.existsSync(file)) {
5439
+ fs28.unlinkSync(file);
4370
5440
  }
4371
5441
  run3("systemctl", [...scopeFlag(scope), "daemon-reload"]);
4372
5442
  }
@@ -4383,7 +5453,7 @@ function createSystemdManager(scope) {
4383
5453
  }
4384
5454
  }
4385
5455
  async function status() {
4386
- if (!fs25.existsSync(file)) return "absent";
5456
+ if (!fs28.existsSync(file)) return "absent";
4387
5457
  const res = run3("systemctl", [...scopeFlag(scope), "is-active", unitName]);
4388
5458
  const out = res.stdout.trim();
4389
5459
  if (out === "active") return "running";
@@ -4416,8 +5486,100 @@ function getServiceManager(scope) {
4416
5486
  );
4417
5487
  }
4418
5488
 
5489
+ // src/core/cli-version.ts
5490
+ import { readFileSync } from "fs";
5491
+ import { fileURLToPath as fileURLToPath2 } from "url";
5492
+ import path28 from "path";
5493
+ var cached2 = null;
5494
+ function readCliVersion() {
5495
+ if (cached2 !== null) return cached2;
5496
+ const here = path28.dirname(fileURLToPath2(import.meta.url));
5497
+ const candidates = [
5498
+ path28.resolve(here, "..", "package.json"),
5499
+ path28.resolve(here, "..", "..", "package.json")
5500
+ ];
5501
+ for (const candidate of candidates) {
5502
+ try {
5503
+ const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
5504
+ if (pkg.name === "@task0/cli" && pkg.version) {
5505
+ cached2 = pkg.version;
5506
+ return cached2;
5507
+ }
5508
+ } catch {
5509
+ }
5510
+ }
5511
+ cached2 = "unknown";
5512
+ return cached2;
5513
+ }
5514
+
5515
+ // src/core/scaffold-default-agents.ts
5516
+ init_node();
5517
+ import fs29 from "fs";
5518
+ import path29 from "path";
5519
+ import yaml9 from "js-yaml";
5520
+ function userAgentsDir() {
5521
+ return path29.join(task0Home(), "agents");
5522
+ }
5523
+ var NAMES = {
5524
+ "claude-code": "Claude Code",
5525
+ codex: "Codex",
5526
+ "cursor-agent": "Cursor"
5527
+ };
5528
+ var DESCRIPTIONS = {
5529
+ "claude-code": "Anthropic Claude Code CLI, launched inside a tmux session against the task workspace.",
5530
+ codex: "OpenAI Codex CLI, launched with `codex exec` against the task workspace.",
5531
+ "cursor-agent": "Cursor agent CLI (`cursor-agent`), launched against the task workspace."
5532
+ };
5533
+ var STABLE_OBJECT_IDS = {
5534
+ "claude-code": "agt_sysCC",
5535
+ codex: "agt_sysCX",
5536
+ "cursor-agent": "agt_sysCR"
5537
+ };
5538
+ function buildDefaultAgentYaml(provider) {
5539
+ const fetchCommand = defaultAgentModelFetchCommand(provider);
5540
+ const fetchFormat = defaultAgentModelOutputFormat(provider);
5541
+ return {
5542
+ object_id: STABLE_OBJECT_IDS[provider],
5543
+ slug: provider,
5544
+ name: NAMES[provider],
5545
+ description: DESCRIPTIONS[provider],
5546
+ kind: "coding",
5547
+ spec: {
5548
+ agent_provider: provider,
5549
+ model: AGENT_DEFAULT_MODEL[provider],
5550
+ effort: AGENT_DEFAULT_EFFORT[provider],
5551
+ available_models: AGENT_MODEL_DEFAULTS[provider].map((m) => ({ id: m.id, label: m.label })),
5552
+ available_efforts: AGENT_EFFORT_DEFAULTS[provider].map((m) => ({ id: m.id, label: m.label })),
5553
+ ...fetchCommand ? { model_fetch_command: fetchCommand } : {},
5554
+ ...fetchFormat ? { model_fetch_format: fetchFormat } : {}
5555
+ }
5556
+ };
5557
+ }
5558
+ function isDirectoryEmpty(dir) {
5559
+ if (!fs29.existsSync(dir)) return true;
5560
+ try {
5561
+ return fs29.readdirSync(dir).length === 0;
5562
+ } catch {
5563
+ return true;
5564
+ }
5565
+ }
5566
+ function scaffoldDefaultAgentsIfEmpty() {
5567
+ const dir = userAgentsDir();
5568
+ if (!isDirectoryEmpty(dir)) {
5569
+ return { scaffolded: false, written: [] };
5570
+ }
5571
+ fs29.mkdirSync(dir, { recursive: true });
5572
+ const written = [];
5573
+ for (const provider of AGENT_PROVIDERS) {
5574
+ const file = path29.join(dir, `${provider}.yml`);
5575
+ const body = yaml9.dump(buildDefaultAgentYaml(provider), { lineWidth: 100 });
5576
+ fs29.writeFileSync(file, body, "utf-8");
5577
+ written.push(file);
5578
+ }
5579
+ return { scaffolded: true, written };
5580
+ }
5581
+
4419
5582
  // src/commands/daemon.ts
4420
- var DAEMON_VERSION = "0.1.0";
4421
5583
  async function dispatchRpc(ws, id, method, params) {
4422
5584
  const handler = rpcHandlers[method];
4423
5585
  if (!handler) {
@@ -4425,7 +5587,8 @@ async function dispatchRpc(ws, id, method, params) {
4425
5587
  return;
4426
5588
  }
4427
5589
  try {
4428
- const result = await handler(params ?? {});
5590
+ const ctx = { notifyManifestChanged: () => pushManifest(ws) };
5591
+ const result = await handler(params ?? {}, ctx);
4429
5592
  sendRpc(ws, { type: "rpc_response", id, result });
4430
5593
  } catch (error2) {
4431
5594
  const code = error2 && typeof error2 === "object" && "code" in error2 && typeof error2.code === "string" ? error2.code : "handler_error";
@@ -4438,6 +5601,79 @@ function sendRpc(ws, payload) {
4438
5601
  ws.send(JSON.stringify(payload));
4439
5602
  }
4440
5603
  }
5604
+ function summariseTask(projectName, task2) {
5605
+ const { object_id = null, id, status, title, task_dir, ...extra } = task2;
5606
+ return {
5607
+ project: projectName,
5608
+ object_id: typeof object_id === "string" ? object_id : null,
5609
+ id,
5610
+ status,
5611
+ title,
5612
+ task_dir,
5613
+ extra
5614
+ };
5615
+ }
5616
+ function buildManifest() {
5617
+ const projectSources = loadConfig().sources.filter((source2) => source2.type === "project");
5618
+ const projects = projectSources.map((source2) => ({
5619
+ name: source2.name,
5620
+ path: source2.path,
5621
+ enabled: source2.enabled
5622
+ }));
5623
+ const tasks = [];
5624
+ const scanErrors = {};
5625
+ for (const source2 of projectSources) {
5626
+ if (!source2.enabled) continue;
5627
+ try {
5628
+ const result = scanProjectWithRepair(source2.path, source2.name);
5629
+ for (const task2 of result.tasks) tasks.push(summariseTask(source2.name, task2));
5630
+ if (result.errors.length > 0) scanErrors[source2.name] = result.errors;
5631
+ } catch (err) {
5632
+ scanErrors[source2.name] = [err instanceof Error ? err.message : String(err)];
5633
+ }
5634
+ }
5635
+ return { type: "manifest", projects, tasks, scan_errors: scanErrors };
5636
+ }
5637
+ function pushManifest(ws) {
5638
+ if (ws.readyState !== ws.OPEN) return;
5639
+ const manifest = buildManifest();
5640
+ ws.send(JSON.stringify(manifest));
5641
+ }
5642
+ var AGENT_PROVIDER_BINARIES = {
5643
+ "claude-code": "claude",
5644
+ codex: "codex",
5645
+ "cursor-agent": "cursor-agent"
5646
+ };
5647
+ function detectAgentProvider(provider) {
5648
+ const binary = AGENT_PROVIDER_BINARIES[provider];
5649
+ let resolvedPath;
5650
+ try {
5651
+ resolvedPath = execFileSync2("which", [binary], {
5652
+ encoding: "utf-8",
5653
+ timeout: 2e3,
5654
+ stdio: ["ignore", "pipe", "ignore"]
5655
+ }).trim();
5656
+ } catch {
5657
+ return null;
5658
+ }
5659
+ if (!resolvedPath) return null;
5660
+ let version = null;
5661
+ try {
5662
+ version = execFileSync2(binary, ["--version"], {
5663
+ encoding: "utf-8",
5664
+ timeout: 3e3,
5665
+ stdio: ["ignore", "pipe", "ignore"]
5666
+ }).trim();
5667
+ } catch {
5668
+ version = null;
5669
+ }
5670
+ return { agent_provider: provider, path: resolvedPath, version };
5671
+ }
5672
+ function detectInstalledAgentProviders() {
5673
+ return AGENT_PROVIDERS.map(detectAgentProvider).filter(
5674
+ (entry) => entry !== null
5675
+ );
5676
+ }
4441
5677
  function fail6(message, code = 1) {
4442
5678
  console.error(chalk19.red(message));
4443
5679
  process.exit(code);
@@ -4508,7 +5744,7 @@ daemonCmd.command("register").description("Register this host with a central ser
4508
5744
  throw error2;
4509
5745
  }
4510
5746
  const body = {
4511
- hostname: os7.hostname(),
5747
+ hostname: os6.hostname(),
4512
5748
  platform: process.platform,
4513
5749
  name: opts.name
4514
5750
  };
@@ -4600,6 +5836,12 @@ daemonCmd.command("stop").description("Stop the autostart service via launchctl
4600
5836
  });
4601
5837
  daemonCmd.command("run").description("Run the daemon WebSocket loop in foreground (invoked by the service unit; useful for debugging)").action(async () => {
4602
5838
  const identity = loadRequiredIdentity();
5839
+ const scaffold = scaffoldDefaultAgentsIfEmpty();
5840
+ if (scaffold.scaffolded) {
5841
+ console.log(
5842
+ chalk19.green(`Scaffolded ${scaffold.written.length} default agent yml(s) under ~/.task0/agents/`)
5843
+ );
5844
+ }
4603
5845
  const wsUrl = identity.server_url.replace(/^http/, "ws").replace(/\/$/, "") + "/ws/daemon";
4604
5846
  console.log(chalk19.green(`Starting daemon ${identity.daemon_id} \u2192 ${wsUrl}`));
4605
5847
  let reconnectDelay = 1e3;
@@ -4613,18 +5855,34 @@ daemonCmd.command("run").description("Run the daemon WebSocket loop in foregroun
4613
5855
  ws.on("open", () => {
4614
5856
  reconnectDelay = 1e3;
4615
5857
  console.log(chalk19.green(`[${(/* @__PURE__ */ new Date()).toISOString()}] connected`));
5858
+ const installedAgentProviders = detectInstalledAgentProviders();
5859
+ if (installedAgentProviders.length > 0) {
5860
+ console.log(
5861
+ chalk19.dim(
5862
+ `detected agent providers: ${installedAgentProviders.map((p) => `${p.agent_provider}${p.version ? ` (${p.version})` : ""}`).join(", ")}`
5863
+ )
5864
+ );
5865
+ } else {
5866
+ console.log(chalk19.yellow("no agent providers detected on PATH (claude / codex / cursor-agent)"));
5867
+ }
4616
5868
  const hello = {
4617
5869
  type: "hello",
4618
5870
  daemon_id: identity.daemon_id,
4619
- version: DAEMON_VERSION,
5871
+ version: readCliVersion(),
4620
5872
  hostname: identity.hostname,
4621
- platform: identity.platform
5873
+ platform: identity.platform,
5874
+ installed_agent_providers: installedAgentProviders
4622
5875
  };
4623
5876
  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 };
5877
+ const manifest = buildManifest();
4626
5878
  ws.send(JSON.stringify(manifest));
4627
- console.log(chalk19.dim(`pushed manifest: ${projects.length} project(s)`));
5879
+ console.log(chalk19.dim(`pushed manifest: ${manifest.projects.length} project(s)`));
5880
+ const sink = {
5881
+ send: (frame) => {
5882
+ if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(frame));
5883
+ }
5884
+ };
5885
+ bindAgentRunFrameSink(sink);
4628
5886
  });
4629
5887
  ws.on("message", (raw) => {
4630
5888
  let msg;
@@ -4642,10 +5900,16 @@ daemonCmd.command("run").description("Run the daemon WebSocket loop in foregroun
4642
5900
  console.error(chalk19.yellow(`server error: ${msg.message}`));
4643
5901
  } else if (msg.type === "rpc_request") {
4644
5902
  void dispatchRpc(ws, msg.id, msg.method, msg.params);
5903
+ } else if (msg.type === "agent_run_resume_request") {
5904
+ const sent = replayAfterRanges(msg.ranges);
5905
+ if (sent > 0) {
5906
+ console.log(chalk19.dim(`replayed ${sent} agent-run log frame(s) for hub`));
5907
+ }
4645
5908
  }
4646
5909
  });
4647
5910
  ws.on("close", (code, reason) => {
4648
5911
  activeWs = null;
5912
+ bindAgentRunFrameSink(null);
4649
5913
  const reasonText = reason.toString("utf-8") || "no reason";
4650
5914
  console.log(chalk19.yellow(`[${(/* @__PURE__ */ new Date()).toISOString()}] disconnected (code=${code}, ${reasonText})`));
4651
5915
  if (code === 4001) {
@@ -4709,6 +5973,55 @@ daemonCmd.command("show [daemonId]").description("Show local daemon identity (no
4709
5973
  const data = await jsonGet(`${base}/api/daemons/${encodeURIComponent(daemonId)}`);
4710
5974
  console.log(JSON.stringify(data.daemon, null, 2));
4711
5975
  });
5976
+ 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) => {
5977
+ const identity = loadRequiredIdentity();
5978
+ const cliVersion = readCliVersion();
5979
+ const scope = opts.system ? "system" : "user";
5980
+ let serviceState = "unsupported";
5981
+ let unitPath2 = null;
5982
+ let serviceError = null;
5983
+ if (isPlatformSupported()) {
5984
+ try {
5985
+ const svc = getServiceManager(scope);
5986
+ serviceState = await svc.status();
5987
+ unitPath2 = svc.unitPath();
5988
+ } catch (error2) {
5989
+ serviceState = "unknown";
5990
+ serviceError = error2 instanceof Error ? error2.message : String(error2);
5991
+ }
5992
+ }
5993
+ if (opts.json) {
5994
+ console.log(JSON.stringify({
5995
+ daemon_id: identity.daemon_id,
5996
+ name: identity.name,
5997
+ hostname: identity.hostname,
5998
+ platform: identity.platform,
5999
+ server_url: identity.server_url,
6000
+ registered_at: identity.registered_at,
6001
+ cli_version: cliVersion,
6002
+ service: {
6003
+ scope,
6004
+ state: serviceState,
6005
+ unit_path: unitPath2,
6006
+ error: serviceError
6007
+ }
6008
+ }, null, 2));
6009
+ return;
6010
+ }
6011
+ const stateColor = serviceState === "running" ? chalk19.green : serviceState === "stopped" || serviceState === "absent" ? chalk19.yellow : serviceState === "errored" || serviceState === "unknown" ? chalk19.red : chalk19.dim;
6012
+ const label = (s) => chalk19.bold(s.padEnd(11));
6013
+ console.log(`${label("daemon")}${identity.daemon_id} (${identity.name})`);
6014
+ console.log(`${label("host")}${identity.hostname} \xB7 ${identity.platform}`);
6015
+ console.log(`${label("server")}${identity.server_url}`);
6016
+ console.log(`${label("cli")}v${cliVersion}`);
6017
+ console.log(`${label("registered")}${identity.registered_at}`);
6018
+ if (isPlatformSupported()) {
6019
+ console.log(`${label("service")}${stateColor(serviceState)} (${scope})${unitPath2 ? ` \u2192 ${unitPath2}` : ""}`);
6020
+ if (serviceError) console.log(chalk19.dim(` ${serviceError}`));
6021
+ } else {
6022
+ console.log(`${label("service")}${chalk19.dim(`not supported on ${process.platform}`)}`);
6023
+ }
6024
+ });
4712
6025
  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
6026
  const scope = opts.system ? "system" : "user";
4714
6027
  if (!opts.keepService) {
@@ -4735,16 +6048,15 @@ daemonCmd.command("logout").description("Stop and uninstall the autostart servic
4735
6048
  // src/commands/user.ts
4736
6049
  import { Command as Command20 } from "commander";
4737
6050
  import chalk20 from "chalk";
4738
- var DEFAULT_BASE2 = (process.env.TASK0_API_URL || "http://127.0.0.1:4318").replace(/\/$/, "");
4739
6051
  function serverBase2() {
4740
- return DEFAULT_BASE2;
6052
+ return (process.env.TASK0_API_URL || "http://127.0.0.1:4318").replace(/\/$/, "");
4741
6053
  }
4742
6054
  function fail7(message, code = 1) {
4743
6055
  console.error(chalk20.red(message));
4744
6056
  process.exit(code);
4745
6057
  }
4746
- async function callServer(path26, init = {}) {
4747
- const url = `${serverBase2()}${path26}`;
6058
+ async function callServer(path31, init = {}) {
6059
+ const url = `${serverBase2()}${path31}`;
4748
6060
  let auth;
4749
6061
  try {
4750
6062
  auth = adminAuthHeader();
@@ -5247,6 +6559,160 @@ automation.command("runs <id>").description("List recent runs for an automation"
5247
6559
  for (const run4 of automation_runs) printRun(run4);
5248
6560
  });
5249
6561
 
6562
+ // src/commands/profile.ts
6563
+ import fs30 from "fs";
6564
+ import os7 from "os";
6565
+ import path30 from "path";
6566
+ import { Command as Command23 } from "commander";
6567
+ import chalk23 from "chalk";
6568
+ var VALID_NAME = /^[a-zA-Z0-9_-]+$/;
6569
+ function fail9(msg) {
6570
+ console.error(chalk23.red(msg));
6571
+ process.exit(1);
6572
+ }
6573
+ function legacyTask0Home() {
6574
+ return path30.join(os7.homedir(), ".task0");
6575
+ }
6576
+ function readDaemonAt(home) {
6577
+ const file = path30.join(home, "daemon.json");
6578
+ if (!fs30.existsSync(file)) return null;
6579
+ try {
6580
+ return JSON.parse(fs30.readFileSync(file, "utf-8"));
6581
+ } catch {
6582
+ return null;
6583
+ }
6584
+ }
6585
+ var profile2 = new Command23("profile").description("Manage named CLI profiles (each isolates TASK0_HOME)");
6586
+ profile2.command("list").description("List configured profiles").action(() => {
6587
+ const profiles = listProfiles();
6588
+ const current = getCurrentProfileName();
6589
+ const names = Object.keys(profiles);
6590
+ if (names.length === 0) {
6591
+ console.log("No profiles configured. Add one with `task0 profile add <name> --task0-home <path>`.");
6592
+ console.log(chalk23.dim(`(currently running in legacy mode \u2014 TASK0_HOME=${process.env.TASK0_HOME ?? legacyTask0Home()})`));
6593
+ return;
6594
+ }
6595
+ for (const name of names) {
6596
+ const entry = profiles[name];
6597
+ const marker = name === current ? chalk23.green("\u25CF ") : " ";
6598
+ const url = entry.api_url ? chalk23.dim(` \u2192 ${entry.api_url}`) : "";
6599
+ console.log(`${marker}${name} ${chalk23.dim(entry.task0_home)}${url}`);
6600
+ }
6601
+ if (!current) {
6602
+ console.log(chalk23.dim("\nNo current profile selected. Use `task0 profile use <name>` to activate one."));
6603
+ }
6604
+ });
6605
+ profile2.command("add <name>").description("Register a new profile").requiredOption("--task0-home <path>", "Directory to hold this profile's daemon/admin state (will be created if missing)").option("--api-url <url>", "API server URL the CLI should call when this profile is active").action((name, opts) => {
6606
+ if (!VALID_NAME.test(name)) {
6607
+ fail9(`Invalid profile name "${name}". Must match ${VALID_NAME}.`);
6608
+ }
6609
+ const existing = getProfile(name);
6610
+ if (existing) {
6611
+ fail9(`Profile "${name}" already exists.`);
6612
+ }
6613
+ const absHome = path30.resolve(opts.task0Home);
6614
+ const profiles = listProfiles();
6615
+ for (const [otherName, entry2] of Object.entries(profiles)) {
6616
+ if (path30.resolve(entry2.task0_home) === absHome) {
6617
+ fail9(`Profile "${otherName}" already uses task0_home "${absHome}". Two profiles cannot share a task0_home (service labels collide).`);
6618
+ }
6619
+ }
6620
+ const parent = path30.dirname(absHome);
6621
+ if (!fs30.existsSync(parent)) {
6622
+ fail9(`Parent directory does not exist: ${parent}`);
6623
+ }
6624
+ try {
6625
+ fs30.accessSync(parent, fs30.constants.W_OK);
6626
+ } catch {
6627
+ fail9(`Parent directory is not writable: ${parent}`);
6628
+ }
6629
+ const isFirstAdd = Object.keys(profiles).length === 0;
6630
+ if (isFirstAdd && name !== "default") {
6631
+ const legacyHome = legacyTask0Home();
6632
+ const legacy = readDaemonAt(legacyHome);
6633
+ if (legacy) {
6634
+ addProfile("default", {
6635
+ task0_home: legacyHome,
6636
+ ...legacy.server_url ? { api_url: legacy.server_url } : {}
6637
+ });
6638
+ console.log(chalk23.dim(`Auto-imported existing ~/.task0 state as profile "default" (use \`task0 profile use default\` to keep using it).`));
6639
+ }
6640
+ }
6641
+ const entry = {
6642
+ task0_home: absHome,
6643
+ ...opts.apiUrl ? { api_url: opts.apiUrl } : {}
6644
+ };
6645
+ addProfile(name, entry);
6646
+ fs30.mkdirSync(absHome, { recursive: true });
6647
+ const adopted = readDaemonAt(absHome);
6648
+ if (adopted) {
6649
+ console.log(chalk23.yellow(`warn: ${absHome} already contains daemon state. Profile "${name}" will adopt it.`));
6650
+ }
6651
+ console.log(chalk23.green(`Added profile "${name}"`));
6652
+ console.log(` task0_home: ${absHome}`);
6653
+ if (entry.api_url) console.log(` api_url: ${entry.api_url}`);
6654
+ console.log(chalk23.dim(`Use \`task0 profile use ${name}\` to activate.`));
6655
+ });
6656
+ profile2.command("remove <name>").description("Remove a profile entry (does not delete the task0_home directory)").option("-f, --force", "Allow removing the current profile (clears current_profile)").action((name, opts) => {
6657
+ const entry = getProfile(name);
6658
+ if (!entry) {
6659
+ fail9(`Profile "${name}" not found.`);
6660
+ }
6661
+ const current = getCurrentProfileName();
6662
+ if (current === name && !opts.force) {
6663
+ fail9(`Profile "${name}" is current. Re-run with --force to remove it (this clears current_profile).`);
6664
+ }
6665
+ removeProfile(name);
6666
+ console.log(chalk23.green(`Removed profile "${name}".`));
6667
+ if (current === name) {
6668
+ console.log(chalk23.dim("current_profile cleared. Use `task0 profile use <name>` to pick another."));
6669
+ }
6670
+ console.log(chalk23.dim(`Note: ${entry.task0_home} was not deleted.`));
6671
+ });
6672
+ profile2.command("use <name>").description("Set the current profile").action((name) => {
6673
+ const entry = getProfile(name);
6674
+ if (!entry) {
6675
+ const names = Object.keys(listProfiles());
6676
+ fail9(`Profile "${name}" not found. Available: ${names.length > 0 ? names.join(", ") : "(none)"}.`);
6677
+ }
6678
+ setCurrentProfile(name);
6679
+ console.log(chalk23.green(`Now using profile "${name}".`));
6680
+ });
6681
+ profile2.command("current").description("Print the current profile name (exits non-zero if none)").action(() => {
6682
+ const current = getCurrentProfileName();
6683
+ if (!current) {
6684
+ console.error(chalk23.dim("No current profile. Set one with `task0 profile use <name>`."));
6685
+ process.exit(1);
6686
+ }
6687
+ console.log(current);
6688
+ });
6689
+ profile2.command("show [name]").description("Show a profile's configuration and detect drift vs daemon.json").action((name) => {
6690
+ const target = name ?? getCurrentProfileName();
6691
+ if (!target) {
6692
+ fail9("No profile name given and no current_profile set. Pass a name or run `task0 profile use <name>` first.");
6693
+ }
6694
+ const entry = getProfile(target);
6695
+ if (!entry) {
6696
+ fail9(`Profile "${target}" not found.`);
6697
+ }
6698
+ const current = getCurrentProfileName();
6699
+ console.log(`${chalk23.bold(target)}${current === target ? chalk23.green(" (current)") : ""}`);
6700
+ console.log(` task0_home: ${entry.task0_home}`);
6701
+ console.log(` api_url: ${entry.api_url ?? chalk23.dim("(unset)")}`);
6702
+ console.log(` config: ${configFilePath()}`);
6703
+ const identity = readDaemonAt(entry.task0_home);
6704
+ if (!identity) {
6705
+ console.log(chalk23.dim(" daemon.json: (not registered yet)"));
6706
+ return;
6707
+ }
6708
+ console.log(` daemon_id: ${identity.daemon_id ?? chalk23.dim("(missing)")}`);
6709
+ console.log(` daemon.server_url: ${identity.server_url ?? chalk23.dim("(missing)")}`);
6710
+ if (entry.api_url && identity.server_url && entry.api_url !== identity.server_url) {
6711
+ console.log(chalk23.yellow(` warn: profile.api_url and daemon.json.server_url disagree.`));
6712
+ console.log(chalk23.dim(` Re-register the daemon to align: task0 --profile ${target} daemon register --server ${entry.api_url}`));
6713
+ }
6714
+ });
6715
+
5250
6716
  // src/core/error-capture.ts
5251
6717
  init_node();
5252
6718
  var DEFAULT_KEEP = 50;
@@ -5344,17 +6810,8 @@ function captureTopLevel(err, options) {
5344
6810
  }
5345
6811
 
5346
6812
  // 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
- }
5357
- var program = new Command23().name("task0").description("Task-centric control layer for agent workflow").version(TASK0_VERSION);
6813
+ var TASK0_VERSION = readCliVersion();
6814
+ var program = new Command24().name("task0").description("Task-centric control layer for agent workflow").version(TASK0_VERSION).option("--profile <name>", "Use a named profile from ~/.config/task0/config.yml (overrides current_profile and TASK0_HOME)");
5358
6815
  program.addCommand(source);
5359
6816
  program.addCommand(project);
5360
6817
  program.addCommand(task);
@@ -5372,7 +6829,17 @@ program.addCommand(daemonCmd);
5372
6829
  program.addCommand(userCmd);
5373
6830
  program.addCommand(error);
5374
6831
  program.addCommand(automation);
6832
+ program.addCommand(profile2);
5375
6833
  async function main() {
6834
+ try {
6835
+ activateProfile(process.argv);
6836
+ } catch (err) {
6837
+ if (err instanceof ProfileNotFoundError) {
6838
+ console.error(chalk24.red(err.message));
6839
+ process.exit(1);
6840
+ }
6841
+ throw err;
6842
+ }
5376
6843
  installErrorCapture({ version: TASK0_VERSION });
5377
6844
  try {
5378
6845
  await program.parseAsync(process.argv);