@fastagent-sh/fastagent 0.15.0 → 0.16.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 (168) hide show
  1. package/README.md +13 -3
  2. package/dist/channels/agentcore-limits.d.ts +7 -0
  3. package/dist/channels/agentcore-limits.js +9 -0
  4. package/dist/channels/agentcore-state.d.ts +83 -0
  5. package/dist/channels/agentcore-state.js +258 -0
  6. package/dist/channels/agentcore.d.ts +98 -0
  7. package/dist/channels/agentcore.js +260 -0
  8. package/dist/channels/busy.d.ts +23 -0
  9. package/dist/channels/busy.js +53 -0
  10. package/dist/channels/context-buffer.d.ts +5 -0
  11. package/dist/channels/context-buffer.js +5 -0
  12. package/dist/channels/feishu/context-buffer.d.ts +13 -8
  13. package/dist/channels/feishu/context-buffer.js +60 -6
  14. package/dist/channels/feishu/feishu-api.js +4 -1
  15. package/dist/channels/feishu/feishu.d.ts +0 -12
  16. package/dist/channels/feishu/feishu.js +143 -54
  17. package/dist/channels/feishu/invoke-turn.js +35 -18
  18. package/dist/channels/feishu/model.d.ts +0 -1
  19. package/dist/channels/feishu/normalize.js +0 -1
  20. package/dist/channels/feishu/parse.d.ts +21 -7
  21. package/dist/channels/feishu/parse.js +24 -7
  22. package/dist/channels/feishu/preview.js +3 -2
  23. package/dist/channels/feishu/scaffold/channel.ts +9 -8
  24. package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
  25. package/dist/channels/feishu/setup-mode.d.ts +30 -1
  26. package/dist/channels/feishu/setup-mode.js +27 -1
  27. package/dist/channels/github/github.js +8 -1
  28. package/dist/channels/http.js +1 -1
  29. package/dist/channels/lark/scaffold/channel.ts +9 -8
  30. package/dist/channels/lark/scaffold/lark-send.ts +6 -4
  31. package/dist/channels/preview-kit.d.ts +7 -1
  32. package/dist/channels/preview-kit.js +3 -2
  33. package/dist/channels/slack/parse.d.ts +16 -1
  34. package/dist/channels/slack/parse.js +46 -3
  35. package/dist/channels/slack/preview.d.ts +1 -2
  36. package/dist/channels/slack/preview.js +68 -24
  37. package/dist/channels/slack/scaffold/channel.ts +5 -5
  38. package/dist/channels/slack/slack-api.d.ts +3 -23
  39. package/dist/channels/slack/slack-api.js +6 -22
  40. package/dist/channels/slack/slack.d.ts +13 -20
  41. package/dist/channels/slack/slack.js +95 -50
  42. package/dist/channels/state.d.ts +11 -4
  43. package/dist/channels/state.js +19 -12
  44. package/dist/channels/tasks.d.ts +0 -6
  45. package/dist/channels/tasks.js +16 -1
  46. package/dist/channels/telegram/parse.d.ts +0 -7
  47. package/dist/channels/telegram/parse.js +4 -2
  48. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
  49. package/dist/channels/telegram/telegram.js +1 -1
  50. package/dist/channels/text.d.ts +14 -0
  51. package/dist/channels/text.js +14 -0
  52. package/dist/channels/thread-participants.d.ts +21 -0
  53. package/dist/channels/thread-participants.js +132 -0
  54. package/dist/channels/turn-queue.js +7 -0
  55. package/dist/cli/add-feishu.d.ts +7 -4
  56. package/dist/cli/add-feishu.js +57 -37
  57. package/dist/cli/add-slack.d.ts +2 -1
  58. package/dist/cli/add-slack.js +6 -11
  59. package/dist/cli/commands/add.js +50 -51
  60. package/dist/cli/commands/attach.js +8 -4
  61. package/dist/cli/commands/chat.js +8 -8
  62. package/dist/cli/commands/deploy.d.ts +1 -1
  63. package/dist/cli/commands/deploy.js +323 -77
  64. package/dist/cli/commands/dev.js +24 -22
  65. package/dist/cli/commands/fire.js +15 -16
  66. package/dist/cli/commands/info.js +36 -29
  67. package/dist/cli/commands/init.d.ts +1 -1
  68. package/dist/cli/commands/init.js +65 -53
  69. package/dist/cli/commands/invoke.js +9 -6
  70. package/dist/cli/commands/login.js +35 -21
  71. package/dist/cli/commands/schedule.js +6 -8
  72. package/dist/cli/commands/start.js +93 -37
  73. package/dist/cli/commands/tool.js +22 -18
  74. package/dist/cli/fail.d.ts +17 -0
  75. package/dist/cli/fail.js +24 -0
  76. package/dist/cli/program.js +57 -36
  77. package/dist/cli/serve.d.ts +26 -6
  78. package/dist/cli/serve.js +62 -15
  79. package/dist/cli/shared.d.ts +15 -2
  80. package/dist/cli/shared.js +30 -18
  81. package/dist/deploy/agentcore/plan.d.ts +117 -0
  82. package/dist/deploy/agentcore/plan.js +721 -0
  83. package/dist/deploy/agentcore/run.d.ts +73 -0
  84. package/dist/deploy/agentcore/run.js +412 -0
  85. package/dist/deploy/agentcore/zip.d.ts +17 -0
  86. package/dist/deploy/agentcore/zip.js +68 -0
  87. package/dist/deploy/container.d.ts +26 -25
  88. package/dist/deploy/container.js +93 -89
  89. package/dist/deploy/docker/plan.d.ts +1 -1
  90. package/dist/deploy/docker/plan.js +12 -17
  91. package/dist/deploy/fly/plan.d.ts +2 -0
  92. package/dist/deploy/fly/plan.js +27 -19
  93. package/dist/deploy/fly/run.d.ts +12 -1
  94. package/dist/deploy/fly/run.js +36 -2
  95. package/dist/deploy/preflight.d.ts +11 -5
  96. package/dist/deploy/preflight.js +217 -65
  97. package/dist/deploy/railway/plan.d.ts +7 -0
  98. package/dist/deploy/railway/plan.js +41 -16
  99. package/dist/deploy/railway/run.d.ts +8 -1
  100. package/dist/deploy/railway/run.js +7 -2
  101. package/dist/deploy/runner.d.ts +5 -2
  102. package/dist/deploy/runner.js +9 -3
  103. package/dist/dev-supervisor.d.ts +11 -8
  104. package/dist/dev-supervisor.js +53 -51
  105. package/dist/engines/pi/auth.d.ts +8 -7
  106. package/dist/engines/pi/auth.js +12 -10
  107. package/dist/engines/pi/channel.d.ts +1 -1
  108. package/dist/engines/pi/channel.js +5 -5
  109. package/dist/engines/pi/chat.js +2 -2
  110. package/dist/engines/pi/config.d.ts +6 -46
  111. package/dist/engines/pi/config.js +21 -108
  112. package/dist/engines/pi/create.d.ts +8 -8
  113. package/dist/engines/pi/create.js +13 -12
  114. package/dist/engines/pi/definition.d.ts +7 -26
  115. package/dist/engines/pi/definition.js +8 -54
  116. package/dist/engines/pi/login.d.ts +1 -1
  117. package/dist/engines/pi/models.d.ts +3 -3
  118. package/dist/engines/pi/models.js +1 -1
  119. package/dist/engines/pi/{workspace.d.ts → open.d.ts} +29 -23
  120. package/dist/engines/pi/{workspace.js → open.js} +27 -29
  121. package/dist/engines/pi/session-builder.d.ts +2 -2
  122. package/dist/engines/pi/session-builder.js +11 -11
  123. package/dist/engines/pi/tool.js +4 -0
  124. package/dist/env.d.ts +16 -4
  125. package/dist/env.js +43 -5
  126. package/dist/host/node.d.ts +2 -2
  127. package/dist/loader.d.ts +2 -2
  128. package/dist/loader.js +3 -3
  129. package/dist/log.d.ts +1 -1
  130. package/dist/log.js +1 -1
  131. package/dist/paths.d.ts +138 -0
  132. package/dist/paths.js +326 -0
  133. package/dist/pi.d.ts +1 -1
  134. package/dist/pi.js +2 -2
  135. package/dist/runtime.d.ts +7 -5
  136. package/dist/runtime.js +2 -2
  137. package/dist/scaffold/add-channel.d.ts +7 -3
  138. package/dist/scaffold/add-channel.js +55 -29
  139. package/dist/scaffold/init.d.ts +32 -41
  140. package/dist/scaffold/init.js +161 -185
  141. package/dist/scaffold/templates/env.example +15 -6
  142. package/dist/scaffold/templates/fastagent.config.mjs +1 -1
  143. package/dist/scaffold/templates/gitignore +14 -6
  144. package/dist/scaffold/templates/persona.md +4 -2
  145. package/dist/scaffold/templates/secrets.gitignore +5 -0
  146. package/dist/scaffold/templates.d.ts +1 -7
  147. package/dist/scaffold/templates.js +3 -25
  148. package/dist/scaffold/vendor-skill.d.ts +2 -2
  149. package/dist/scaffold/vendor-skill.js +13 -13
  150. package/dist/schedule/discover.js +4 -4
  151. package/dist/schedule/scheduler.d.ts +40 -1
  152. package/dist/schedule/scheduler.js +89 -56
  153. package/dist/schedule/state.js +1 -1
  154. package/dist/schedule/wake-alarm.d.ts +47 -0
  155. package/dist/schedule/wake-alarm.js +136 -0
  156. package/dist/schedule/wakeups.d.ts +1 -0
  157. package/dist/schedule/wakeups.js +18 -0
  158. package/dist/tunnel.d.ts +3 -3
  159. package/dist/tunnel.js +7 -7
  160. package/package.json +4 -1
  161. package/dist/channels/feishu/owned-threads.d.ts +0 -7
  162. package/dist/channels/feishu/owned-threads.js +0 -47
  163. package/dist/channels/slack/owned-threads.d.ts +0 -6
  164. package/dist/channels/slack/owned-threads.js +0 -43
  165. package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
  166. package/dist/scaffold/templates/gitignore.kit +0 -2
  167. package/dist/workspace.d.ts +0 -9
  168. package/dist/workspace.js +0 -45
