@inteeka/task-cli 0.2.16 → 0.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -247,7 +247,7 @@ import open from "open";
247
247
  import ora from "ora";
248
248
 
249
249
  // src/config/credentials.ts
250
- import { mkdir, readFile, writeFile, unlink, chmod, stat } from "fs/promises";
250
+ import { mkdir, readFile, writeFile, unlink, chmod, stat, rename } from "fs/promises";
251
251
  import { homedir } from "os";
252
252
  import { dirname, join } from "path";
253
253
  var CONFIG_DIR = join(homedir(), ".config", "task");
@@ -289,8 +289,10 @@ async function readCredentials() {
289
289
  }
290
290
  async function writeCredentials(creds) {
291
291
  await ensureDir(dirname(CREDENTIALS_PATH));
292
- await writeFile(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), { mode: 384 });
293
- await chmod(CREDENTIALS_PATH, 384);
292
+ const tmpPath = `${CREDENTIALS_PATH}.${process.pid}.tmp`;
293
+ await writeFile(tmpPath, JSON.stringify(creds, null, 2), { mode: 384 });
294
+ await chmod(tmpPath, 384);
295
+ await rename(tmpPath, CREDENTIALS_PATH);
294
296
  }
295
297
  async function clearCredentials() {
296
298
  try {
@@ -299,6 +301,7 @@ async function clearCredentials() {
299
301
  if (err.code !== "ENOENT") throw err;
300
302
  }
301
303
  }
304
+ var CREDENTIALS_DIR = CONFIG_DIR;
302
305
 
303
306
  // src/util/host.ts
304
307
  import { createHash } from "crypto";
@@ -449,13 +452,34 @@ function sleep(ms) {
449
452
 
450
453
  // src/api/client.ts
451
454
  import { request as request3 } from "undici";
452
- import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
455
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
453
456
  import { homedir as homedir2 } from "os";
454
- import { join as join2 } from "path";
457
+ import { join as join3 } from "path";
455
458
 
456
459
  // src/auth/refresh.ts
457
460
  import { request as request2 } from "undici";
461
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
462
+ import { join as join2 } from "path";
463
+ import { lock } from "proper-lockfile";
458
464
  var REFRESH_LEEWAY_MS = 6e4;
465
+ var REFRESH_LOCK_PATH = join2(CREDENTIALS_DIR, ".refresh.lock");
466
+ async function ensureLockFileExists() {
467
+ await mkdir2(CREDENTIALS_DIR, { recursive: true, mode: 448 });
468
+ await writeFile2(REFRESH_LOCK_PATH, "", { mode: 384, flag: "a" });
469
+ }
470
+ async function withRefreshLock(fn) {
471
+ await ensureLockFileExists();
472
+ const release = await lock(REFRESH_LOCK_PATH, {
473
+ retries: { retries: 50, minTimeout: 100, maxTimeout: 1e3, factor: 1.5 },
474
+ stale: 3e4,
475
+ update: 1e4
476
+ });
477
+ try {
478
+ return await fn();
479
+ } finally {
480
+ await release();
481
+ }
482
+ }
459
483
  async function ensureFreshAccessToken(creds) {
460
484
  const expiresMs = new Date(creds.access_expires_at).getTime();
461
485
  if (Number.isFinite(expiresMs) && expiresMs - Date.now() > REFRESH_LEEWAY_MS) {
@@ -464,6 +488,18 @@ async function ensureFreshAccessToken(creds) {
464
488
  return performRefresh(creds);
465
489
  }
466
490
  async function performRefresh(creds) {
491
+ return withRefreshLock(async () => {
492
+ const onDisk = await readCredentials();
493
+ if (onDisk) {
494
+ const expiresMs = new Date(onDisk.access_expires_at).getTime();
495
+ if (Number.isFinite(expiresMs) && expiresMs - Date.now() > REFRESH_LEEWAY_MS) {
496
+ return onDisk;
497
+ }
498
+ }
499
+ return refreshHttp(onDisk ?? creds);
500
+ });
501
+ }
502
+ async function refreshHttp(creds, retriesLeft = 1) {
467
503
  const { hostId } = getHostInfo();
468
504
  const apiUrl = creds.api_url.replace(/\/$/, "");
469
505
  const res = await request2(`${apiUrl}/api/v1/cli/auth/refresh`, {
@@ -481,6 +517,16 @@ async function performRefresh(creds) {
481
517
  headersTimeout: 15e3
482
518
  });
483
519
  if (res.statusCode === 401 || res.statusCode === 403) {
520
+ const onDisk = await readCredentials();
521
+ if (onDisk && onDisk.refresh_token !== creds.refresh_token) {
522
+ const expiresMs = new Date(onDisk.access_expires_at).getTime();
523
+ if (Number.isFinite(expiresMs) && expiresMs - Date.now() > REFRESH_LEEWAY_MS) {
524
+ return onDisk;
525
+ }
526
+ if (retriesLeft > 0) {
527
+ return refreshHttp(onDisk, retriesLeft - 1);
528
+ }
529
+ }
484
530
  await clearCredentials();
485
531
  throw new CliError(
486
532
  CLI_EXIT_CODES.UNAUTHORISED,
@@ -518,13 +564,13 @@ async function manualRefresh() {
518
564
  // src/api/client.ts
519
565
  async function dumpServerError(method, path, status, rawBody, headers) {
520
566
  try {
521
- const dir = join2(homedir2(), ".cache", "task", "api-debug");
522
- await mkdir2(dir, { recursive: true });
523
- const file = join2(dir, `${Date.now()}-${status}.log`);
567
+ const dir = join3(homedir2(), ".cache", "task", "api-debug");
568
+ await mkdir3(dir, { recursive: true });
569
+ const file = join3(dir, `${Date.now()}-${status}.log`);
524
570
  const safeHeaders = { ...headers };
525
571
  delete safeHeaders["Authorization"];
526
572
  delete safeHeaders["authorization"];
527
- await writeFile2(
573
+ await writeFile3(
528
574
  file,
529
575
  [
530
576
  `## ${method} ${path}`,
@@ -666,10 +712,10 @@ async function apiCallOrThrow(method, path, options = {}) {
666
712
  }
667
713
 
668
714
  // src/config/local-config.ts
669
- import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
715
+ import { mkdir as mkdir4, readFile as readFile2, writeFile as writeFile4 } from "fs/promises";
670
716
  import { homedir as homedir3 } from "os";
671
- import { dirname as dirname2, join as join3 } from "path";
672
- var CONFIG_PATH = join3(homedir3(), ".config", "task", "config.json");
717
+ import { dirname as dirname2, join as join4 } from "path";
718
+ var CONFIG_PATH = join4(homedir3(), ".config", "task", "config.json");
673
719
  var DEFAULT_CONFIG = {
674
720
  api_url: process.env["TASK_API_URL"] ?? "http://localhost:3400",
675
721
  default_project: null,
@@ -689,8 +735,8 @@ async function readLocalConfig() {
689
735
  }
690
736
  }
691
737
  async function writeLocalConfig(config) {
692
- await mkdir3(dirname2(CONFIG_PATH), { recursive: true });
693
- await writeFile3(CONFIG_PATH, JSON.stringify(config, null, 2));
738
+ await mkdir4(dirname2(CONFIG_PATH), { recursive: true });
739
+ await writeFile4(CONFIG_PATH, JSON.stringify(config, null, 2));
694
740
  }
695
741
  async function setConfigValue(key, value) {
696
742
  const cfg = await readLocalConfig();
@@ -809,14 +855,14 @@ function registerAuthRefresh(program2) {
809
855
  }
810
856
 
811
857
  // src/commands/link.ts
812
- import { readFile as readFile4, writeFile as writeFile5, appendFile, access } from "fs/promises";
858
+ import { readFile as readFile4, writeFile as writeFile6, appendFile, access } from "fs/promises";
813
859
  import { constants as fsConstants } from "fs";
814
- import { join as join5 } from "path";
860
+ import { join as join6 } from "path";
815
861
  import inquirer from "inquirer";
816
862
 
817
863
  // src/config/project.ts
818
- import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile4, unlink as unlink2 } from "fs/promises";
819
- import { dirname as dirname3, join as join4, resolve } from "path";
864
+ import { mkdir as mkdir5, readFile as readFile3, writeFile as writeFile5, unlink as unlink2 } from "fs/promises";
865
+ import { dirname as dirname3, join as join5, resolve } from "path";
820
866
  import { execSync } from "child_process";
821
867
  function findRepoRoot(start = process.cwd()) {
822
868
  try {
@@ -827,7 +873,7 @@ function findRepoRoot(start = process.cwd()) {
827
873
  }
828
874
  }
829
875
  function configPath(repoRoot) {
830
- return join4(repoRoot ?? findRepoRoot(), ".task", "config.json");
876
+ return join5(repoRoot ?? findRepoRoot(), ".task", "config.json");
831
877
  }
832
878
  async function readProjectConfig(repoRoot) {
833
879
  const path = configPath(repoRoot);
@@ -841,8 +887,8 @@ async function readProjectConfig(repoRoot) {
841
887
  }
842
888
  async function writeProjectConfig(config, repoRoot) {
843
889
  const path = configPath(repoRoot);
844
- await mkdir4(dirname3(path), { recursive: true });
845
- await writeFile4(path, JSON.stringify(config, null, 2));
890
+ await mkdir5(dirname3(path), { recursive: true });
891
+ await writeFile5(path, JSON.stringify(config, null, 2));
846
892
  }
847
893
  async function clearProjectConfig(repoRoot) {
848
894
  const path = configPath(repoRoot);
@@ -941,7 +987,7 @@ async function resolveProject(projects, opts) {
941
987
  return picked;
942
988
  }
943
989
  async function ensureGitignored(repoRoot) {
944
- const gitignorePath = join5(repoRoot, ".gitignore");
990
+ const gitignorePath = join6(repoRoot, ".gitignore");
945
991
  let existing = null;
946
992
  try {
947
993
  await access(gitignorePath, fsConstants.F_OK);
@@ -956,7 +1002,7 @@ async function ensureGitignored(repoRoot) {
956
1002
  await appendFile(gitignorePath, block);
957
1003
  return "added";
958
1004
  }
959
- await writeFile5(gitignorePath, "# task CLI link config\n.task/\n");
1005
+ await writeFile6(gitignorePath, "# task CLI link config\n.task/\n");
960
1006
  return "created";
961
1007
  }
962
1008
 
@@ -1468,9 +1514,9 @@ import inquirer2 from "inquirer";
1468
1514
 
1469
1515
  // src/agent/agent-service.ts
1470
1516
  import { spawn } from "child_process";
1471
- import { mkdir as mkdir5, writeFile as writeFile6 } from "fs/promises";
1517
+ import { mkdir as mkdir6, writeFile as writeFile7 } from "fs/promises";
1472
1518
  import { homedir as homedir4 } from "os";
1473
- import { join as join6 } from "path";
1519
+ import { join as join7 } from "path";
1474
1520
 
1475
1521
  // src/agent/allowed-tools.ts
1476
1522
  var ALLOWED_TOOLS = CLI_ALLOWED_TOOLS;
@@ -1527,10 +1573,10 @@ async function runAgent(args) {
1527
1573
  let outputLogPath = null;
1528
1574
  let logHandle = null;
1529
1575
  if (args.silent) {
1530
- const dir = join6(homedir4(), ".cache", "task", "runs");
1531
- await mkdir5(dir, { recursive: true });
1532
- outputLogPath = join6(dir, `${args.runId}.log`);
1533
- await writeFile6(outputLogPath, "");
1576
+ const dir = join7(homedir4(), ".cache", "task", "runs");
1577
+ await mkdir6(dir, { recursive: true });
1578
+ outputLogPath = join7(dir, `${args.runId}.log`);
1579
+ await writeFile7(outputLogPath, "");
1534
1580
  const { createWriteStream } = await import("fs");
1535
1581
  logHandle = createWriteStream(outputLogPath, { flags: "a" });
1536
1582
  }
@@ -1731,11 +1777,111 @@ async function runProjectTest(args) {
1731
1777
  });
1732
1778
  }
1733
1779
 
1780
+ // src/test-runner/auto-install.ts
1781
+ import { spawn as spawn3 } from "child_process";
1782
+ import { stat as stat2 } from "fs/promises";
1783
+ import { dirname as dirname4, join as join8 } from "path";
1784
+ var INSTALL_TIMEOUT_MS = 10 * 60 * 1e3;
1785
+ var TAIL_BYTES2 = 4e3;
1786
+ async function findPnpmWorkspaceRoot(start) {
1787
+ let cur = start;
1788
+ for (; ; ) {
1789
+ try {
1790
+ await stat2(join8(cur, "pnpm-lock.yaml"));
1791
+ return cur;
1792
+ } catch {
1793
+ }
1794
+ const parent = dirname4(cur);
1795
+ if (parent === cur) return null;
1796
+ cur = parent;
1797
+ }
1798
+ }
1799
+ async function mtimeMs(path) {
1800
+ try {
1801
+ return (await stat2(path)).mtimeMs;
1802
+ } catch {
1803
+ return null;
1804
+ }
1805
+ }
1806
+ async function ensureWorkspaceInstalled(args) {
1807
+ const start = Date.now();
1808
+ const workspaceRoot = await findPnpmWorkspaceRoot(args.cwd);
1809
+ if (!workspaceRoot) {
1810
+ return {
1811
+ ok: true,
1812
+ skipped: true,
1813
+ reason: "no pnpm-lock.yaml on the path \u2014 not a pnpm workspace",
1814
+ workspaceRoot: null,
1815
+ durationMs: Date.now() - start,
1816
+ exitCode: null,
1817
+ tail: ""
1818
+ };
1819
+ }
1820
+ const lockMtime = await mtimeMs(join8(workspaceRoot, "pnpm-lock.yaml"));
1821
+ const markerMtime = await mtimeMs(join8(workspaceRoot, "node_modules", ".modules.yaml"));
1822
+ if (lockMtime !== null && markerMtime !== null && markerMtime >= lockMtime) {
1823
+ return {
1824
+ ok: true,
1825
+ skipped: true,
1826
+ reason: "node_modules in sync with pnpm-lock.yaml",
1827
+ workspaceRoot,
1828
+ durationMs: Date.now() - start,
1829
+ exitCode: null,
1830
+ tail: ""
1831
+ };
1832
+ }
1833
+ const reason = markerMtime === null ? "node_modules missing \u2014 cold install" : "pnpm-lock.yaml newer than install marker";
1834
+ return new Promise((resolve2) => {
1835
+ const child = spawn3("pnpm", ["install", "--frozen-lockfile"], {
1836
+ cwd: workspaceRoot,
1837
+ stdio: ["ignore", "pipe", "pipe"],
1838
+ shell: false,
1839
+ env: { ...process.env, CI: "1" },
1840
+ ...args.signal ? { signal: args.signal } : {}
1841
+ });
1842
+ let buf = "";
1843
+ const append = (chunk) => {
1844
+ buf += chunk.toString("utf8");
1845
+ if (buf.length > TAIL_BYTES2 * 2) buf = buf.slice(-TAIL_BYTES2);
1846
+ };
1847
+ child.stdout?.on("data", append);
1848
+ child.stderr?.on("data", append);
1849
+ const timeoutHandle = setTimeout(() => {
1850
+ child.kill("SIGKILL");
1851
+ }, INSTALL_TIMEOUT_MS);
1852
+ child.on("close", (code) => {
1853
+ clearTimeout(timeoutHandle);
1854
+ resolve2({
1855
+ ok: code === 0,
1856
+ skipped: false,
1857
+ reason,
1858
+ workspaceRoot,
1859
+ durationMs: Date.now() - start,
1860
+ exitCode: code,
1861
+ tail: buf.slice(-TAIL_BYTES2)
1862
+ });
1863
+ });
1864
+ child.on("error", (err) => {
1865
+ clearTimeout(timeoutHandle);
1866
+ const msg = err.code === "ENOENT" ? "`pnpm` not on PATH" : err.message;
1867
+ resolve2({
1868
+ ok: false,
1869
+ skipped: false,
1870
+ reason: `${reason} \u2014 spawn error: ${msg}`,
1871
+ workspaceRoot,
1872
+ durationMs: Date.now() - start,
1873
+ exitCode: null,
1874
+ tail: buf.slice(-TAIL_BYTES2)
1875
+ });
1876
+ });
1877
+ });
1878
+ }
1879
+
1734
1880
  // src/util/progress.ts
1735
- import { mkdir as mkdir6, writeFile as writeFile7, rename, unlink as unlink3, readdir, stat as stat2 } from "fs/promises";
1881
+ import { mkdir as mkdir7, writeFile as writeFile8, rename as rename2, unlink as unlink3, readdir, stat as stat3 } from "fs/promises";
1736
1882
  import { tmpdir } from "os";
1737
- import { join as join7 } from "path";
1738
- var PROGRESS_DIR = join7(tmpdir(), "task-progress");
1883
+ import { join as join9 } from "path";
1884
+ var PROGRESS_DIR = join9(tmpdir(), "task-progress");
1739
1885
  var STALE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
1740
1886
  var ProgressWriter = class {
1741
1887
  path;
@@ -1748,7 +1894,7 @@ var ProgressWriter = class {
1748
1894
  this.ticketId = ticketId;
1749
1895
  const deliveryId = process.env["TASK_DELIVERY_ID"]?.trim();
1750
1896
  const fileBase = deliveryId && deliveryId.length > 0 ? deliveryId : `manual-${process.pid}`;
1751
- this.path = join7(PROGRESS_DIR, `${fileBase}.json`);
1897
+ this.path = join9(PROGRESS_DIR, `${fileBase}.json`);
1752
1898
  }
1753
1899
  /** Switch the in-flight ticket between phases (used by `task scan` which iterates). */
1754
1900
  setTicketId(ticketId) {
@@ -1782,13 +1928,13 @@ var ProgressWriter = class {
1782
1928
  });
1783
1929
  }
1784
1930
  async writeAtomic(payload) {
1785
- await mkdir6(PROGRESS_DIR, { recursive: true }).catch(() => {
1931
+ await mkdir7(PROGRESS_DIR, { recursive: true }).catch(() => {
1786
1932
  });
1787
1933
  const body = JSON.stringify(payload);
1788
1934
  const tmp = `${this.path}.tmp`;
1789
1935
  try {
1790
- await writeFile7(tmp, body, { encoding: "utf8", mode: 384 });
1791
- await rename(tmp, this.path);
1936
+ await writeFile8(tmp, body, { encoding: "utf8", mode: 384 });
1937
+ await rename2(tmp, this.path);
1792
1938
  } catch {
1793
1939
  await unlink3(tmp).catch(() => {
1794
1940
  });
@@ -1804,9 +1950,9 @@ var ProgressWriter = class {
1804
1950
  const cutoff = Date.now() - STALE_MAX_AGE_MS;
1805
1951
  await Promise.all(
1806
1952
  entries.filter((name) => name.endsWith(".json") || name.endsWith(".json.tmp")).map(async (name) => {
1807
- const p = join7(PROGRESS_DIR, name);
1953
+ const p = join9(PROGRESS_DIR, name);
1808
1954
  try {
1809
- const s = await stat2(p);
1955
+ const s = await stat3(p);
1810
1956
  if (s.mtimeMs < cutoff) {
1811
1957
  await unlink3(p).catch(() => {
1812
1958
  });
@@ -2170,6 +2316,50 @@ ${detail.ai_fix_approval_notes}` : ""
2170
2316
  await progress.setPhase("testing", {
2171
2317
  detail: testCommand ?? "pnpm typecheck"
2172
2318
  });
2319
+ const installResult = await ensureWorkspaceInstalled({ cwd });
2320
+ if (!installResult.ok) {
2321
+ discardWorkingTreeChanges(cwd);
2322
+ try {
2323
+ checkoutBranch(cwd, baseBranch);
2324
+ } catch {
2325
+ }
2326
+ deleteLocalBranch(cwd, branchName);
2327
+ await apiCall("POST", "/api/v1/cli/me/runs", {
2328
+ body: {
2329
+ ticket_id: detail.id,
2330
+ schedule_id: opts.scheduleId,
2331
+ event: "tests_failed",
2332
+ claude_session_id: runId,
2333
+ duration_ms: installResult.durationMs,
2334
+ output_excerpt: `pnpm install --frozen-lockfile failed (${installResult.reason})
2335
+ ${installResult.tail}`.slice(
2336
+ 0,
2337
+ 4e3
2338
+ )
2339
+ }
2340
+ });
2341
+ if (!silent) {
2342
+ process.stdout.write(
2343
+ `${c.err("\u2717 pnpm install --frozen-lockfile failed")} (exit ${installResult.exitCode}) \u2014 branch deleted, no push.
2344
+ `
2345
+ );
2346
+ if (installResult.tail.trim().length > 0) {
2347
+ process.stdout.write(c.dim(installResult.tail.slice(-1e3) + "\n"));
2348
+ }
2349
+ }
2350
+ throw new CliError(
2351
+ CLI_EXIT_CODES.GENERIC_ERROR,
2352
+ `pnpm install --frozen-lockfile failed (exit ${installResult.exitCode}) \u2014 ${installResult.reason}`
2353
+ );
2354
+ }
2355
+ if (!installResult.skipped && !silent) {
2356
+ process.stdout.write(
2357
+ c.dim(
2358
+ ` pnpm install --frozen-lockfile (${installResult.reason}) \u2014 ${installResult.durationMs}ms
2359
+ `
2360
+ )
2361
+ );
2362
+ }
2173
2363
  if (!silent)
2174
2364
  process.stdout.write(c.dim(` running pre-push test: ${testCommand ?? "pnpm typecheck"}
2175
2365
  `));
@@ -3050,14 +3240,17 @@ async function jsonRequest(url, init) {
3050
3240
  };
3051
3241
  }
3052
3242
  var AutopilotApi = class {
3243
+ creds;
3053
3244
  constructor(opts) {
3054
- this.opts = opts;
3245
+ this.creds = opts.creds;
3246
+ this.apiUrl = opts.apiUrl;
3055
3247
  }
3056
- adminHeaders() {
3248
+ apiUrl;
3249
+ async userHeaders() {
3250
+ this.creds = await ensureFreshAccessToken(this.creds);
3057
3251
  return {
3058
3252
  "Content-Type": "application/json",
3059
- Authorization: `Bearer ${this.opts.apiKey}`,
3060
- "X-Actor-Email": this.opts.actorEmail,
3253
+ Authorization: `Bearer ${this.creds.access_token}`,
3061
3254
  "User-Agent": "task-cli/scan"
3062
3255
  };
3063
3256
  }
@@ -3070,12 +3263,13 @@ var AutopilotApi = class {
3070
3263
  };
3071
3264
  }
3072
3265
  async listEligibleProjects() {
3073
- const url = `${this.opts.apiUrl}/api/v1/cli/projects`;
3266
+ const url = `${this.apiUrl}/api/v1/cli/projects`;
3074
3267
  const result = await jsonRequest(url, {
3075
3268
  method: "GET",
3076
- headers: this.adminHeaders()
3269
+ headers: await this.userHeaders()
3077
3270
  });
3078
3271
  if (!result.ok) {
3272
+ await handleUserAuthFailure(result.code, result.status);
3079
3273
  throw new CliError(
3080
3274
  autopilotExitCode(result.code, result.status),
3081
3275
  `${result.code}: ${result.message}`
@@ -3084,10 +3278,10 @@ var AutopilotApi = class {
3084
3278
  return result.data ?? [];
3085
3279
  }
3086
3280
  async issueSkillToken(args) {
3087
- const url = `${this.opts.apiUrl}/api/v1/cli/issue-skill-token`;
3281
+ const url = `${this.apiUrl}/api/v1/cli/issue-skill-token`;
3088
3282
  const result = await jsonRequest(url, {
3089
3283
  method: "POST",
3090
- headers: this.adminHeaders(),
3284
+ headers: await this.userHeaders(),
3091
3285
  body: {
3092
3286
  project_id: args.project_id,
3093
3287
  scope: "fix_prompt_sync",
@@ -3096,6 +3290,7 @@ var AutopilotApi = class {
3096
3290
  }
3097
3291
  });
3098
3292
  if (!result.ok) {
3293
+ await handleUserAuthFailure(result.code, result.status);
3099
3294
  throw new CliError(
3100
3295
  autopilotExitCode(result.code, result.status),
3101
3296
  `${result.code}: ${result.message}`
@@ -3104,7 +3299,7 @@ var AutopilotApi = class {
3104
3299
  return result.data;
3105
3300
  }
3106
3301
  async prepare(skillToken, batchSize, idempotencyKey) {
3107
- const url = `${this.opts.apiUrl}/api/v1/cli/fix-prompt-sync/prepare`;
3302
+ const url = `${this.apiUrl}/api/v1/cli/fix-prompt-sync/prepare`;
3108
3303
  const result = await jsonRequest(url, {
3109
3304
  method: "POST",
3110
3305
  headers: this.skillHeaders(skillToken, { "Idempotency-Key": idempotencyKey }),
@@ -3122,7 +3317,7 @@ var AutopilotApi = class {
3122
3317
  return result.data;
3123
3318
  }
3124
3319
  async submit(args) {
3125
- const url = `${this.opts.apiUrl}/api/v1/cli/fix-prompt-sync/submit`;
3320
+ const url = `${this.apiUrl}/api/v1/cli/fix-prompt-sync/submit`;
3126
3321
  const result = await jsonRequest(url, {
3127
3322
  method: "POST",
3128
3323
  headers: this.skillHeaders(args.skillToken, { "X-Prepare-Nonce": args.nonce }),
@@ -3148,7 +3343,7 @@ var AutopilotApi = class {
3148
3343
  }
3149
3344
  async abort(skillToken, ticketIds) {
3150
3345
  if (ticketIds.length === 0) return;
3151
- const url = `${this.opts.apiUrl}/api/v1/cli/fix-prompt-sync/abort`;
3346
+ const url = `${this.apiUrl}/api/v1/cli/fix-prompt-sync/abort`;
3152
3347
  await jsonRequest(url, {
3153
3348
  method: "POST",
3154
3349
  headers: this.skillHeaders(skillToken),
@@ -3156,7 +3351,7 @@ var AutopilotApi = class {
3156
3351
  }).catch(() => void 0);
3157
3352
  }
3158
3353
  async runSummary(skillToken, summary) {
3159
- const url = `${this.opts.apiUrl}/api/v1/cli/fix-prompt-sync/run-summary`;
3354
+ const url = `${this.apiUrl}/api/v1/cli/fix-prompt-sync/run-summary`;
3160
3355
  await jsonRequest(url, {
3161
3356
  method: "POST",
3162
3357
  headers: this.skillHeaders(skillToken),
@@ -3164,6 +3359,24 @@ var AutopilotApi = class {
3164
3359
  }).catch(() => void 0);
3165
3360
  }
3166
3361
  };
3362
+ async function handleUserAuthFailure(code, status) {
3363
+ if (status === 401 && (code === "UNAUTHORIZED" || code === "TOKEN_EXPIRED")) {
3364
+ await clearCredentials();
3365
+ throw new CliError(
3366
+ CLI_EXIT_CODES.UNAUTHORISED,
3367
+ "Your CLI session is no longer valid",
3368
+ "Run 'task login' to authenticate again."
3369
+ );
3370
+ }
3371
+ if (status === 403 && code === "CLI_ACCESS_REVOKED") {
3372
+ await clearCredentials();
3373
+ throw new CliError(
3374
+ CLI_EXIT_CODES.UNAUTHORISED,
3375
+ "CLI access has been revoked",
3376
+ "Ask a project admin to re-grant access from the Agentic CLI page."
3377
+ );
3378
+ }
3379
+ }
3167
3380
  function autopilotExitCode(code, status) {
3168
3381
  if (status === 401 || status === 403) return CLI_EXIT_CODES.UNAUTHORISED;
3169
3382
  if (code === "TIER_LIMIT_EXCEEDED" || code === "NO_GIT_INTEGRATION") {
@@ -3174,10 +3387,10 @@ function autopilotExitCode(code, status) {
3174
3387
  }
3175
3388
 
3176
3389
  // src/scan/llm.ts
3177
- import { spawn as spawn3 } from "child_process";
3178
- import { mkdir as mkdir7, writeFile as writeFile8 } from "fs/promises";
3390
+ import { spawn as spawn4 } from "child_process";
3391
+ import { mkdir as mkdir8, writeFile as writeFile9 } from "fs/promises";
3179
3392
  import { homedir as homedir5 } from "os";
3180
- import { join as join8 } from "path";
3393
+ import { join as join10 } from "path";
3181
3394
  var FIX_PROMPT_JSON_SCHEMA = {
3182
3395
  type: "object",
3183
3396
  // Phase 3 — confidence_reason is REQUIRED unconditionally so the
@@ -3266,7 +3479,7 @@ async function generateFixPromptJson(args) {
3266
3479
  return new Promise((resolve2, reject) => {
3267
3480
  let child;
3268
3481
  try {
3269
- child = spawn3(claude, cliArgs, {
3482
+ child = spawn4(claude, cliArgs, {
3270
3483
  stdio: ["pipe", "pipe", "pipe"],
3271
3484
  signal: args.signal
3272
3485
  });
@@ -3392,10 +3605,10 @@ function readEnvelopeTokens(raw, userPrompt, innerText) {
3392
3605
  async function maybeDumpDebug(ticketId, stdout, stderr) {
3393
3606
  if (!DEBUG && stdout.length === 0 && stderr.length === 0) return null;
3394
3607
  try {
3395
- const dir = join8(homedir5(), ".cache", "task", "scan-debug");
3396
- await mkdir7(dir, { recursive: true });
3397
- const path = join8(dir, `${ticketId}-${Date.now()}.log`);
3398
- await writeFile8(
3608
+ const dir = join10(homedir5(), ".cache", "task", "scan-debug");
3609
+ await mkdir8(dir, { recursive: true });
3610
+ const path = join10(dir, `${ticketId}-${Date.now()}.log`);
3611
+ await writeFile9(
3399
3612
  path,
3400
3613
  ["## ticket_id", ticketId, "", "## stdout", stdout, "", "## stderr", stderr].join("\n")
3401
3614
  );
@@ -3467,7 +3680,7 @@ function registerScan(program2) {
3467
3680
  "Restrict to one project (default: the linked project from .task/config.json, falling back to every visible project if the repo is not linked)"
3468
3681
  ).option(
3469
3682
  "--all-projects",
3470
- "Override the linked-project default and scan every CLI-eligible project the admin token can see"
3683
+ "Override the linked-project default and scan every CLI-eligible project the signed-in user has cli_access on"
3471
3684
  ).option("--max <n>", "Max submissions per project token", "50").option("--batch <n>", "Tickets per /prepare batch (1-10)", "5").option("--api-url <url>", "Override TASK_API_URL").option("--silent", "Suppress per-ticket progress chrome").action(async (opts) => {
3472
3685
  await runScan(opts);
3473
3686
  });
@@ -3489,25 +3702,18 @@ async function runScan(opts) {
3489
3702
  }
3490
3703
  }
3491
3704
  async function runScanImpl(opts, progress) {
3492
- const apiKey = process.env["TASK_API_KEY"];
3493
- if (!apiKey || apiKey.length < 32) {
3494
- throw new CliError(
3495
- CLI_EXIT_CODES.MISCONFIGURATION,
3496
- "TASK_API_KEY is missing or shorter than 32 chars",
3497
- "Set TASK_API_KEY in your environment. The autopilot loop authenticates with the shared admin secret, not the per-user CLI bearer."
3498
- );
3499
- }
3500
- const actorEmail = process.env["TASK_API_KEY_OWNER_EMAIL"];
3501
- if (!actorEmail || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(actorEmail)) {
3705
+ let creds = await readCredentials();
3706
+ if (!creds) {
3502
3707
  throw new CliError(
3503
3708
  CLI_EXIT_CODES.MISCONFIGURATION,
3504
- "TASK_API_KEY_OWNER_EMAIL is not set or not a valid email",
3505
- "Set TASK_API_KEY_OWNER_EMAIL=<you@example.com>. The server records this on every audit row."
3709
+ "Not signed in",
3710
+ "Run 'task login' to authenticate. The scan autopilot uses the same per-user OAuth bearer as every other CLI command."
3506
3711
  );
3507
3712
  }
3713
+ creds = await ensureFreshAccessToken(creds);
3508
3714
  const localCfg = await readLocalConfig();
3509
3715
  const linkedProject = await readProjectConfig(findRepoRoot());
3510
- const apiUrl = (opts.apiUrl ?? process.env["TASK_API_URL"] ?? localCfg.api_url ?? linkedProject?.api_url ?? "http://localhost:3400").replace(/\/$/, "");
3716
+ const apiUrl = (opts.apiUrl ?? process.env["TASK_API_URL"] ?? creds.api_url ?? localCfg.api_url ?? linkedProject?.api_url ?? "http://localhost:3400").replace(/\/$/, "");
3511
3717
  const max = clampInt(opts.max, 1, 500, 50);
3512
3718
  const batchSize = clampInt(opts.batch, 1, 10, 5);
3513
3719
  const silent = !!opts.silent || localCfg.silent;
@@ -3526,7 +3732,7 @@ async function runScanImpl(opts, progress) {
3526
3732
  projectFilter = linkedProject.project_id;
3527
3733
  filterSource = "link";
3528
3734
  }
3529
- const api = new AutopilotApi({ apiUrl, apiKey, actorEmail });
3735
+ const api = new AutopilotApi({ apiUrl, creds });
3530
3736
  if (!silent) process.stdout.write(`${c.dim("Discovering eligible projects\u2026")}
3531
3737
  `);
3532
3738
  const all = await api.listEligibleProjects();
@@ -3542,7 +3748,7 @@ async function runScanImpl(opts, progress) {
3542
3748
  throw new CliError(
3543
3749
  CLI_EXIT_CODES.GENERIC_ERROR,
3544
3750
  `Linked project ${linkedProject.organisation_slug}/${linkedProject.project_slug} has no CLI-eligible tickets right now`,
3545
- "Mark a ticket as CLI-eligible from the dashboard, or pass --all-projects to scan everything the admin token can see."
3751
+ "Mark a ticket as CLI-eligible from the dashboard, or pass --all-projects to scan every project you have cli_access on."
3546
3752
  );
3547
3753
  }
3548
3754
  throw new CliError(
@@ -3742,6 +3948,252 @@ function clampInt(raw, min, max, fallback) {
3742
3948
  return Math.min(v, max);
3743
3949
  }
3744
3950
 
3951
+ // src/commands/fast-track.ts
3952
+ import { randomUUID as randomUUID3 } from "crypto";
3953
+ import ora3 from "ora";
3954
+ function registerFastTrack(program2) {
3955
+ program2.command("fast-track").description(
3956
+ "End-to-end: scan + auto-approve + work on the next CLI-eligible ticket(s) in the linked project \u2014 no admin review step"
3957
+ ).option("--max <n>", "Process up to N tickets in this invocation", "1").option("--api-url <url>", "Override TASK_API_URL").option("--silent", "Suppress per-ticket progress chrome").option("--dry-run", "Run scan + approve + agent + tests but do not commit, push, or open a PR").option(
3958
+ "--reset",
3959
+ "DESTRUCTIVE: discard local working-tree changes before the first ticket. Requires --confirm in non-TTY contexts."
3960
+ ).option("--confirm", "Confirm --reset in non-TTY (silent / scheduled-task) contexts").option("--schedule-id <id>", "Internal: schedule id when invoked from a scheduled task").action(async (opts) => {
3961
+ await runFastTrack(opts);
3962
+ });
3963
+ }
3964
+ async function runFastTrack(opts) {
3965
+ let creds = await readCredentials();
3966
+ if (!creds) {
3967
+ throw new CliError(
3968
+ CLI_EXIT_CODES.MISCONFIGURATION,
3969
+ "Not signed in",
3970
+ "Run 'task login' to authenticate."
3971
+ );
3972
+ }
3973
+ creds = await ensureFreshAccessToken(creds);
3974
+ const localCfg = await readLocalConfig();
3975
+ const linkedProject = await readProjectConfig(findRepoRoot());
3976
+ if (!linkedProject) {
3977
+ throw new CliError(
3978
+ CLI_EXIT_CODES.MISCONFIGURATION,
3979
+ "No project link in this repo",
3980
+ "Run 'task link' first \u2014 fast-track operates on the linked project only."
3981
+ );
3982
+ }
3983
+ const apiUrl = (opts.apiUrl ?? process.env["TASK_API_URL"] ?? creds.api_url ?? localCfg.api_url ?? linkedProject.api_url ?? "http://localhost:3400").replace(/\/$/, "");
3984
+ const max = Math.max(1, parseInt(opts.max, 10) || 1);
3985
+ const silent = !!opts.silent || localCfg.silent;
3986
+ const ctx = await buildWorkContext({ max: "1", silent: opts.silent });
3987
+ const api = new AutopilotApi({ apiUrl, creds });
3988
+ const claudePath = localCfg.claude_path ?? void 0;
3989
+ let firstIteration = true;
3990
+ const results = [];
3991
+ for (let i = 0; i < max; i++) {
3992
+ const innerWorkOpts = {
3993
+ auto: false,
3994
+ // We pin a specific ticketId per iteration.
3995
+ next: false,
3996
+ max: "1",
3997
+ silent: opts.silent,
3998
+ ...opts.dryRun ? { dryRun: true } : {},
3999
+ ...opts.scheduleId ? { scheduleId: opts.scheduleId } : {},
4000
+ ...firstIteration && opts.reset ? { reset: true } : {},
4001
+ ...firstIteration && opts.confirm ? { confirm: true } : {}
4002
+ };
4003
+ const outcome = await fastTrackOneTicket({
4004
+ api,
4005
+ apiUrl,
4006
+ project: linkedProject,
4007
+ ctx,
4008
+ claudePath,
4009
+ silent,
4010
+ innerWorkOpts,
4011
+ scheduleId: opts.scheduleId
4012
+ });
4013
+ if (outcome.kind === "no_eligible") {
4014
+ if (results.length === 0 && !silent) {
4015
+ process.stdout.write(c.dim("No CLI-eligible tickets available to fast-track.\n"));
4016
+ }
4017
+ break;
4018
+ }
4019
+ results.push(outcome.result);
4020
+ firstIteration = false;
4021
+ if (i < max - 1 && outcome.result.status === "completed") {
4022
+ enforceBaseBranchClean(ctx.cwd, ctx.baseBranch);
4023
+ }
4024
+ }
4025
+ if (!silent) {
4026
+ summarise(results);
4027
+ }
4028
+ }
4029
+ async function fastTrackOneTicket(args) {
4030
+ const { api, project, ctx, claudePath, silent, innerWorkOpts, scheduleId } = args;
4031
+ const issued = await api.issueSkillToken({
4032
+ project_id: project.project_id,
4033
+ max_submits: 1
4034
+ });
4035
+ const skillToken = issued.token;
4036
+ const prepared = await api.prepare(skillToken, 1, randomUUID3());
4037
+ if (prepared.tickets.length === 0) {
4038
+ return { kind: "no_eligible" };
4039
+ }
4040
+ const ticket = prepared.tickets[0];
4041
+ const nonce = prepared.prepare_nonce;
4042
+ const spinner = silent ? null : ora3(`#${ticket.sequence_number} ${ticket.title.slice(0, 60)} \u2014 scanning`).start();
4043
+ let generated;
4044
+ try {
4045
+ generated = await generateFixPromptJson({
4046
+ systemPrompt: ticket.system_prompt,
4047
+ repoOverviewBlock: ticket.repo_overview_block,
4048
+ ticketBlock: ticket.ticket_block,
4049
+ outputSchemaHint: ticket.output_schema_hint,
4050
+ modelId: ticket.model_id,
4051
+ ticketId: ticket.ticket_id,
4052
+ ...claudePath ? { claudePath } : {}
4053
+ });
4054
+ } catch (err) {
4055
+ await api.abort(skillToken, [ticket.ticket_id]).catch(() => void 0);
4056
+ const reason = err instanceof LlmGenerationError ? `${err.reason}: ${err.message.slice(0, 200)}` : "";
4057
+ spinner?.fail(`#${ticket.sequence_number} scan failed${reason ? ` (${reason})` : ""}`);
4058
+ throw err instanceof CliError ? err : new CliError(
4059
+ CLI_EXIT_CODES.GENERIC_ERROR,
4060
+ `Fix-prompt generation failed for #${ticket.sequence_number}: ${err.message.slice(0, 200)}`
4061
+ );
4062
+ }
4063
+ const submitted = await api.submit({
4064
+ skillToken,
4065
+ nonce,
4066
+ ticketId: ticket.ticket_id,
4067
+ structured: generated.structured,
4068
+ inputTokens: generated.inputTokens,
4069
+ outputTokens: generated.outputTokens,
4070
+ model: ticket.model_id
4071
+ });
4072
+ if (submitted.status === "skip") {
4073
+ spinner?.warn(`#${ticket.sequence_number} server rejected submission (${submitted.reason})`);
4074
+ return {
4075
+ kind: "processed",
4076
+ result: {
4077
+ sequenceNumber: ticket.sequence_number,
4078
+ ticketId: ticket.ticket_id,
4079
+ previousFixStatus: submitted.reason,
4080
+ status: "scan_skipped",
4081
+ error: submitted.reason
4082
+ }
4083
+ };
4084
+ }
4085
+ const denylistHit = submitted.status === "needs_review";
4086
+ if (spinner) {
4087
+ spinner.text = `#${ticket.sequence_number} approving${denylistHit ? " (denylist override)" : ""}`;
4088
+ }
4089
+ let approved;
4090
+ try {
4091
+ approved = await apiCallOrThrow(
4092
+ "POST",
4093
+ `/api/v1/cli/me/tickets/${ticket.ticket_id}/ai-fix-prompt/fast-approve`,
4094
+ {
4095
+ body: {
4096
+ ...denylistHit ? { denylist_acknowledged: true } : {},
4097
+ reason: `fast-track: ${denylistHit ? "denylist override" : "auto-approve"} by CLI session`
4098
+ }
4099
+ }
4100
+ );
4101
+ } catch (err) {
4102
+ spinner?.fail(
4103
+ `#${ticket.sequence_number} fast-approve failed: ${err.message.slice(0, 120)}`
4104
+ );
4105
+ throw err;
4106
+ }
4107
+ if (spinner) {
4108
+ spinner.text = `#${ticket.sequence_number} building`;
4109
+ }
4110
+ const innerOpts = { ...innerWorkOpts };
4111
+ if (scheduleId !== void 0) innerOpts.scheduleId = scheduleId;
4112
+ try {
4113
+ const outcome = await processOneTicket(ctx, innerOpts, ticket.ticket_id);
4114
+ if (outcome.kind === "no_eligible") {
4115
+ spinner?.warn(`#${ticket.sequence_number} unexpectedly invisible to work after approve`);
4116
+ return {
4117
+ kind: "processed",
4118
+ result: {
4119
+ sequenceNumber: ticket.sequence_number,
4120
+ ticketId: ticket.ticket_id,
4121
+ previousFixStatus: approved.from_status ?? "unknown",
4122
+ status: "failed",
4123
+ error: "ticket vanished between approve and claim"
4124
+ }
4125
+ };
4126
+ }
4127
+ if (outcome.kind === "completed") {
4128
+ spinner?.succeed(
4129
+ `#${ticket.sequence_number} PR opened${outcome.prUrl ? ` ${outcome.prUrl}` : ""}`
4130
+ );
4131
+ const result = {
4132
+ sequenceNumber: outcome.sequenceNumber,
4133
+ ticketId: ticket.ticket_id,
4134
+ previousFixStatus: approved.from_status ?? "unknown",
4135
+ status: "completed"
4136
+ };
4137
+ if (outcome.prNumber !== void 0) result.prNumber = outcome.prNumber;
4138
+ if (outcome.prUrl !== void 0) result.prUrl = outcome.prUrl;
4139
+ return { kind: "processed", result };
4140
+ }
4141
+ if (outcome.kind === "dry_run") {
4142
+ spinner?.succeed(`#${ticket.sequence_number} dry-run (no PR)`);
4143
+ return {
4144
+ kind: "processed",
4145
+ result: {
4146
+ sequenceNumber: outcome.sequenceNumber,
4147
+ ticketId: ticket.ticket_id,
4148
+ previousFixStatus: approved.from_status ?? "unknown",
4149
+ status: "dry_run"
4150
+ }
4151
+ };
4152
+ }
4153
+ spinner?.warn(`#${ticket.sequence_number} agent produced no changes`);
4154
+ return {
4155
+ kind: "processed",
4156
+ result: {
4157
+ sequenceNumber: outcome.sequenceNumber,
4158
+ ticketId: ticket.ticket_id,
4159
+ previousFixStatus: approved.from_status ?? "unknown",
4160
+ status: "no_changes"
4161
+ }
4162
+ };
4163
+ } catch (err) {
4164
+ spinner?.fail(
4165
+ `#${ticket.sequence_number} work failed: ${err.message.slice(0, 120)}`
4166
+ );
4167
+ throw err;
4168
+ }
4169
+ }
4170
+ function summarise(results) {
4171
+ if (results.length === 0) return;
4172
+ const counts = results.reduce(
4173
+ (acc, r) => {
4174
+ acc[r.status] = (acc[r.status] ?? 0) + 1;
4175
+ return acc;
4176
+ },
4177
+ {}
4178
+ );
4179
+ const parts = [];
4180
+ if (counts.completed) parts.push(`${c.ok(String(counts.completed))} PR(s) opened`);
4181
+ if (counts.dry_run) parts.push(`${c.dim(String(counts.dry_run))} dry-run`);
4182
+ if (counts.no_changes) parts.push(`${c.dim(String(counts.no_changes))} no-changes`);
4183
+ if (counts.scan_skipped) parts.push(`${c.warn(String(counts.scan_skipped))} scan skipped`);
4184
+ if (counts.failed) parts.push(`${c.err(String(counts.failed))} failed`);
4185
+ process.stdout.write(`
4186
+ ${c.bold("Fast-track summary")}: ${parts.join(", ")}
4187
+ `);
4188
+ const denylistOverrides = results.filter((r) => r.previousFixStatus === "needs_review").length;
4189
+ if (denylistOverrides > 0) {
4190
+ process.stdout.write(
4191
+ `${c.warn("\u26A0")} ${denylistOverrides} ticket(s) bypassed the denylist gate \u2014 review the PR diff(s) carefully.
4192
+ `
4193
+ );
4194
+ }
4195
+ }
4196
+
3745
4197
  // src/commands/pr-test.ts
3746
4198
  import { execFileSync as execFileSync8 } from "child_process";
3747
4199
  function registerPrTest(program2) {
@@ -3880,16 +4332,16 @@ ${c.err("\u2717 pr-test failed")}: ${err.message}
3880
4332
  }
3881
4333
 
3882
4334
  // src/commands/scheduled-task.ts
3883
- import { randomUUID as randomUUID3 } from "crypto";
4335
+ import { randomUUID as randomUUID4 } from "crypto";
3884
4336
 
3885
4337
  // src/scheduler/index.ts
3886
4338
  import { platform as platform2 } from "os";
3887
4339
 
3888
4340
  // src/scheduler/launchd.ts
3889
- import { mkdir as mkdir8, readFile as readFile5, writeFile as writeFile9, unlink as unlink4, readdir as readdir2 } from "fs/promises";
4341
+ import { mkdir as mkdir9, readFile as readFile5, writeFile as writeFile10, unlink as unlink4, readdir as readdir2 } from "fs/promises";
3890
4342
  import { homedir as homedir6 } from "os";
3891
- import { join as join9 } from "path";
3892
- import { execFileSync as execFileSync9, spawn as spawn4 } from "child_process";
4343
+ import { join as join11 } from "path";
4344
+ import { execFileSync as execFileSync9, spawn as spawn5 } from "child_process";
3893
4345
 
3894
4346
  // src/scheduler/cron-translate.ts
3895
4347
  function translateToLaunchd(cron) {
@@ -3990,14 +4442,14 @@ function expandField(field, min, max) {
3990
4442
  }
3991
4443
 
3992
4444
  // src/scheduler/launchd.ts
3993
- var PLIST_DIR = join9(homedir6(), "Library", "LaunchAgents");
4445
+ var PLIST_DIR = join11(homedir6(), "Library", "LaunchAgents");
3994
4446
  var LABEL_PREFIX = "com.inteeka.task.cli.";
3995
4447
  var SAFE_ID_RE = /^[0-9a-zA-Z._-]+$/;
3996
4448
  function plistPath(id) {
3997
4449
  if (!SAFE_ID_RE.test(id) || id.includes("..")) {
3998
4450
  throw new Error(`Refusing to compute plist path for unsafe id: ${id}`);
3999
4451
  }
4000
- return join9(PLIST_DIR, `${LABEL_PREFIX}${id}.plist`);
4452
+ return join11(PLIST_DIR, `${LABEL_PREFIX}${id}.plist`);
4001
4453
  }
4002
4454
  function buildPlist(entry) {
4003
4455
  const calendars = translateToLaunchd(entry.cron);
@@ -4033,9 +4485,9 @@ ${fields}
4033
4485
  ` <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>`,
4034
4486
  ` </dict>`,
4035
4487
  ` <key>StandardOutPath</key>`,
4036
- ` <string>${escapeXml(join9(homedir6(), ".cache", "task", "launchd-stdout.log"))}</string>`,
4488
+ ` <string>${escapeXml(join11(homedir6(), ".cache", "task", "launchd-stdout.log"))}</string>`,
4037
4489
  ` <key>StandardErrorPath</key>`,
4038
- ` <string>${escapeXml(join9(homedir6(), ".cache", "task", "launchd-stderr.log"))}</string>`,
4490
+ ` <string>${escapeXml(join11(homedir6(), ".cache", "task", "launchd-stderr.log"))}</string>`,
4039
4491
  !entry.enabled ? ` <key>Disabled</key>
4040
4492
  <true/>` : "",
4041
4493
  "</dict>",
@@ -4053,9 +4505,9 @@ function bootstrapDomain() {
4053
4505
  }
4054
4506
  var launchdAdapter = {
4055
4507
  async upsert(entry) {
4056
- await mkdir8(PLIST_DIR, { recursive: true });
4508
+ await mkdir9(PLIST_DIR, { recursive: true });
4057
4509
  const path = plistPath(entry.id);
4058
- await writeFile9(path, buildPlist(entry));
4510
+ await writeFile10(path, buildPlist(entry));
4059
4511
  try {
4060
4512
  execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
4061
4513
  } catch {
@@ -4084,7 +4536,7 @@ var launchdAdapter = {
4084
4536
  for (const file of ours) {
4085
4537
  const id = file.slice(LABEL_PREFIX.length, -".plist".length);
4086
4538
  try {
4087
- const xml = await readFile5(join9(PLIST_DIR, file), "utf8");
4539
+ const xml = await readFile5(join11(PLIST_DIR, file), "utf8");
4088
4540
  const cron = xml.match(/<key>StartCalendarInterval<\/key>[\s\S]*?<\/array>/)?.[0] ?? "";
4089
4541
  const command = xml.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/)?.[1] ?? "";
4090
4542
  const disabled = /<key>Disabled<\/key>\s*<true\/>/.test(xml);
@@ -4107,7 +4559,7 @@ var launchdAdapter = {
4107
4559
  return new Promise((resolve2) => {
4108
4560
  const args = entry.command.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [entry.command];
4109
4561
  const cmd = args.shift() ?? entry.command;
4110
- const child = spawn4(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
4562
+ const child = spawn5(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
4111
4563
  let stdoutTail = "";
4112
4564
  let stderrTail = "";
4113
4565
  child.stdout?.on("data", (chunk) => {
@@ -4130,7 +4582,7 @@ var launchdAdapter = {
4130
4582
  }
4131
4583
  if (enabled) {
4132
4584
  xml = xml.replace(/\s*<key>Disabled<\/key>\s*<true\/>/, "");
4133
- await writeFile9(path, xml);
4585
+ await writeFile10(path, xml);
4134
4586
  try {
4135
4587
  execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
4136
4588
  } catch {
@@ -4142,7 +4594,7 @@ var launchdAdapter = {
4142
4594
  "</dict>\n</plist>",
4143
4595
  " <key>Disabled</key>\n <true/>\n</dict>\n</plist>"
4144
4596
  );
4145
- await writeFile9(path, xml);
4597
+ await writeFile10(path, xml);
4146
4598
  }
4147
4599
  try {
4148
4600
  execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
@@ -4153,7 +4605,7 @@ var launchdAdapter = {
4153
4605
  };
4154
4606
 
4155
4607
  // src/scheduler/cron.ts
4156
- import { execFileSync as execFileSync10, spawn as spawn5 } from "child_process";
4608
+ import { execFileSync as execFileSync10, spawn as spawn6 } from "child_process";
4157
4609
 
4158
4610
  // src/scheduler/safe-command.ts
4159
4611
  var FORBIDDEN = /[;&|`$()<>\\]/;
@@ -4214,7 +4666,7 @@ function readCrontab() {
4214
4666
  }
4215
4667
  }
4216
4668
  function writeCrontab(text) {
4217
- const child = spawn5("crontab", ["-"], { stdio: ["pipe", "inherit", "inherit"] });
4669
+ const child = spawn6("crontab", ["-"], { stdio: ["pipe", "inherit", "inherit"] });
4218
4670
  child.stdin.write(text);
4219
4671
  child.stdin.end();
4220
4672
  }
@@ -4295,7 +4747,7 @@ var cronAdapter = {
4295
4747
  return Promise.resolve({ exitCode: 1, stdoutTail: "", stderrTail: `rejected: ${reason}` });
4296
4748
  }
4297
4749
  return new Promise((resolve2) => {
4298
- const child = spawn5(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
4750
+ const child = spawn6(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
4299
4751
  let stdoutTail = "";
4300
4752
  let stderrTail = "";
4301
4753
  child.stdout?.on(
@@ -4323,7 +4775,7 @@ var cronAdapter = {
4323
4775
  };
4324
4776
 
4325
4777
  // src/scheduler/windows.ts
4326
- import { execFileSync as execFileSync11, spawn as spawn6 } from "child_process";
4778
+ import { execFileSync as execFileSync11, spawn as spawn7 } from "child_process";
4327
4779
  var TASK_PREFIX = "TaskCLI_";
4328
4780
  function taskName(id) {
4329
4781
  return `${TASK_PREFIX}${id.replace(/[^A-Za-z0-9_-]/g, "_")}`;
@@ -4436,7 +4888,7 @@ var windowsAdapter = {
4436
4888
  return Promise.resolve({ exitCode: 1, stdoutTail: "", stderrTail: `rejected: ${reason}` });
4437
4889
  }
4438
4890
  return new Promise((resolve2) => {
4439
- const child = spawn6(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
4891
+ const child = spawn7(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
4440
4892
  let stdoutTail = "";
4441
4893
  let stderrTail = "";
4442
4894
  child.stdout?.on(
@@ -4495,10 +4947,10 @@ var unsupportedAdapter = {
4495
4947
  };
4496
4948
 
4497
4949
  // src/scheduler/registry.ts
4498
- import { mkdir as mkdir9, readFile as readFile6, writeFile as writeFile10 } from "fs/promises";
4950
+ import { mkdir as mkdir10, readFile as readFile6, writeFile as writeFile11 } from "fs/promises";
4499
4951
  import { homedir as homedir7 } from "os";
4500
- import { dirname as dirname4, join as join10 } from "path";
4501
- var REGISTRY_PATH = join10(homedir7(), ".config", "task", "schedules.json");
4952
+ import { dirname as dirname5, join as join12 } from "path";
4953
+ var REGISTRY_PATH = join12(homedir7(), ".config", "task", "schedules.json");
4502
4954
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4503
4955
  function looksLikeRegistryRow(value) {
4504
4956
  if (!value || typeof value !== "object") return false;
@@ -4518,8 +4970,8 @@ async function readRegistry() {
4518
4970
  }
4519
4971
  }
4520
4972
  async function writeRegistry(rows) {
4521
- await mkdir9(dirname4(REGISTRY_PATH), { recursive: true });
4522
- await writeFile10(REGISTRY_PATH, JSON.stringify(rows, null, 2));
4973
+ await mkdir10(dirname5(REGISTRY_PATH), { recursive: true });
4974
+ await writeFile11(REGISTRY_PATH, JSON.stringify(rows, null, 2));
4523
4975
  }
4524
4976
  async function upsertRegistry(row) {
4525
4977
  if (!UUID_RE.test(row.id)) {
@@ -4607,7 +5059,7 @@ function registerScheduledTask(program2) {
4607
5059
  const max = Math.min(100, Math.max(1, parseInt(opts.max, 10) || 5));
4608
5060
  const command = opts.command ?? `task work --auto --silent --max ${max}`;
4609
5061
  const { hostId, hostLabel } = getHostInfo();
4610
- const id = randomUUID3();
5062
+ const id = randomUUID4();
4611
5063
  const created = await apiCall("POST", "/api/v1/cli/schedules", {
4612
5064
  body: {
4613
5065
  name,
@@ -4760,7 +5212,7 @@ function stripAnsi(s) {
4760
5212
  // src/commands/runs.ts
4761
5213
  import { readFile as readFile7 } from "fs/promises";
4762
5214
  import { homedir as homedir8 } from "os";
4763
- import { join as join11 } from "path";
5215
+ import { join as join13 } from "path";
4764
5216
  function registerRuns(program2) {
4765
5217
  const cmd = program2.command("runs").description("Inspect agentic CLI run history");
4766
5218
  cmd.command("list").description("List recent runs").option("--limit <n>", "Max rows", "50").option("--ticket <id>", "Filter by ticket").option("--schedule <id>", "Filter by schedule").action(async (opts) => {
@@ -4789,7 +5241,7 @@ function registerRuns(program2) {
4789
5241
  process.stdout.write(JSON.stringify(row, null, 2) + "\n");
4790
5242
  });
4791
5243
  cmd.command("logs <id>").description("Show captured agent output for a run, if available").action(async (id) => {
4792
- const localPath = join11(homedir8(), ".cache", "task", "runs", `${id}.log`);
5244
+ const localPath = join13(homedir8(), ".cache", "task", "runs", `${id}.log`);
4793
5245
  try {
4794
5246
  const text = await readFile7(localPath, "utf8");
4795
5247
  process.stdout.write(text);
@@ -4862,32 +5314,43 @@ function registerConfig(program2) {
4862
5314
 
4863
5315
  // src/commands/doctor.ts
4864
5316
  import { execFileSync as execFileSync12 } from "child_process";
4865
- import { readFile as readFile8, writeFile as writeFile11 } from "fs/promises";
4866
- import { join as join12 } from "path";
5317
+ import { readFile as readFile8, writeFile as writeFile12 } from "fs/promises";
5318
+ import { join as join14 } from "path";
4867
5319
  import { request as request5 } from "undici";
4868
5320
  var ALLOWED_TEST_EXECUTABLES = /* @__PURE__ */ new Set(["pnpm", "npm", "yarn", "bun", "node", "npx"]);
4869
5321
  var DEFAULT_TEST_COMMAND = "pnpm typecheck";
4870
5322
  function registerDoctor(program2) {
4871
- program2.command("doctor").description("Diagnose your CLI setup").option("--fix", "attempt to auto-remediate fixable problems (e.g. add a typecheck script)").action(async (opts) => {
5323
+ program2.command("doctor").description(
5324
+ "Diagnose your CLI setup \u2014 Identity (who you are signed in as) first, then Setup checks"
5325
+ ).option("--fix", "attempt to auto-remediate fixable problems (e.g. add a typecheck script)").action(async (opts) => {
4872
5326
  const checks = [];
4873
5327
  const creds = await readCredentials();
5328
+ let accessLite = null;
5329
+ if (creds) {
5330
+ accessLite = await fetchAccessLite();
5331
+ }
5332
+ const authDetail = creds ? renderAuthDetail(creds.email, creds.access_expires_at, accessLite) : "not signed in \u2014 run 'task login'";
4874
5333
  checks.push({
5334
+ group: "identity",
4875
5335
  name: "auth",
4876
- ok: !!creds,
4877
- detail: creds ? `signed in as ${creds.email ?? "(unknown)"}, expires ${creds.access_expires_at}` : "not signed in \u2014 run 'task login'"
5336
+ ok: !!creds && (accessLite?.has_access ?? true),
5337
+ detail: authDetail,
5338
+ remediation: !creds ? "run 'task login' to authenticate" : accessLite && !accessLite.has_access ? "Your account has no project with cli_access \u2014 ask an admin to grant access on the Agentic CLI page." : void 0
4878
5339
  });
4879
5340
  const root = findRepoRoot();
4880
5341
  const project = await readProjectConfig(root);
4881
5342
  checks.push({
5343
+ group: "identity",
4882
5344
  name: "project link",
4883
5345
  ok: !!project,
4884
5346
  detail: project ? `${project.organisation_slug}/${project.project_slug}` : "no link \u2014 run 'task link'"
4885
5347
  });
4886
5348
  const cfg = await readLocalConfig();
4887
- checks.push(checkBinary("claude", cfg.claude_path ?? "claude"));
4888
- checks.push(checkBinary("git", "git"));
5349
+ checks.push({ group: "setup", ...checkBinary("claude", cfg.claude_path ?? "claude") });
5350
+ checks.push({ group: "setup", ...checkBinary("git", "git") });
4889
5351
  const { kind } = getSchedulerAdapter();
4890
5352
  checks.push({
5353
+ group: "setup",
4891
5354
  name: "scheduler",
4892
5355
  ok: kind !== "unsupported",
4893
5356
  detail: kind === "unsupported" ? "unsupported platform" : kind
@@ -4901,12 +5364,14 @@ function registerDoctor(program2) {
4901
5364
  });
4902
5365
  await res.body.dump();
4903
5366
  checks.push({
5367
+ group: "setup",
4904
5368
  name: "api reachable",
4905
5369
  ok: true,
4906
5370
  detail: `${apiUrl} (HTTP ${res.statusCode})`
4907
5371
  });
4908
5372
  } catch (err) {
4909
5373
  checks.push({
5374
+ group: "setup",
4910
5375
  name: "api reachable",
4911
5376
  ok: false,
4912
5377
  detail: `${apiUrl}: ${err.message}`
@@ -4917,6 +5382,7 @@ function registerDoctor(program2) {
4917
5382
  try {
4918
5383
  const exists = remoteBranchExists(root, baseBranch);
4919
5384
  checks.push({
5385
+ group: "setup",
4920
5386
  name: "base branch on origin",
4921
5387
  ok: exists,
4922
5388
  detail: exists ? `origin/${baseBranch} reachable` : `origin has no branch "${baseBranch}"`,
@@ -4924,6 +5390,7 @@ function registerDoctor(program2) {
4924
5390
  });
4925
5391
  } catch (err) {
4926
5392
  checks.push({
5393
+ group: "setup",
4927
5394
  name: "base branch on origin",
4928
5395
  ok: false,
4929
5396
  detail: err instanceof CliError ? err.message : `could not check origin: ${err.message}`,
@@ -4937,41 +5404,57 @@ function registerDoctor(program2) {
4937
5404
  encoding: "utf8"
4938
5405
  }).trim();
4939
5406
  checks.push({
5407
+ group: "setup",
4940
5408
  name: "working tree",
4941
5409
  ok: dirty.length === 0,
4942
5410
  detail: dirty.length === 0 ? "clean" : "has uncommitted changes"
4943
5411
  });
4944
5412
  } catch {
4945
- checks.push({ name: "working tree", ok: false, detail: "not in a git repo" });
5413
+ checks.push({
5414
+ group: "setup",
5415
+ name: "working tree",
5416
+ ok: false,
5417
+ detail: "not in a git repo"
5418
+ });
4946
5419
  }
4947
5420
  const testCheck = await checkPrePushTest(
4948
5421
  root,
4949
5422
  project?.cli_test_command ?? null,
4950
5423
  opts.fix === true
4951
5424
  );
4952
- checks.push(testCheck);
5425
+ checks.push({ group: "setup", ...testCheck });
4953
5426
  let inFlight = [];
4954
5427
  if (creds && project) {
4955
5428
  inFlight = await listInFlightTickets(project.project_id, root);
4956
5429
  checks.push({
5430
+ group: "setup",
4957
5431
  name: "in-flight tickets",
4958
5432
  ok: true,
4959
5433
  detail: inFlight.length === 0 ? "none" : `${inFlight.length} ticket(s) waiting to be resumed`
4960
5434
  });
4961
5435
  }
4962
5436
  let allOk = true;
4963
- for (const check of checks) {
4964
- const sym = check.ok ? c.ok("\u2713") : c.err("\u2717");
4965
- process.stdout.write(`${sym} ${check.name.padEnd(20)} ${c.dim(check.detail)}
5437
+ const renderGroup = (label, group) => {
5438
+ const groupChecks = checks.filter((ch) => ch.group === group);
5439
+ if (groupChecks.length === 0) return;
5440
+ process.stdout.write(`${c.bold(label)}
5441
+ `);
5442
+ for (const check of groupChecks) {
5443
+ const sym = check.ok ? c.ok("\u2713") : c.err("\u2717");
5444
+ process.stdout.write(`${sym} ${check.name.padEnd(20)} ${c.dim(check.detail)}
4966
5445
  `);
4967
- if (!check.ok) {
4968
- allOk = false;
4969
- if (check.remediation) {
4970
- process.stdout.write(` ${c.dim("\u2192 " + check.remediation)}
5446
+ if (!check.ok) {
5447
+ allOk = false;
5448
+ if (check.remediation) {
5449
+ process.stdout.write(` ${c.dim("\u2192 " + check.remediation)}
4971
5450
  `);
5451
+ }
4972
5452
  }
4973
5453
  }
4974
- }
5454
+ };
5455
+ renderGroup("Identity", "identity");
5456
+ process.stdout.write("\n");
5457
+ renderGroup("Setup", "setup");
4975
5458
  for (const t of inFlight) {
4976
5459
  const status = t.branchPresent ? c.ok("local branch present") : c.err("local branch missing");
4977
5460
  process.stdout.write(
@@ -4982,6 +5465,24 @@ function registerDoctor(program2) {
4982
5465
  if (!allOk) process.exit(1);
4983
5466
  });
4984
5467
  }
5468
+ async function fetchAccessLite() {
5469
+ try {
5470
+ return await apiCallOrThrow("GET", "/api/v1/cli/access");
5471
+ } catch {
5472
+ return null;
5473
+ }
5474
+ }
5475
+ function renderAuthDetail(email, expiresAt, access2) {
5476
+ const who = `signed in as ${email ?? "(unknown)"}`;
5477
+ const expiry = `expires ${expiresAt}`;
5478
+ if (!access2) return `${who}, ${expiry}`;
5479
+ if (access2.projects.length === 0) {
5480
+ return `${who}, ${expiry}, no cli_access projects`;
5481
+ }
5482
+ const sample = access2.projects.slice(0, 3).map((p) => `${p.organisation_slug}/${p.slug}`).join(", ");
5483
+ const more = access2.projects.length > 3 ? `, +${access2.projects.length - 3} more` : "";
5484
+ return `${who}, ${expiry}, ${access2.projects.length} project${access2.projects.length === 1 ? "" : "s"} (${sample}${more})`;
5485
+ }
4985
5486
  async function listInFlightTickets(projectId, cwd) {
4986
5487
  const result = await apiCall(
4987
5488
  "GET",
@@ -5028,7 +5529,7 @@ async function checkPrePushTest(root, configuredCommand, fix) {
5028
5529
  detail: `${command} (non-script executable, not statically verifiable)`
5029
5530
  };
5030
5531
  }
5031
- const pkgPath = join12(root, "package.json");
5532
+ const pkgPath = join14(root, "package.json");
5032
5533
  let pkgRaw;
5033
5534
  try {
5034
5535
  pkgRaw = await readFile8(pkgPath, "utf8");
@@ -5064,7 +5565,7 @@ async function checkPrePushTest(root, configuredCommand, fix) {
5064
5565
  pkg.scripts = { ...scripts, typecheck: "tsc --noEmit" };
5065
5566
  const indent = detectIndent(pkgRaw);
5066
5567
  const trailingNewline = pkgRaw.endsWith("\n") ? "\n" : "";
5067
- await writeFile11(pkgPath, JSON.stringify(pkg, null, indent) + trailingNewline);
5568
+ await writeFile12(pkgPath, JSON.stringify(pkg, null, indent) + trailingNewline);
5068
5569
  return {
5069
5570
  name: "pre-push test",
5070
5571
  ok: true,
@@ -5113,7 +5614,7 @@ function checkBinary(name, command) {
5113
5614
  }
5114
5615
 
5115
5616
  // src/commands/version.ts
5116
- var CLI_VERSION = true ? "0.2.16" : "0.0.0-dev";
5617
+ var CLI_VERSION = true ? "0.2.18" : "0.0.0-dev";
5117
5618
  function registerVersion(program2) {
5118
5619
  program2.command("version").description("Print the CLI version").action(() => {
5119
5620
  process.stdout.write(CLI_VERSION + "\n");
@@ -5140,6 +5641,7 @@ registerMultiWork(program);
5140
5641
  registerResume(program);
5141
5642
  registerReset(program);
5142
5643
  registerScan(program);
5644
+ registerFastTrack(program);
5143
5645
  registerPrTest(program);
5144
5646
  registerScheduledTask(program);
5145
5647
  registerRuns(program);