@@ -1,21 +1,23 @@
1
1
  /**
2
2
  * `fastagent dev`: a SUPERVISOR that spawns a worker (this command with FASTAGENT_DEV_WORKER set) to
3
- * assemble + serve, restarting it on workspace edits. A fresh process per reload means what is served
3
+ * assemble + serve, restarting it on agent edits. A fresh process per reload means what is served
4
4
  * is always the latest code, including modules a tool/config imports.
5
5
  */
6
6
  import { resolve } from "node:path";
7
7
  import { runDevSupervisor } from "../../dev-supervisor.js";
8
8
  import { loadDotEnv } from "../../env.js";
9
9
  import { reportDefinitionWarnings, reportModuleLoadFailures, reportToolCollisions } from "../../engines/pi/report.js";
10
- import { createPiAgentFromWorkspace } from "../../engines/pi/workspace.js";
11
- import { log, setLogLevel } from "../../log.js";
10
+ import { createPiAgentFromDir } from "../../engines/pi/open.js";
11
+ import { setLogLevel } from "../../log.js";
12
12
  import { logAgentLoop } from "../../observe.js";
13
13
  import { installProxyFetch } from "../../proxy.js";
14
- import { failStartup } from "../fail.js";
14
+ import { workspaceHint } from "../../paths.js";
15
+ import { failStartup, placementOrExit } from "../fail.js";
15
16
  import { maybeTunnel, mountSessionControl, routesFor, serve, startSchedules } from "../serve.js";
16
- import { parsePort, reportAuth, resolveFirstRunModel } from "../shared.js";
17
+ import { parsePort, reportAuth, reportLine, resolveFirstRunModel, reportWorkspaceHint } from "../shared.js";
17
18
  export async function runDev(dirArg, opts) {
18
19
  const dir = resolve(dirArg);
20
+ const placement = placementOrExit(dir);
19
21
  setLogLevel("debug"); // dev posture: verbose, includes the debug turn trace (content) — supervisor and worker both
20
22
  const isWorker = process.env.FASTAGENT_DEV_WORKER === "1";
21
23
  // Pick a model interactively once, in the parent (both watch and --no-watch have a TTY); a spawned
@@ -23,32 +25,32 @@ export async function runDev(dirArg, opts) {
23
25
  // the proxy FIRST (as invoke/start do): the picker reads FASTAGENT_MODEL and provider keys from
24
26
  // .env, and getAuth's OAuth refresh must go through HTTPS_PROXY. The worker re-loads both in serveOnce.
25
27
  if (!isWorker) {
26
- loadDotEnv(dir);
28
+ loadDotEnv(placement.agentDir);
27
29
  installProxyFetch();
28
- await resolveFirstRunModel(dir, opts);
30
+ await resolveFirstRunModel(placement.agentDir, opts);
29
31
  }
30
32
  if (isWorker || opts.watch === false) {
31
33
  await serveOnce(dir, opts);
32
34
  return;
33
35
  }
34
36
  parsePort(opts.port, "--port", "flag"); // flag-shape check before spawning
35
- await runDevSupervisor(dir, { tunnel: opts.tunnel ?? false });
37
+ await runDevSupervisor(placement, { tunnel: opts.tunnel ?? false });
36
38
  }
37
- /** Assemble the workspace agent and serve it once (the dev worker; also the --no-watch path). */
39
+ /** Assemble the agent and serve it once (the dev worker; also the --no-watch path). */
38
40
  async function serveOnce(dir, opts) {
39
41
  const portFlag = parsePort(opts.port, "--port", "flag");
40
- loadDotEnv(dir);
42
+ loadDotEnv(placementOrExit(dir).agentDir);
41
43
  installProxyFetch();
42
- const a = await createPiAgentFromWorkspace(dir, {
44
+ const a = await createPiAgentFromDir(dir, {
43
45
  model: opts.model,
44
46
  authPath: opts.authPath, // flag > FASTAGENT_AUTH_PATH > default — resolved by the opener (one owner)
45
47
  serving: true, // long-running serve: the scheduler poller runs (wake mounts iff config.selfSchedule)
46
48
  }).catch(failStartup);
47
- log.info(`[fastagent] dir: ${dir}`);
48
- if (a.agentDir !== dir)
49
- log.info(`[fastagent] agent: ${a.agentDir}`);
50
- log.info(`[fastagent] config: ${a.configPath ?? "(zero-config)"}`);
51
- log.info(`[fastagent] model: ${a.modelSpec}${a.config.thinkingLevel ? ` (thinking: ${a.config.thinkingLevel})` : ""}`);
49
+ reportLine("agent", a.agentDir);
50
+ reportLine("workspace", a.workspace);
51
+ reportWorkspaceHint(workspaceHint(a));
52
+ reportLine("config", a.configPath ?? "(none)");
53
+ reportLine("model", `${a.modelSpec}${a.config.thinkingLevel ? ` (thinking: ${a.config.thinkingLevel})` : ""}`);
52
54
  await reportAuth(a.modelSpec, a.authPath);
53
55
  reportAgentsSkillsTools(a);
54
56
  // Trace each turn's agent loop (tool calls + reply) to the log at debug level — shown in dev, gated
@@ -62,19 +64,19 @@ async function serveOnce(dir, opts) {
62
64
  await startSchedules(a.agentDir, traced, a.stateRoot, a.config.selfSchedule ?? false);
63
65
  serve({ ...routed, routes: withControl.routes }, portFlag ?? a.config.http?.port ?? 8787, (p) => {
64
66
  withControl.announce(p);
65
- maybeTunnel(dir, routed.routeChannels, p, opts.tunnel ?? false, a.stateRoot);
67
+ maybeTunnel(a.agentDir, routed.routeChannels, p, opts.tunnel ?? false, a.stateRoot);
66
68
  });
67
69
  }
68
70
  /** The agents/skills/tools/collisions report lines. */
69
71
  function reportAgentsSkillsTools(a) {
70
- log.info(`[fastagent] context: ${a.definition.contextFiles.map((f) => f.path).join(", ") || "(none)"}`);
72
+ reportLine("context", a.definition.contextFiles.map((f) => f.path).join(", ") || "(none)");
71
73
  if (a.definition.persona)
72
- log.info(`[fastagent] persona: persona.md`);
73
- log.info(`[fastagent] skills: ${a.definition.skills.map((s) => s.name).join(", ") || "(none)"}`);
74
+ reportLine("persona", "persona.md");
75
+ reportLine("skills", a.definition.skills.map((s) => s.name).join(", ") || "(none)");
74
76
  if (a.toolNames.length > 0)
75
- log.info(`[fastagent] tools: ${a.toolNames.join(", ")}`);
77
+ reportLine("tools", a.toolNames.join(", "));
76
78
  if (a.deferredToolNames.length > 0) {
77
- log.info(`[fastagent] deferred: ${a.deferredToolNames.join(", ")} (activated via search_tools)`);
79
+ reportLine("deferred", `${a.deferredToolNames.join(", ")} (activated via search_tools)`);
78
80
  }
79
81
  reportToolCollisions(a.toolCollisions);
80
82
  reportModuleLoadFailures(a.toolFailures);
@@ -4,36 +4,35 @@
4
4
  * the schedule's stable session (faithful to the served behavior). Does NOT advance the schedule's fire
5
5
  * state — a test run must never make the scheduler skip the real next run.
6
6
  */
7
- import { relative, resolve, sep } from "node:path";
7
+ import { join, resolve } from "node:path";
8
8
  import { loadDotEnv } from "../../env.js";
9
- import { loadConfig, resolveAgentDir } from "../../engines/pi/config.js";
9
+ import { displayPath } from "../../paths.js";
10
10
  import { reportModuleLoadFailures } from "../../engines/pi/report.js";
11
- import { createPiAgentFromWorkspace } from "../../engines/pi/workspace.js";
11
+ import { createPiAgentFromDir } from "../../engines/pi/open.js";
12
12
  import { runInvokeStream } from "../invoke-stream.js";
13
13
  import { installProxyFetch } from "../../proxy.js";
14
14
  import { loadSchedules } from "../../schedule/discover.js";
15
15
  import { scheduleSession } from "../../schedule/scheduler.js";
16
- import { failStartup } from "../fail.js";
16
+ import { failStartup, placementOrExit } from "../fail.js";
17
17
  import { reportAuth, resolveFirstRunModel } from "../shared.js";
18
18
  export async function runFire(name, dirArg, opts) {
19
19
  const fireDir = resolve(dirArg);
20
- loadDotEnv(fireDir);
20
+ const placement = placementOrExit(fireDir);
21
+ loadDotEnv(placement.agentDir);
21
22
  installProxyFetch();
22
- await resolveFirstRunModel(fireDir, opts);
23
- // Schedules are agent surface — discover them where dev/start/`schedule list` do (agentDir), not the
24
- // run root, so `fire` sees the same set the scheduler serves in the kit layout.
25
- const { config: fireConfig } = await loadConfig(fireDir).catch(failStartup);
26
- const fireAgentDir = resolveAgentDir(fireDir, fireConfig);
27
- const { schedules, failures } = await loadSchedules(fireAgentDir).catch(failStartup);
23
+ await resolveFirstRunModel(placement.agentDir, opts);
24
+ // Schedules are agent surface — discover them where dev/start/`schedule list` do (the agent
25
+ // dir), so `fire` sees the same set the scheduler serves.
26
+ const { schedules, failures } = await loadSchedules(placement.agentDir).catch(failStartup);
28
27
  reportModuleLoadFailures(failures);
29
28
  const schedule = schedules.find((s) => s.name === name);
30
29
  if (!schedule) {
31
- // Name the discovery path in the kit layout: a schedule misplaced at the run root should read as
32
- // "wrong place", not "broken file".
33
- const looked = fireAgentDir === fireDir ? "" : ` (looked in ${relative(fireDir, fireAgentDir).split(sep).join("/")}/schedules)`;
34
- failStartup(new Error(`unknown schedule "${name}"${looked}. available: ${schedules.map((s) => s.name).join(", ") || "(none)"}`));
30
+ // Name the discovery path: a schedule misplaced in the workspace (outside the agent dir)
31
+ // should read as "wrong place", not "broken file".
32
+ failStartup(new Error(`unknown schedule "${name}" (looked in ${displayPath(process.cwd(), join(placement.agentDir, "schedules")) ?? "schedules"}). ` +
33
+ `available: ${schedules.map((s) => s.name).join(", ") || "(none)"}`));
35
34
  }
36
- const { agent, modelSpec, authPath } = await createPiAgentFromWorkspace(fireDir, {
35
+ const { agent, modelSpec, authPath } = await createPiAgentFromDir(fireDir, {
37
36
  model: opts.model,
38
37
  authPath: opts.authPath, // flag > FASTAGENT_AUTH_PATH > default — resolved by the opener (one owner)
39
38
  }).catch(failStartup);
@@ -2,27 +2,29 @@
2
2
  import { resolve } from "node:path";
3
3
  import { loadDotEnv } from "../../env.js";
4
4
  import { discoverChannelFiles } from "../../engines/pi/channel.js";
5
- import { defaultSessionsDir, loadConfig, resolveAgentDir, resolveAuthPath, resolveModelSpec, resolveSessionsDirOverride, resolveStateRoot, } from "../../engines/pi/config.js";
6
- import { resolveWorkspaceTools } from "../../engines/pi/create.js";
5
+ import { defaultSessionsDir, loadConfig, resolveAuthPath, resolveModelSpec, resolveSessionsDirOverride, } from "../../engines/pi/config.js";
6
+ import { resolveStateRoot, workspaceHint } from "../../paths.js";
7
+ import { resolveAgentTools } from "../../engines/pi/create.js";
7
8
  import { loadAgentDefinition } from "../../engines/pi/definition.js";
8
9
  import { reportDefinitionWarnings, reportModuleLoadFailures, reportToolCollisions } from "../../engines/pi/report.js";
9
10
  import { log } from "../../log.js";
10
11
  import { nextRun } from "../../schedule/cron.js";
11
12
  import { loadSchedules } from "../../schedule/discover.js";
12
- import { failStartup } from "../fail.js";
13
+ import { failStartup, placementOrExit } from "../fail.js";
13
14
  export async function runInfo(dirArg, opts) {
14
15
  const dir = resolve(dirArg);
15
- loadDotEnv(dir); // skills/tools may read env at load time
16
- const { config, path: configPath } = await loadConfig(dir).catch(failStartup);
16
+ const { agentDir, workspace } = placementOrExit(dir);
17
+ loadDotEnv(agentDir); // skills/tools may read env at load time
18
+ const { config, path: configPath } = await loadConfig(agentDir).catch(failStartup);
17
19
  const modelSpec = resolveModelSpec(opts.model, config);
18
- // dir = the run root (cwd, whose AGENTS.md is ② context); the agent's own surface lives in agentDir.
19
- const agentDir = resolveAgentDir(dir, config);
20
- const definition = await loadAgentDefinition(agentDir, { cwd: dir }).catch(failStartup);
20
+ // agentDir = where the agent lives (definition + config + machinery); workspace = what it works ON
21
+ // (its cwd, whose AGENTS.md ancestors are ② context).
22
+ const definition = await loadAgentDefinition(agentDir, { cwd: workspace }).catch(failStartup);
21
23
  // A tool that fails to load, for any reason (a missing dep, a top-level throw, or just not being a
22
24
  // tool), is isolated the same way everywhere (G2): info, dev, AND start report it and keep going with
23
25
  // the tools that loaded. The `error`/`.catch` below only fires for a whole-load fault (an unreadable
24
26
  // tools/ dir), not a single bad file.
25
- const tools = await resolveWorkspaceTools(config, agentDir, dir)
27
+ const tools = await resolveAgentTools(config, agentDir, workspace)
26
28
  .then((r) => ({
27
29
  names: r.toolNames,
28
30
  deferred: r.deferredToolNames,
@@ -50,13 +52,13 @@ export async function runInfo(dirArg, opts) {
50
52
  }));
51
53
  // The default sessions/auth paths WITHOUT creating anything (info is read-only; dev/start mkdir/login
52
54
  // create them, info must not).
53
- const stateRoot = resolveStateRoot(dir);
55
+ const stateRoot = resolveStateRoot(agentDir);
54
56
  const sessionsDir = resolveSessionsDirOverride(opts.sessionsDir) ?? defaultSessionsDir(stateRoot);
55
- const authPath = resolveAuthPath(dir, opts.authPath); // flag > FASTAGENT_AUTH_PATH > default — the one owner
57
+ const authPath = resolveAuthPath(agentDir, opts.authPath); // flag > FASTAGENT_AUTH_PATH > default — the one owner
56
58
  if (opts.json) {
57
59
  console.log(JSON.stringify({
58
- dir,
59
60
  agentDir,
61
+ workspace,
60
62
  configPath: configPath ?? null,
61
63
  model: modelSpec ?? null,
62
64
  thinkingLevel: config.thinkingLevel ?? null,
@@ -80,25 +82,30 @@ export async function runInfo(dirArg, opts) {
80
82
  }, null, 2));
81
83
  return;
82
84
  }
83
- console.log(`dir: ${dir}`);
84
- if (agentDir !== dir)
85
- console.log(`agent: ${agentDir}`);
86
- console.log(`config: ${configPath ?? "(none)"}`);
87
- console.log(`model: ${modelSpec ?? "(not set — pass --model, set FASTAGENT_MODEL, or config.model)"}`);
85
+ // One padded label writer: hand-spaced labels drifted out of alignment the moment a longer one
86
+ // (agent/workspace/selfSchedule) joined the report.
87
+ const line = (label, value) => console.log(`${`${label}:`.padEnd(13)} ${value}`);
88
+ line("agent", agentDir);
89
+ line("workspace", workspace);
90
+ const hint = workspaceHint({ agentDir, workspace });
91
+ if (hint)
92
+ line("hint", hint);
93
+ line("config", configPath ?? "(none)");
94
+ line("model", modelSpec ?? "(not set — pass --model, set FASTAGENT_MODEL, or config.model)");
88
95
  if (config.thinkingLevel)
89
- console.log(`thinking: ${config.thinkingLevel}`);
90
- console.log(`context: ${definition.contextFiles.map((f) => f.path).join(", ") || "(none)"}`);
91
- console.log(`persona: ${definition.persona ? "persona.md" : "(none)"}`);
92
- console.log(`skills: ${definition.skills.map((skill) => skill.name).join(", ") || "(none)"}`);
93
- console.log(`tools: ${tools.error ? "(could not load — see warning below)" : tools.names.join(", ") || "(none)"}`);
96
+ line("thinking", config.thinkingLevel);
97
+ line("context", definition.contextFiles.map((f) => f.path).join(", ") || "(none)");
98
+ line("persona", definition.persona ? "persona.md" : "(none)");
99
+ line("skills", definition.skills.map((skill) => skill.name).join(", ") || "(none)");
100
+ line("tools", tools.error ? "(could not load — see warning below)" : tools.names.join(", ") || "(none)");
94
101
  if (tools.deferred.length > 0)
95
- console.log(`deferred: ${tools.deferred.join(", ")} (activated via search_tools)`);
96
- console.log(`channels: ${channels.join(", ") || "(none)"}`);
97
- console.log(`schedules: ${schedules.map((s) => `${s.name} (next ${s.next ?? "never"})`).join(", ") || "(none)"}`);
98
- console.log(`selfSchedule: ${config.selfSchedule ? "on (mounts the wake tool when serving)" : "off"}`);
99
- console.log(`state: ${stateRoot}`);
100
- console.log(`sessions: ${sessionsDir}`);
101
- console.log(`auth: ${authPath}`);
102
+ line("deferred", `${tools.deferred.join(", ")} (activated via search_tools)`);
103
+ line("channels", channels.join(", ") || "(none)");
104
+ line("schedules", schedules.map((s) => `${s.name} (next ${s.next ?? "never"})`).join(", ") || "(none)");
105
+ line("selfSchedule", config.selfSchedule ? "on (mounts the wake tool when serving)" : "off");
106
+ line("state", stateRoot);
107
+ line("sessions", sessionsDir);
108
+ line("auth", authPath);
102
109
  reportToolCollisions(tools.collisions);
103
110
  reportModuleLoadFailures(tools.failures);
104
111
  reportModuleLoadFailures(sched.failures);
@@ -2,7 +2,7 @@ export interface InitOptions {
2
2
  minimal: boolean;
3
3
  /** false ⇔ `--no-install`. */
4
4
  install: boolean;
5
- flat: boolean;
5
+ /** The agent directory's name inside `dir` — undefined = the default, `"."` = `dir` itself (`--flat`). */
6
6
  agentDir?: string;
7
7
  }
8
8
  export declare function runInit(dirArg: string, opts: InitOptions): Promise<void>;
@@ -1,73 +1,85 @@
1
1
  /**
2
- * `fastagent init [dir]`: scaffold a runnable agent and install its dependencies. Layout: flags force;
3
- * otherwise the jurisdiction rule decides (see detectHostSignals) and the reason is printed.
4
- * Deliberately no prompt non-interactive executors (coding agents) get a deterministic default they
5
- * can read and override.
2
+ * `fastagent init [dir]`: scaffold a runnable agent and install its dependencies. Where the files land is
3
+ * a default plus one knob, never a detection and never a prompt — non-interactive executors (coding
4
+ * agents) get deterministic behavior they can read. By default the agent goes into `./fastagent/` and the
5
+ * directory around it is untouched; `--agent-dir <name>` names that directory anything, and `--agent-dir .`
6
+ * (spelled `--flat`) makes the directory itself the agent (a standalone agent repo, a monorepo package).
6
7
  */
7
8
  import { spawn } from "node:child_process";
8
- import { isAbsolute, join, relative, resolve, sep } from "node:path";
9
- import { detectHostSignals, nextStepCd, scaffoldWorkspace } from "../../scaffold/init.js";
9
+ import { basename, join, resolve } from "node:path";
10
+ import { DEFAULT_AGENT_DIRNAME, SECRETS_DIRNAME, agentsAt } from "../../paths.js";
11
+ import { detectRuntime, readPackageJson } from "../../runtime.js";
12
+ import { agentDirName, agentDirNameError, scaffoldAgent } from "../../scaffold/init.js";
13
+ import { displayPath } from "../../paths.js";
10
14
  import { failStartup, failUsage } from "../fail.js";
11
15
  export async function runInit(dirArg, opts) {
12
16
  const dir = resolve(dirArg);
13
- let agentDir;
14
- let signals = [];
15
- if (opts.agentDir) {
16
- // Same containment contract loadConfig enforces on config.agentDir: an escaping value would write
17
- // the kit outside the workspace AND produce a config that can never load — refuse up front.
18
- // POSIX-normalized: this lands verbatim in the generated config (agentDir: "./a/b") and the persona
19
- // locator note a Windows `relative()` would write backslashes into both.
20
- const rel = relative(dir, resolve(dir, opts.agentDir)).split(sep).join("/");
21
- if (rel === "" || rel === ".." || rel.startsWith("../") || isAbsolute(rel)) {
22
- // An invalid flag VALUE is a usage error (exit 2), same class as a value the parser rejects.
23
- failUsage(`--agent-dir ("${opts.agentDir}") must be a subdirectory of ${dir}`);
24
- }
25
- agentDir = `./${rel}`;
26
- }
27
- else if (!opts.flat) {
28
- signals = await detectHostSignals(dir).catch(failStartup);
29
- if (signals.length > 0)
30
- agentDir = "./agent";
31
- }
32
- const { complete, created, skipped, patched, intoNonEmpty, warnings } = await scaffoldWorkspace(dir, {
17
+ // A rejected flag VALUE is the usage class, same as one the parser rejects — so it is checked here,
18
+ // where exit 2 lives, not inside the scaffolder (whose throws are runtime failures, exit 1).
19
+ const requested = agentDirName(opts.agentDir);
20
+ const invalid = agentDirNameError(requested);
21
+ if (invalid)
22
+ failUsage(`--agent-dir "${requested}" ${invalid}`);
23
+ const { complete, agentDir: rel, created, kept, } = await scaffoldAgent(dir, {
33
24
  minimal: opts.minimal,
34
- agentDir,
25
+ agentDir: requested,
35
26
  }).catch(failStartup);
36
- // The layout reason prints only once the scaffold actually happened — an "already a workspace" refusal
37
- // must not be preceded by an announced decision that then never takes place.
38
- if (signals.length > 0) {
39
- console.error(`[fastagent] found ${signals.join(", ")} — an existing toolchain/deploy claims this directory, so the agent kit goes into ./agent (its own namespace; config.agentDir points there). cwd stays this directory. Override: --flat`);
27
+ const flat = rel === ".";
28
+ // The agent dir is where the manifest lives, so any install runs there — never against a surrounding
29
+ // workspace's package.json (its deps are its own concern). With `.` they are the same directory.
30
+ const agentDir = resolve(dir, rel);
31
+ console.error(`[fastagent] initialized ${dir}${complete ? "" : " (minimal)"} — ${flat ? "the directory IS the agent" : `agent in ./${rel}/`}`);
32
+ console.error(` created: ${created.join(", ")}`);
33
+ // A second agent beside an existing one is a supported shape, not an accident (an engineer's, a PM's
34
+ // and a content owner's agent can drive one repository), but it changes how this workspace resolves
35
+ // from now on — so say it HERE, at the moment it becomes true, instead of at the next command's
36
+ // refusal. The default name is the tie-break, so a workspace keeping `fastagent/` needs nothing.
37
+ const siblings = agentsAt(dir).map((a) => basename(a));
38
+ if (siblings.length > 1) {
39
+ const pick = siblings.includes(DEFAULT_AGENT_DIRNAME)
40
+ ? `\`${DEFAULT_AGENT_DIRNAME}\` answers by default; set FASTAGENT_AGENT=<name> for another`
41
+ : `set FASTAGENT_AGENT=<name> (in your shell or .envrc) to pick one`;
42
+ console.error(`[fastagent] note: ${dir} now holds ${siblings.length} agents (${siblings.join(", ")}) — ${pick}`);
40
43
  }
41
- console.error(`[fastagent] initialized ${dir}${complete ? "" : " (minimal)"}${agentDir ? ` — agent kit in ${agentDir}` : ""}`);
42
- if (created.length > 0)
43
- console.error(` created: ${created.join(", ")}`);
44
- if (skipped.length > 0)
45
- console.error(` kept existing: ${skipped.join(", ")}`);
46
- if (patched.length > 0)
47
- console.error(` updated: ${patched.join(", ")} (missing fastagent excludes appended)`);
48
- if (intoNonEmpty && !agentDir) {
49
- console.error(` note: scaffolded flat into a non-empty directory (nothing claims it — the directory is the agent); use --agent-dir <name> to put the kit in a subdir instead`);
44
+ if (kept.length > 0) {
45
+ // Adopting a directory: its own files win, always. Say which ones — and for the two that carry a
46
+ // consequence, say what that consequence is, because keeping them silently leaves the agent broken
47
+ // (package.json) or its machinery committable (.gitignore).
48
+ console.error(` kept your existing: ${kept.join(", ")}`);
49
+ if (kept.includes(join(rel, "package.json")) && complete) {
50
+ const add = detectRuntime(agentDir, await readPackageJson(agentDir)).runtime === "bun" ? "bun add" : "npm install";
51
+ console.error(`[fastagent] note: your package.json is untouched, so it does not list @fastagent-sh/fastagent — ` +
52
+ `run \`${add} @fastagent-sh/fastagent\` there or the scaffolded tools/ cannot resolve`);
53
+ }
54
+ const keptSecretsIgnore = kept.includes(join(rel, SECRETS_DIRNAME, ".gitignore"));
55
+ if (kept.includes(join(rel, ".gitignore"))) {
56
+ // The "credentials are covered either way" reassurance holds only when `.secrets/.gitignore` is
57
+ // OURS. On an adopted directory it may be the author's too, and then we know nothing about it —
58
+ // `add <channel>` defends that file with a write; `init` must not claim more than it did.
59
+ console.error(`[fastagent] note: your .gitignore is untouched — make sure it ignores node_modules, .state ` +
60
+ `and .cache` +
61
+ (keptSecretsIgnore ? `` : ` (.secrets/ carries its own .gitignore, so credentials are covered either way)`));
62
+ }
63
+ if (keptSecretsIgnore) {
64
+ console.error(`[fastagent] note: your ${join(rel, SECRETS_DIRNAME, ".gitignore")} is untouched — verify it ignores ` +
65
+ `everything but .env.example, because credentials land in that directory`);
66
+ }
50
67
  }
51
- for (const w of warnings)
52
- console.error(`[fastagent] warn: ${w}`);
53
- // Install deps only for a complete agent whose package.json we just wrote (a kept one is not ours).
54
- // The manifest lives with the kit (agentDir when set), so the install runs there — never against a
55
- // host repo's own package.json.
56
- const kitDir = resolve(dir, agentDir ?? ".");
57
- const willInstall = complete && opts.install && created.includes(join(agentDir ?? ".", "package.json"));
68
+ const willInstall = complete && opts.install && !kept.includes(join(rel, "package.json"));
58
69
  let installFailed = false;
59
70
  if (willInstall) {
60
- console.error(`[fastagent] installing dependencies (npm install${agentDir ? ` in ${agentDir}` : ""})…`);
61
- installFailed = (await npmInstall(kitDir)) !== 0;
71
+ console.error(`[fastagent] installing dependencies (npm install${flat ? "" : ` in ${rel}`})…`);
72
+ installFailed = (await npmInstall(agentDir)) !== 0;
62
73
  if (installFailed)
63
- console.error(`[fastagent] warn: npm install failed — run it manually in ${kitDir} before \`fastagent dev\``);
74
+ console.error(`[fastagent] warn: npm install failed — run it manually in ${agentDir} before \`fastagent dev\``);
64
75
  }
65
76
  console.error(` next steps:`);
66
- const cdTarget = nextStepCd(process.cwd(), dir);
77
+ const cdTarget = displayPath(process.cwd(), dir);
67
78
  if (cdTarget)
68
79
  console.error(` cd ${cdTarget}`);
69
- if (complete && (!opts.install || installFailed))
70
- console.error(` ${agentDir ? `(cd ${agentDir} && npm install)` : "npm install"}`);
80
+ if (complete && (!willInstall || installFailed)) {
81
+ console.error(` ${flat ? "npm install" : `(cd ${rel} && npm install)`}`);
82
+ }
71
83
  console.error(` fastagent dev # serve locally and iterate`);
72
84
  console.error(` fastagent add skill <owner/repo/path> # vendor more skills from GitHub`);
73
85
  }
@@ -2,21 +2,24 @@
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { resolve } from "node:path";
4
4
  import { loadDotEnv } from "../../env.js";
5
- import { createPiAgentFromWorkspace } from "../../engines/pi/workspace.js";
5
+ import { createPiAgentFromDir } from "../../engines/pi/open.js";
6
6
  import { runInvokeStream } from "../invoke-stream.js";
7
7
  import { installProxyFetch } from "../../proxy.js";
8
- import { failStartup } from "../fail.js";
8
+ import { failStartup, placementOrExit } from "../fail.js";
9
9
  import { reportAuth, resolveFirstRunModel } from "../shared.js";
10
10
  export async function runInvoke(message, dirArg, opts) {
11
11
  const invokeDir = resolve(dirArg);
12
- loadDotEnv(invokeDir);
12
+ const placement = placementOrExit(invokeDir);
13
+ loadDotEnv(placement.agentDir);
13
14
  installProxyFetch();
14
- await resolveFirstRunModel(invokeDir, opts);
15
- const { agent, modelSpec, authPath } = await createPiAgentFromWorkspace(invokeDir, {
15
+ await resolveFirstRunModel(placement.agentDir, opts);
16
+ const { agent, modelSpec, authPath } = await createPiAgentFromDir(invokeDir, {
16
17
  model: opts.model,
17
18
  authPath: opts.authPath, // flag > FASTAGENT_AUTH_PATH > default — resolved by the opener (one owner)
18
19
  }).catch(failStartup);
19
- console.error(`[fastagent] invoke: ${invokeDir} (${modelSpec})`);
20
+ // BOTH directories, like dev/start: from the workspace, `placement.workspace` alone equals the dir you
21
+ // typed, so it cannot tell you which agent actually ran.
22
+ console.error(`[fastagent] invoke: ${placement.agentDir} (workspace ${placement.workspace}, ${modelSpec})`);
20
23
  await reportAuth(modelSpec, authPath);
21
24
  // Fresh session per invoke (one-shot, no resume). runInvokeStream maps events→IO: reply→stdout,
22
25
  // tool/failure→stderr, exit 1 iff the turn failed (so CI can gate on it).
@@ -1,39 +1,53 @@
1
1
  /**
2
2
  * `fastagent login [provider]`: authenticate a model provider into the project-level auth file
3
- * (`<cwd>/.fastagent/auth.json`) by default, or `--auth-path`/`FASTAGENT_AUTH_PATH`. The positional is
4
- * the PROVIDER (not a dir), so the project is cwd — `cd` into your agent before logging in (running it
5
- * from $HOME writes the global `~/.fastagent/auth.json`).
3
+ * (`<agentDir>/.secrets/auth.json`) by default, or `--auth-path`/`FASTAGENT_AUTH_PATH`. The
4
+ * positional is the PROVIDER (not a dir), so the agent resolves from cwd — `cd` into your agent
5
+ * before logging in. OUTSIDE an agent there is no project credential to write, so the target is the
6
+ * user-global `~/.fastagent/.secrets/auth.json` (which is what running it from $HOME has always
7
+ * meant) — announced on stderr BEFORE the flow, because that is a different credential than the one
8
+ * `dev`/`start` in a real agent would read, and a silent scope switch is exactly the surprise this
9
+ * command must not ship.
6
10
  *
7
- * Creates and self-ignores `<cwd>/.fastagent/` (the credential's gitignored home) BEFORE the auth flow,
8
- * so the secret can never land untracked a flow that then fails (bad provider, abort) leaves that
9
- * empty state dir behind, by design (no secret without its `.gitignore`). Skipped for the HOME-global dir.
11
+ * fastagent writes no `.gitignore` here or anywhere else at runtime: `init` scaffolds the agent's one
12
+ * ignore file (covering `.secrets/`) and the author owns it from then on. The credential store creates
13
+ * its own directory, mode 0700.
10
14
  */
15
+ import { homedir } from "node:os";
16
+ import { join } from "node:path";
11
17
  import { loadDotEnv } from "../../env.js";
12
- import { resolveAuthPath, resolveStateRoot } from "../../engines/pi/config.js";
13
- import { ensureStateRootSelfIgnored, isUnderDir } from "../../engines/pi/definition.js";
18
+ import { resolveAuthPath } from "../../engines/pi/config.js";
19
+ import { GLOBAL_HOME_DIR, findAgentDir, placementDeadEnd } from "../../paths.js";
14
20
  import { LoginCancelled } from "../../engines/pi/login.js";
15
21
  import { installProxyFetch } from "../../proxy.js";
16
- import { failStartup } from "../fail.js";
22
+ import { failStartup, placementOrExit } from "../fail.js";
17
23
  import { isInteractive, loginWithKeyCheck } from "../shared.js";
18
24
  export async function runLogin(provider, opts) {
19
- const loginDir = process.cwd();
25
+ const cwd = process.cwd();
26
+ const agentDir = findAgentDir(cwd);
27
+ // "Outside an agent" must mean exactly that: a position with its own way out (inside an agent, or on a
28
+ // directory holding several) is a dead end every other command refuses, and taking the global fallback
29
+ // there would write a credential to a different scope under a message saying there is no agent here.
30
+ if (!agentDir && placementDeadEnd(cwd))
31
+ placementOrExit(cwd);
32
+ // Outside any agent the target is the user-global machinery home — handed over explicitly, so the
33
+ // path resolvers need no "is this $HOME?" special case to infer it.
34
+ const loginDir = agentDir ?? join(homedir(), GLOBAL_HOME_DIR);
20
35
  loadDotEnv(loginDir); // FASTAGENT_AUTH_PATH / a proxy (HTTPS_PROXY) may be configured in the project .env
21
36
  installProxyFetch(); // the OAuth token exchange must go through HTTPS_PROXY (region-locked providers)
22
- const stateRoot = resolveStateRoot(loginDir);
23
37
  const authPath = resolveAuthPath(loginDir, opts.authPath); // flag > FASTAGENT_AUTH_PATH > default — the one owner
24
- // login is the command that CREATES the credential file, so the leak guard binds HERE too (not only
25
- // in the opener): on an adapted project dir, a `login` before the first dev/start would otherwise
26
- // leave the secret untracked-but-committable. Unlike the opener (which populates the WHOLE root, so
27
- // it always self-ignores an in-tree root), login writes ONLY auth.json so guard iff the credential
28
- // actually lands under the in-tree root. An external `--auth-path`/`FASTAGENT_AUTH_PATH` writes
29
- // nothing in-tree (don't create an empty `.fastagent`); the guard also skips the HOME-global root.
30
- if (isUnderDir(authPath, stateRoot))
31
- await ensureStateRootSelfIgnored(loginDir, stateRoot);
38
+ // Announce when the FALLBACK is what decided the target: outside an agent with no explicit path,
39
+ // the credential lands somewhere no agent will read. An explicit `--auth-path`/FASTAGENT_AUTH_PATH
40
+ // is the user naming the file, so there is no surprise to announce (and announcing a path this run
41
+ // does not write would be worse than saying nothing). The resolved path goes in the message.
42
+ if (!agentDir && !opts.authPath && !process.env.FASTAGENT_AUTH_PATH) {
43
+ console.error(`[fastagent] no agent here (no fastagent.config.*, here or one level inside) ` +
44
+ `logging in GLOBALLY (${authPath}). An agent reads its own .secrets/auth.json: \`cd\` into one ` +
45
+ `first, or point this run at it with --auth-path.`);
46
+ }
32
47
  // login is inherently interactive — loginFlow renders provider/method menus and opens a browser (or
33
48
  // prompts for a key). In a non-TTY (a pipe, CI, a coding-agent shell) the menu can't receive keystrokes
34
49
  // and would hang; --no-input asks for the same posture explicitly. Fail fast with the reason instead
35
- // of stalling on an unanswerable prompt. (After the secret-hygiene self-ignore above, which is cheap
36
- // prep, so a later terminal login is safe.)
50
+ // of stalling on an unanswerable prompt.
37
51
  if (opts.input === false || !isInteractive()) {
38
52
  failStartup(new Error(`login is interactive (it shows a menu and opens a browser) — run it in a terminal, not a pipe/CI`));
39
53
  }
@@ -5,20 +5,20 @@
5
5
  */
6
6
  import { resolve } from "node:path";
7
7
  import { loadDotEnv } from "../../env.js";
8
- import { loadConfig, resolveAgentDir, resolveStateRoot } from "../../engines/pi/config.js";
8
+ import { resolveStateRoot } from "../../paths.js";
9
9
  import { reportModuleLoadFailures } from "../../engines/pi/report.js";
10
10
  import { readRuns } from "../../schedule/audit.js";
11
11
  import { nextRun } from "../../schedule/cron.js";
12
12
  import { loadSchedules } from "../../schedule/discover.js";
13
13
  import { listWakeups, removeWakeup } from "../../schedule/wakeups.js";
14
- import { failStartup } from "../fail.js";
14
+ import { failStartup, placementOrExit } from "../fail.js";
15
15
  /**
16
16
  * `fastagent schedule history <name> [dir]`: print the run audit for one schedule (or "wake") — fired
17
17
  * time, outcome, duration, reply/error. Read-only (reads `<stateRoot>/schedule/runs.jsonl`); the answer
18
18
  * to "did last night's run silently fail?". Text mode previews the reply/error; --json is the full record.
19
19
  */
20
20
  export function runScheduleHistory(name, dirArg, json) {
21
- const target = resolve(dirArg);
21
+ const { agentDir: target } = placementOrExit(resolve(dirArg));
22
22
  loadDotEnv(target); // FASTAGENT_STATE_DIR may live in .env — read the SAME state root the scheduler wrote
23
23
  const runs = readRuns(resolveStateRoot(target), name);
24
24
  if (json) {
@@ -45,11 +45,9 @@ export function runScheduleHistory(name, dirArg, json) {
45
45
  /** `fastagent schedule list [dir]`: everything that will fire — BOTH producers: the static `schedules/`
46
46
  * files (with their next instant) and the agent's pending self-scheduled wake-ups. Read-only. */
47
47
  export async function runScheduleList(dirArg, json) {
48
- const target = resolve(dirArg);
48
+ const { agentDir: target } = placementOrExit(resolve(dirArg));
49
49
  loadDotEnv(target);
50
- const { config } = await loadConfig(target).catch(failStartup);
51
- const agentDir = resolveAgentDir(target, config);
52
- const { schedules, failures } = await loadSchedules(agentDir).catch(failStartup);
50
+ const { schedules, failures } = await loadSchedules(target).catch(failStartup);
53
51
  reportModuleLoadFailures(failures);
54
52
  const wakeups = listWakeups(resolveStateRoot(target));
55
53
  if (json) {
@@ -75,7 +73,7 @@ export async function runScheduleList(dirArg, json) {
75
73
  /** `fastagent schedule cancel <id> [dir]`: remove a pending wake-up — the operator's kill switch (the
76
74
  * agent's own is the `unwake` tool). Unlike unwake it is NOT session-scoped: the operator owns the box. */
77
75
  export function runScheduleCancel(id, dirArg) {
78
- const target = resolve(dirArg);
76
+ const { agentDir: target } = placementOrExit(resolve(dirArg));
79
77
  loadDotEnv(target);
80
78
  if (removeWakeup(resolveStateRoot(target), id)) {
81
79
  // ponytail: the store's load→save is lock-free — a serving scheduler's claim-advance can race this