@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
@@ -13,11 +13,11 @@ export declare function piDefaultTools(cwd: string): AgentTool[];
13
13
  /** `config.tools` semantics: extra tools APPENDED after pi's defaults, never replacing them. */
14
14
  export declare function resolveTools(config: FastagentConfig, cwd: string): AgentTool[];
15
15
  /**
16
- * The full tool set a workspace mounts: pi defaults + `config.tools` + discovered `tools/` (deduped,
16
+ * The full tool set an agent mounts: pi defaults + `config.tools` + discovered `tools/` (deduped,
17
17
  * existing win), plus the non-default tool names and collisions to report. One source for the
18
18
  * dev/start openers AND `fastagent tool`, so they all mount exactly the same set.
19
19
  */
20
- export declare function resolveWorkspaceTools(config: FastagentConfig, agentDir: string, cwd?: string): Promise<{
20
+ export declare function resolveAgentTools(config: FastagentConfig, agentDir: string, cwd?: string): Promise<{
21
21
  tools: AgentTool[];
22
22
  toolNames: string[];
23
23
  /** Tools registered but not initially active (defineTool `deferred: true`) — discovered/activated
@@ -86,8 +86,8 @@ export interface CreatePiAgentOptions {
86
86
  */
87
87
  providers?: Provider[];
88
88
  /**
89
- * Credentials file for stored OAuth/API-key auth. Defaults to `~/.fastagent/auth.json`; the
90
- * directory opener passes the project-level `<dir>/.fastagent/auth.json` instead. Env vars are still
89
+ * Credentials file for stored OAuth/API-key auth. Defaults to `~/.fastagent/.secrets/auth.json`; the
90
+ * directory opener passes the project-level `<root>/.secrets/auth.json` instead. Env vars are still
91
91
  * consulted when a provider is absent from the file (resolution order is upstream-owned).
92
92
  */
93
93
  authPath?: string;
@@ -121,16 +121,16 @@ export interface CreatePiAgentFromDefinitionOptions {
121
121
  tools?: FastagentTool[];
122
122
  /**
123
123
  * The agent's working directory: where the default tools operate AND whose ancestors are walked for
124
- * ② project context (AGENTS.md). Defaults to `dir` (flat: the definition dir is also the run root).
125
- * Set it to the enclosing repo so a coding agent whose definition lives in `dir` operates on — and
126
- * reads the AGENTS.md of that repo (core.md scenario grid).
124
+ * ② project context (AGENTS.md). Defaults to `dir`. Set it to the enclosing repo so a coding agent
125
+ * whose definition lives in `dir` operates on — and reads the AGENTS.md of — that repo (core.md
126
+ * scenario grid); that is what the CLI's opener does with the workspace.
127
127
  */
128
128
  cwd?: string;
129
129
  /** Extra providers registered on top of the built-ins (your own gateway / self-hosted endpoint). */
130
130
  providers?: Provider[];
131
131
  /**
132
132
  * Credentials file (see {@link CreatePiAgentOptions.authPath}). Being dir-aware, this rung defaults
133
- * to the PROJECT-level `<dir>/.fastagent/auth.json` (matching `fastagent dev`/`start` on the same
133
+ * to the PROJECT-level `<dir>/.secrets/auth.json` (matching `fastagent dev`/`start` on the same
134
134
  * dir) — unlike the dir-less {@link createPiAgent}/{@link createPiModels}, which default global.
135
135
  */
136
136
  authPath?: string;
@@ -6,14 +6,15 @@
6
6
  * L1 createPiAgent(options) — assemble from typed parts (the canonical ctor).
7
7
  * L0 createPiAgentFromHarness({ harnessFactory }) — in invoke.ts (its body is the turn mechanism).
8
8
  *
9
- * Above L2 sits the workspace opener createPiAgentFromWorkspace (workspace.ts), which both `dev` and
9
+ * Above L2 sits the agent opener createPiAgentFromDir (open.ts), which both `dev` and
10
10
  * `start` drive. Each rung calls the one below; options narrow as you go up (L2 owns systemPrompt/skills —
11
11
  * they come from the definition; the openers own model/tools — from config resolution).
12
12
  */
13
13
  import { formatSkillsForSystemPrompt } from "@earendil-works/pi-agent-core";
14
14
  import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
15
15
  import { createCodingTools } from "@earendil-works/pi-coding-agent";
16
- import { defaultAuthPath, resolveModel, resolveStateRoot } from "./config.js";
16
+ import { defaultAuthPath, resolveModel } from "./config.js";
17
+ import { resolveSecretsDir } from "../../paths.js";
17
18
  import { loadAgentDefinition } from "./definition.js";
18
19
  import { piHarnessFactory } from "./harness.js";
19
20
  import { createPiModels } from "./models.js";
@@ -37,18 +38,18 @@ export function resolveTools(config, cwd) {
37
38
  return config.tools ? [...defaults, ...config.tools] : defaults;
38
39
  }
39
40
  /**
40
- * The full tool set a workspace mounts: pi defaults + `config.tools` + discovered `tools/` (deduped,
41
+ * The full tool set an agent mounts: pi defaults + `config.tools` + discovered `tools/` (deduped,
41
42
  * existing win), plus the non-default tool names and collisions to report. One source for the
42
43
  * dev/start openers AND `fastagent tool`, so they all mount exactly the same set.
43
44
  */
44
- export async function resolveWorkspaceTools(config, agentDir, cwd = agentDir) {
45
- // Default coding tools (read/bash/edit/write) are rooted at `cwd` (the run root the agent operates on);
46
- // discovered `tools/` come from `agentDir` (the agent's own surface). They coincide in the flat case.
45
+ export async function resolveAgentTools(config, agentDir, cwd = agentDir) {
46
+ // Default coding tools (read/bash/edit/write) are rooted at `cwd` (the workspace the agent operates
47
+ // on); discovered `tools/` come from `agentDir` (the agent's own surface).
47
48
  const discovered = await loadTools(agentDir);
48
49
  const merged = mergeDiscoveredTools(resolveTools(config, cwd), discovered.tools);
49
- // The built-in `search_tools` loader mounts here — the one place the workspace's full tool set is
50
- // computed — so `dev`/`start`/`info`/`fastagent tool` all see the same surface (idempotent; a
51
- // workspace-defined search_tools wins).
50
+ // The built-in `search_tools` loader mounts here — the one place the agent's full tool set is
51
+ // computed — so `dev`/`start`/`info`/`fastagent tool` all see the same surface (idempotent; an
52
+ // agent-defined search_tools wins).
52
53
  const tools = withSearchTool(merged.tools);
53
54
  // Builtin = a search_tools that was ABSENT before withSearchTool (a reference compare would misfire
54
55
  // on the deferred-authored-loader case, where withSearchTool returns a new array without adding one).
@@ -98,7 +99,7 @@ export function piBasePrompt(options = {}) {
98
99
  const deferredCount = mounted.length - tools.length;
99
100
  const toolsList = tools.length > 0 ? tools.map((t) => `- ${t.name}: ${(t.description ?? "").split("\n")[0]}`).join("\n") : "(none)";
100
101
  // Segment ① identity: an authored persona (persona.md) replaces the default engine identity line
101
- // (the standalone×code-repo cell's persona; core.md §11), keeping the tools list + guidelines below.
102
+ // (core.md §11), keeping the tools list + guidelines below.
102
103
  const identity = options.persona?.trim() ||
103
104
  "You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.";
104
105
  const deferredNote = deferredCount > 0
@@ -220,9 +221,9 @@ export async function createPiAgentFromDefinition(dir, options) {
220
221
  model: options.model,
221
222
  thinkingLevel: options.thinkingLevel,
222
223
  providers: options.providers,
223
- // Dir-aware default: the same state-root-derived file the opener uses for this dir (the opener
224
+ // Dir-aware default: the same secrets-dir-derived file the opener uses for this dir (the opener
224
225
  // passes an explicit authPath, so this only affects direct L2 callers).
225
- authPath: options.authPath ?? defaultAuthPath(resolveStateRoot(dir)),
226
+ authPath: options.authPath ?? defaultAuthPath(resolveSecretsDir(dir)),
226
227
  // The directory is the agent, LIVE: re-read the definition on every invoke, so AGENTS.md/skills
227
228
  // edits (the author's, or the agent's own self-modification) take effect on the next turn with
228
229
  // no process restart — restarts are reserved for code (tools/channels/config, module cache).
@@ -32,9 +32,9 @@ export interface LoadedDefinition {
32
32
  }
33
33
  export interface LoadAgentDefinitionOptions {
34
34
  /**
35
- * Working directory whose ancestors are walked for context files (segment ②). Default = `agentDir`
36
- * (flat: the agent dir is also the run root). The opener passes the run root so a coding agent that
37
- * lives in `agentDir` picks up the host repo's AGENTS.md up the tree (core.md scenario grid).
35
+ * Working directory whose ancestors are walked for context files (segment ②). Default = `agentDir`.
36
+ * The opener passes the workspace instead, so an agent that lives in `agentDir` picks up the
37
+ * project's AGENTS.md up the tree (core.md scenario grid).
38
38
  */
39
39
  cwd?: string;
40
40
  env?: ExecutionEnv;
@@ -42,31 +42,12 @@ export interface LoadAgentDefinitionOptions {
42
42
  /** Read an agent definition. persona.md/skills come from `agentDir`; ② context = pi's loadProjectContextFiles({ cwd, agentDir }). */
43
43
  export declare function loadAgentDefinition(agentDir: string, options?: LoadAgentDefinitionOptions): Promise<LoadedDefinition>;
44
44
  /**
45
- * Whether `targetPath` lives inside `baseDir` (same path counts). The self-ignore guard uses it to ask
46
- * "does the resolved state root land inside the workspace tree?" an in-tree root (the default
47
- * `.fastagent`, or a custom `FASTAGENT_STATE_DIR` pointed inside the agent dir) is ours to self-ignore;
48
- * a root on an external volume resolves outside and must not be (we never write a `.gitignore` outside
49
- * the tree). Whether a relative override lands in-tree is a cwd question — see `resolveStateRoot`.
45
+ * Whether `targetPath` lives inside `baseDir` (same path counts). Used to ask "did an override move
46
+ * this OUT of the agent?" the startup report's redeploy notes, `add`'s printed `.env` label, and the
47
+ * dev watcher's "your .env is not watched" warning all turn on that fact. Reporting only: fastagent
48
+ * does not act on where a user's paths point.
50
49
  */
51
50
  export declare function isUnderDir(targetPath: string, baseDir: string): boolean;
52
- /**
53
- * The single owner of the self-ignore MECHANISM: iff the resolved state ROOT lands inside the workspace
54
- * tree, write `<stateRoot>/.gitignore="*"` — which then covers EVERYTHING under it (sessions, auth.json,
55
- * every channel's `channels/<kind>` home). `ensureStateDirSelfIgnored` is private, so a caller cannot
56
- * write a `.gitignore` bypassing this.
57
- *
58
- * ROOT-based, not path-based: everything derives from the state root (config.ts), so protecting the
59
- * root protects all of it — INCLUDING a custom in-tree root (a `FASTAGENT_STATE_DIR` inside the agent
60
- * dir), the case a path-based (`.fastagent`-only) guard would leak. A per-path override
61
- * (`--sessions-dir`/`--auth-path`) is operator-owned: pointed at an external volume it is out-of-tree
62
- * (correctly not ours to ignore); pointed at a custom in-tree dir WE DON'T OWN, we do not write a
63
- * `.gitignore` into it (it may be a directory the operator deliberately tracks).
64
- *
65
- * Excludes the user's HOME-global `~/.fastagent` (e.g. `login`/`dev` run from `$HOME`): self-ignore is
66
- * for protecting state inside an agent PROJECT tree, not for writing a `.gitignore` into the user's
67
- * home, which a dotfiles repo may track. The global credential file there was never self-ignored.
68
- */
69
- export declare function ensureStateRootSelfIgnored(dir: string, stateRoot: string): Promise<void>;
70
51
  /** Resolve to a canonical (symlink-free) absolute path so comparisons match `process.cwd()`'s realpath.
71
52
  * A non-existent path can't be realpath'd, so it stays as the plain absolute resolve. */
72
53
  export declare function canonicalPath(p: string): string;
@@ -14,12 +14,11 @@
14
14
  * (bad skill files, name collisions) are returned as data. An unreadable ② context file only warns (pi).
15
15
  */
16
16
  import { realpathSync } from "node:fs";
17
- import { mkdir, writeFile } from "node:fs/promises";
18
- import { homedir } from "node:os";
19
17
  import { isAbsolute, join, relative, resolve } from "node:path";
20
18
  import { loadSkills } from "@earendil-works/pi-agent-core";
21
19
  import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
22
20
  import { loadProjectContextFiles } from "@earendil-works/pi-coding-agent";
21
+ import { assertInsideAgentDir } from "../../paths.js";
23
22
  /** Read an agent definition. persona.md/skills come from `agentDir`; ② context = pi's loadProjectContextFiles({ cwd, agentDir }). */
24
23
  export async function loadAgentDefinition(agentDir, options = {}) {
25
24
  // One resolved default for the working directory (env cwd AND the context-walk start), so they can
@@ -44,7 +43,9 @@ export async function loadAgentDefinition(agentDir, options = {}) {
44
43
  }
45
44
  const persona = personaRead.ok ? personaRead.value : undefined;
46
45
  // Skills come ONLY from the definition's own skills/ (no external/global mount), so the same
47
- // definition loads the same skills on every machine.
46
+ // definition loads the same skills on every machine — and, like tools/channels/schedules, a symlink
47
+ // that escapes the agent dir is refused rather than followed (the fourth of four surfaces).
48
+ await assertInsideAgentDir(root, "skills");
48
49
  const { skills: raw, diagnostics } = await loadSkills(e, [join(root, "skills")]);
49
50
  const byName = new Map();
50
51
  const collisions = [];
@@ -60,62 +61,15 @@ export async function loadAgentDefinition(agentDir, options = {}) {
60
61
  return { contextFiles, persona, skills: [...byName.values()], diagnostics, collisions, dir: root };
61
62
  }
62
63
  /**
63
- * Whether `targetPath` lives inside `baseDir` (same path counts). The self-ignore guard uses it to ask
64
- * "does the resolved state root land inside the workspace tree?" an in-tree root (the default
65
- * `.fastagent`, or a custom `FASTAGENT_STATE_DIR` pointed inside the agent dir) is ours to self-ignore;
66
- * a root on an external volume resolves outside and must not be (we never write a `.gitignore` outside
67
- * the tree). Whether a relative override lands in-tree is a cwd question — see `resolveStateRoot`.
64
+ * Whether `targetPath` lives inside `baseDir` (same path counts). Used to ask "did an override move
65
+ * this OUT of the agent?" the startup report's redeploy notes, `add`'s printed `.env` label, and the
66
+ * dev watcher's "your .env is not watched" warning all turn on that fact. Reporting only: fastagent
67
+ * does not act on where a user's paths point.
68
68
  */
69
69
  export function isUnderDir(targetPath, baseDir) {
70
70
  const rel = relative(baseDir, targetPath);
71
71
  return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
72
72
  }
73
- /**
74
- * Self-ignore a state dir: create it if missing, then write `<stateDir>/.gitignore` = "*" (idempotent
75
- * — an existing one is kept), so a workspace that runs dev/start never shows machine state as
76
- * untracked. Creates the dir because a caller may self-ignore it before anything else populates it
77
- * (e.g. `login` writing auth.json into a not-yet-created root).
78
- *
79
- * Module-PRIVATE on purpose: the only entry to the leak guard is {@link ensureStateRootSelfIgnored}
80
- * (home exclusion + containment). Keeping this unexported makes that single-owner claim hold at the
81
- * type level — a sibling command can't bypass those checks by writing a `.gitignore` directly.
82
- */
83
- async function ensureStateDirSelfIgnored(stateDir) {
84
- await mkdir(stateDir, { recursive: true });
85
- await writeFile(join(stateDir, ".gitignore"), "*\n", { flag: "wx" }).catch((e) => {
86
- if (e.code !== "EEXIST")
87
- throw e;
88
- });
89
- }
90
- /**
91
- * The single owner of the self-ignore MECHANISM: iff the resolved state ROOT lands inside the workspace
92
- * tree, write `<stateRoot>/.gitignore="*"` — which then covers EVERYTHING under it (sessions, auth.json,
93
- * every channel's `channels/<kind>` home). `ensureStateDirSelfIgnored` is private, so a caller cannot
94
- * write a `.gitignore` bypassing this.
95
- *
96
- * ROOT-based, not path-based: everything derives from the state root (config.ts), so protecting the
97
- * root protects all of it — INCLUDING a custom in-tree root (a `FASTAGENT_STATE_DIR` inside the agent
98
- * dir), the case a path-based (`.fastagent`-only) guard would leak. A per-path override
99
- * (`--sessions-dir`/`--auth-path`) is operator-owned: pointed at an external volume it is out-of-tree
100
- * (correctly not ours to ignore); pointed at a custom in-tree dir WE DON'T OWN, we do not write a
101
- * `.gitignore` into it (it may be a directory the operator deliberately tracks).
102
- *
103
- * Excludes the user's HOME-global `~/.fastagent` (e.g. `login`/`dev` run from `$HOME`): self-ignore is
104
- * for protecting state inside an agent PROJECT tree, not for writing a `.gitignore` into the user's
105
- * home, which a dotfiles repo may track. The global credential file there was never self-ignored.
106
- */
107
- export async function ensureStateRootSelfIgnored(dir, stateRoot) {
108
- // Compare CANONICAL paths for the home check: `dir` arrives realpath-resolved (it is `process.cwd()`
109
- // or `resolve(".")`) but `homedir()` returns the raw `$HOME`, so a symlinked home would slip past raw
110
- // equality and we'd write a `.gitignore` into the real `~/.fastagent` — the very thing the doc forbids
111
- // (chat.ts canonicalizes for the same reason).
112
- if (canonicalPath(dir) === canonicalPath(homedir()))
113
- return;
114
- // Containment on RAW paths: stateRoot is resolve()'d (config.ts) and `dir` is absolute, so it is exact
115
- // by construction. An external-volume root resolves outside the tree → skip (not ours to ignore).
116
- if (isUnderDir(stateRoot, dir))
117
- await ensureStateDirSelfIgnored(stateRoot);
118
- }
119
73
  /** Resolve to a canonical (symlink-free) absolute path so comparisons match `process.cwd()`'s realpath.
120
74
  * A non-existent path can't be realpath'd, so it stays as the plain absolute resolve. */
121
75
  export function canonicalPath(p) {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `fastagent login`: authenticate a MODEL PROVIDER into the resolved auth file (project-level
3
- * `<dir>/.fastagent/auth.json` by default, or `--auth-path`/`FASTAGENT_AUTH_PATH`) via the same
3
+ * `<root>/.secrets/auth.json` by default, or `--auth-path`/`FASTAGENT_AUTH_PATH`) via the same
4
4
  * {@link fastagentCredentialStore} the runtime uses (one writer, one lock/corruption semantics).
5
5
  *
6
6
  * Flow (pi-ai's unified `ProviderAuth` API): pick a method (OAuth or API key), then a provider that
@@ -9,8 +9,8 @@ import { ModelRuntime } from "@earendil-works/pi-coding-agent";
9
9
  import { type FastagentAuthOptions } from "./auth.ts";
10
10
  import { type InteractiveLoginKind } from "./login.ts";
11
11
  export interface CreatePiModelsOptions extends FastagentAuthOptions {
12
- /** Credentials file path. Defaults to the global `~/.fastagent/auth.json`; the directory opener passes
13
- * the project-level `<dir>/.fastagent/auth.json`. */
12
+ /** Credentials file path. Defaults to the global `~/.fastagent/.secrets/auth.json`; the directory opener passes
13
+ * the project-level `<root>/.secrets/auth.json`. */
14
14
  authPath?: string;
15
15
  /** Extra providers registered on top of the built-ins (same id overrides a built-in). */
16
16
  providers?: Provider[];
@@ -18,7 +18,7 @@ export interface CreatePiModelsOptions extends FastagentAuthOptions {
18
18
  /**
19
19
  * A `Models` with every built-in pi provider, wired to fastagent's auth: stored credentials from the
20
20
  * {@link CreatePiModelsOptions.authPath} file (via {@link fastagentCredentialStore}; the global
21
- * `~/.fastagent/auth.json` unless the opener passes a project-level path), then ambient env vars. A
21
+ * `~/.fastagent/.secrets/auth.json` unless the opener passes a project-level path), then ambient env vars. A
22
22
  * stored credential owns the provider; env is consulted only when nothing is stored (resolution order
23
23
  * is upstream-owned).
24
24
  */
@@ -12,7 +12,7 @@ import { interactiveLoginKind } from "./login.js";
12
12
  /**
13
13
  * A `Models` with every built-in pi provider, wired to fastagent's auth: stored credentials from the
14
14
  * {@link CreatePiModelsOptions.authPath} file (via {@link fastagentCredentialStore}; the global
15
- * `~/.fastagent/auth.json` unless the opener passes a project-level path), then ambient env vars. A
15
+ * `~/.fastagent/.secrets/auth.json` unless the opener passes a project-level path), then ambient env vars. A
16
16
  * stored credential owns the provider; env is consulted only when nothing is stored (resolution order
17
17
  * is upstream-owned).
18
18
  */
@@ -5,21 +5,21 @@ import type { SessionControl } from "../../session.ts";
5
5
  import type { SessionObserver } from "./invoke.ts";
6
6
  import type { PiSessionReader, PiSessionStore } from "./sessions.ts";
7
7
  import type { ModuleLoadFailure } from "../../loader.ts";
8
- import { type LoadedDefinition } from "./definition.ts";
8
+ import type { LoadedDefinition } from "./definition.ts";
9
9
  import type { ToolCollision } from "./tool.ts";
10
- export interface CreatePiAgentFromWorkspaceOptions {
10
+ export interface CreatePiAgentFromDirOptions {
11
11
  /** Model spec override (e.g. the CLI --model flag). Precedence: this > FASTAGENT_MODEL > config.model. */
12
12
  model?: string;
13
13
  /**
14
- * Session store directory. Default `<dir>/.fastagent/sessions` (gitignored machine state). `start`
14
+ * Session store directory. Default `<agentDir>/.state/sessions` (machine state). `start`
15
15
  * overrides it (--sessions-dir / FASTAGENT_SESSIONS_DIR / a mounted volume) so production continuity
16
16
  * survives redeploys.
17
17
  */
18
18
  sessionsDir?: string;
19
19
  /**
20
- * Credentials file override. Default `<dir>/.fastagent/auth.json` (project-level, gitignored under
21
- * the same `*`-ignored `.fastagent`). Override via --auth-path / FASTAGENT_AUTH_PATH; point it at
22
- * `~/.fastagent/auth.json` to share one credential across projects.
20
+ * Credentials file override. Default `<agentDir>/.secrets/auth.json` (project-level, under the
21
+ * `.secrets/`). Override via --auth-path / FASTAGENT_AUTH_PATH; point it at
22
+ * the global `~/.fastagent/.secrets/auth.json` to share one credential across projects.
23
23
  */
24
24
  authPath?: string;
25
25
  /**
@@ -28,7 +28,7 @@ export interface CreatePiAgentFromWorkspaceOptions {
28
28
  * and never poll). The built-in `wake` tool mounts only when this is set AND `config.selfSchedule` is on.
29
29
  */
30
30
  serving?: boolean;
31
- /** Assemble the session control plane over this workspace's session store and return it as
31
+ /** Assemble the session control plane over this agent's session store and return it as
32
32
  * {@link sessionControl} — the store is created inside this opener, so the hub must be wired
33
33
  * here too (an external `createPiSessionControl` cannot exist before the store does).
34
34
  * Default: `config.sessionControl` AND {@link serving} — the config key means "serve the control
@@ -43,24 +43,27 @@ export interface CreatePiAgentFromWorkspaceOptions {
43
43
  observer?: SessionObserver;
44
44
  }
45
45
  /**
46
- * The workspace assembly FRONT HALF — everything that is independent of how pi consumes the
46
+ * The agent assembly FRONT HALF — everything that is independent of how pi consumes the
47
47
  * definition (transient harness for serving vs resident AgentSession for chat / session control):
48
- * config → model spec → agentDir → the full tool surface ({@link resolveWorkspaceTools} — the ONE
49
- * place it is computed) → state root → auth path. Both {@link createPiAgentFromWorkspace} and the
48
+ * placement resolution → config → model spec → the full tool surface ({@link resolveAgentTools} — the
49
+ * ONE place it is computed) → state root → auth path. Both {@link createPiAgentFromDir} and the
50
50
  * session builder (session-builder.ts) consume this, so THESE inputs cannot drift between the two
51
51
  * consumption shapes. (Definition loading and prompt assembly stay per-consumer: serving re-reads
52
52
  * them live per invoke, the session builder snapshots at startup and lets pi append skills/env.)
53
53
  */
54
- export interface WorkspaceAssembly {
54
+ export interface AgentAssembly {
55
55
  config: FastagentConfig;
56
56
  configPath?: string;
57
57
  /** The resolved "provider/modelId" spec in use. */
58
58
  modelSpec: string;
59
- /** Absolute agent-definition dir (config.agentDir resolved against dir; = dir when unset). */
59
+ /** Absolute agent dir definition + config + machinery live here (resolvePlacement().agentDir). */
60
60
  agentDir: string;
61
- /** Absolute state root (FASTAGENT_STATE_DIR > <dir>/.fastagent). */
61
+ /** Absolute workspace the agent's cwd and the start of the ②-context walk: the agent dir's parent
62
+ * when the agent sits inside it, the agent dir ITSELF when you point at the agent. */
63
+ workspace: string;
64
+ /** Absolute state root (FASTAGENT_STATE_DIR > <agentDir>/.state). */
62
65
  stateRoot: string;
63
- /** Absolute credentials file (--auth-path/authPath option > FASTAGENT_AUTH_PATH > <stateRoot>/auth.json). */
66
+ /** Absolute credentials file (--auth-path/authPath option > FASTAGENT_AUTH_PATH > <agentDir>/.secrets/auth.json). */
64
67
  authPath: string;
65
68
  /** The full mounted tool surface (config.tools + discovered tools/, search_tools applied). */
66
69
  tools: AgentTool[];
@@ -69,25 +72,28 @@ export interface WorkspaceAssembly {
69
72
  toolCollisions: ToolCollision[];
70
73
  toolFailures: ModuleLoadFailure[];
71
74
  }
72
- export declare function resolveWorkspaceAssembly(dir: string, options?: {
75
+ export declare function resolveAgentAssembly(dir: string, options?: {
73
76
  model?: string;
74
77
  authPath?: string;
75
- }): Promise<WorkspaceAssembly>;
78
+ }): Promise<AgentAssembly>;
76
79
  /**
77
- * "Point at a workspace → agent": load the config, resolve model and tools, then L2. Throws a clear
78
- * error when no model source is set (fail visibly at startup). Returns everything an entry point needs
79
- * to report what it assembled.
80
+ * "Point at a directory → agent": resolve the placement (`dir` may be either end the workspace or
81
+ * the agent dir itself), load the config, resolve model and tools, then L2. Throws a clear error when
82
+ * no model source is set (fail visibly at startup). Returns everything an entry point needs to report
83
+ * what it assembled.
80
84
  */
81
- export declare function createPiAgentFromWorkspace(dir: string, options?: CreatePiAgentFromWorkspaceOptions): Promise<{
85
+ export declare function createPiAgentFromDir(dir: string, options?: CreatePiAgentFromDirOptions): Promise<{
82
86
  agent: Agent;
83
87
  definition: LoadedDefinition;
84
88
  config: FastagentConfig;
85
89
  configPath?: string;
86
90
  /** The resolved "provider/modelId" spec actually in use. */
87
91
  modelSpec: string;
88
- /** Absolute agent-definition dir in use (config.agentDir resolved against dir; = dir when unset). Channels/tools/persona come from here. */
92
+ /** Absolute agent dir in use channels/tools/persona come from here. */
89
93
  agentDir: string;
90
- /** Absolute state root in use (FASTAGENT_STATE_DIR > <dir>/.fastagent) — the ChannelContext's stateRoot. */
94
+ /** Absolute workspace in use — the agent's cwd: ALWAYS the directory that was pointed at. */
95
+ workspace: string;
96
+ /** Absolute state root in use (FASTAGENT_STATE_DIR > <agentDir>/.state) — the ChannelContext's stateRoot. */
91
97
  stateRoot: string;
92
98
  /** Absolute session store directory in use (for the startup report). */
93
99
  sessionsDir: string;
@@ -95,7 +101,7 @@ export declare function createPiAgentFromWorkspace(dir: string, options?: Create
95
101
  authPath: string;
96
102
  /** The session store in use — also a {@link PiSessionReader}. */
97
103
  sessions: PiSessionStore & PiSessionReader;
98
- /** The observation plane over this workspace's sessions; present iff `options.sessionControl`. */
104
+ /** The observation plane over this agent's sessions; present iff `options.sessionControl`. */
99
105
  sessionControl?: SessionControl;
100
106
  /** Non-default, active-by-default tool names in effect: config.tools + discovered tools/. Each name
101
107
  * lives in exactly one report slot — deferred names are in {@link deferredToolNames} instead. */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Open a definition directory into an agent — the single workspace opener BOTH `fastagent dev` and
2
+ * Open a definition directory into an agent — the single agent opener BOTH `fastagent dev` and
3
3
  * `fastagent start` drive.
4
4
  *
5
5
  * A thin command-posture composition over L2 `createPiAgentFromDefinition`: open the directory →
@@ -9,39 +9,35 @@
9
9
  * watch and can point sessions at a mounted volume.
10
10
  */
11
11
  import { mkdir } from "node:fs/promises";
12
- import { defaultSessionsDir, loadConfig, resolveAgentDir, resolveAuthPath, resolveModelSpec, resolveStateRoot, } from "./config.js";
13
- import { createPiAgentFromDefinition, resolveWorkspaceTools } from "./create.js";
12
+ import { defaultSessionsDir, loadConfig, resolveAuthPath, resolveModelSpec, } from "./config.js";
13
+ import { resolveStateRoot, resolvePlacement } from "../../paths.js";
14
+ import { createPiAgentFromDefinition, resolveAgentTools } from "./create.js";
14
15
  import { createPiSessionControl } from "./session-control.js";
15
16
  import { withWakeTool } from "./wake-tool.js";
16
- import { ensureStateRootSelfIgnored } from "./definition.js";
17
17
  import { jsonlSessionStore } from "./sessions.js";
18
- export async function resolveWorkspaceAssembly(dir, options = {}) {
19
- const { config, path: configPath } = await loadConfig(dir);
18
+ export async function resolveAgentAssembly(dir, options = {}) {
19
+ // Placement is structural (resolvePlacement): the AGENT DIR carries definition + config + machinery;
20
+ // its parent — the WORKSPACE — is what the agent works on: its cwd and the start of the ②-context
21
+ // walk (that is where it reads the project's AGENTS.md from).
22
+ const { agentDir, workspace } = resolvePlacement(dir);
23
+ const { config, path: configPath } = await loadConfig(agentDir);
20
24
  const modelSpec = resolveModelSpec(options.model, config);
21
25
  if (!modelSpec) {
22
26
  throw new Error(`missing model: set --model, "model" in fastagent.config.ts, or FASTAGENT_MODEL (e.g. "openai-codex/gpt-5.5")`);
23
27
  }
24
- // The run root is `dir` (cwd where config lives, whose AGENTS.md is ② context); the agent's own
25
- // surface (persona/skills/tools/channels) lives in `agentDir` (config.agentDir, or `dir` when flat).
26
- const agentDir = resolveAgentDir(dir, config);
27
- const { tools, toolNames, deferredToolNames, toolCollisions, toolFailures } = await resolveWorkspaceTools(config, agentDir, dir);
28
- // The state root: auth/sessions/channel state all derive from it, so FASTAGENT_STATE_DIR moves the
29
- // whole machine-state home in one knob (a container mounts one volume); the finer overrides below
30
- // still win for their specific path.
31
- const stateRoot = resolveStateRoot(dir);
32
- // The credentials file: project-level by default (under the state root); only resolved here, never
28
+ const { tools, toolNames, deferredToolNames, toolCollisions, toolFailures } = await resolveAgentTools(config, agentDir, workspace);
29
+ // The state root: sessions/channel state/schedule state derive from it (FASTAGENT_STATE_DIR moves it
30
+ // in one knob — a container points it at its volume); the finer overrides below still win.
31
+ const stateRoot = resolveStateRoot(agentDir);
32
+ // The credentials file: project-level by default (under `<agentDir>/.secrets`); only resolved here, never
33
33
  // created (a missing file reads as not-configured — `fastagent login` creates it).
34
- const authPath = resolveAuthPath(dir, options.authPath);
35
- // Self-ignore the state root iff it lands in-tree — which covers everything under it (sessions, auth,
36
- // every channel's `channels/<kind>` home). HERE, not in the serving opener: every consumer of this
37
- // assembly can WRITE under the state root (serving: sessions/channels; the session builder: pi's
38
- // `/login` writing auth.json), so resolving a workspace's state root must make it leak-safe.
39
- await ensureStateRootSelfIgnored(dir, stateRoot);
34
+ const authPath = resolveAuthPath(agentDir, options.authPath);
40
35
  return {
41
36
  config,
42
37
  configPath,
43
38
  modelSpec,
44
39
  agentDir,
40
+ workspace,
45
41
  stateRoot,
46
42
  authPath,
47
43
  tools,
@@ -52,18 +48,19 @@ export async function resolveWorkspaceAssembly(dir, options = {}) {
52
48
  };
53
49
  }
54
50
  /**
55
- * "Point at a workspace → agent": load the config, resolve model and tools, then L2. Throws a clear
56
- * error when no model source is set (fail visibly at startup). Returns everything an entry point needs
57
- * to report what it assembled.
51
+ * "Point at a directory → agent": resolve the placement (`dir` may be either end the workspace or
52
+ * the agent dir itself), load the config, resolve model and tools, then L2. Throws a clear error when
53
+ * no model source is set (fail visibly at startup). Returns everything an entry point needs to report
54
+ * what it assembled.
58
55
  */
59
- export async function createPiAgentFromWorkspace(dir, options = {}) {
60
- const { config, configPath, modelSpec, agentDir, stateRoot, authPath, tools, toolNames, deferredToolNames, toolCollisions, toolFailures, } = await resolveWorkspaceAssembly(dir, options);
56
+ export async function createPiAgentFromDir(dir, options = {}) {
57
+ const { config, configPath, modelSpec, agentDir, workspace, stateRoot, authPath, tools, toolNames, deferredToolNames, toolCollisions, toolFailures, } = await resolveAgentAssembly(dir, options);
61
58
  // Mount the built-in `wake` tool only when BOTH: this is a long-running serve (the poller honors it) AND
62
- // the author opted into self-scheduling (config.selfSchedule). The workspace's own `wake` wins if defined.
59
+ // the author opted into self-scheduling (config.selfSchedule). The agent's own `wake` wins if defined.
63
60
  const mountedTools = withWakeTool(tools, stateRoot, !!options.serving && !!config.selfSchedule);
64
61
  const sessionsDir = options.sessionsDir ?? defaultSessionsDir(stateRoot);
65
62
  await mkdir(sessionsDir, { recursive: true });
66
- const sessions = jsonlSessionStore({ dir: sessionsDir, cwd: dir });
63
+ const sessions = jsonlSessionStore({ dir: sessionsDir, cwd: workspace });
67
64
  // The hub is wired HERE because the store is created here: chicken-and-egg otherwise (the hub
68
65
  // needs the store; the agent needs the hub's observer). Boundary parts (models/factory/lease)
69
66
  // only exist after the assembly below — the hub takes them as a lazy thunk, filled by the
@@ -93,7 +90,7 @@ export async function createPiAgentFromWorkspace(dir, options = {}) {
93
90
  const { agent, definition } = await createPiAgentFromDefinition(agentDir, {
94
91
  model: modelSpec,
95
92
  thinkingLevel: config.thinkingLevel,
96
- cwd: dir,
93
+ cwd: workspace,
97
94
  tools: mountedTools,
98
95
  authPath,
99
96
  // Skills are definition-only (the agent is its directory), so dev mirrors deployment exactly.
@@ -115,6 +112,7 @@ export async function createPiAgentFromWorkspace(dir, options = {}) {
115
112
  sessions,
116
113
  sessionControl: hub?.control,
117
114
  agentDir,
115
+ workspace,
118
116
  config,
119
117
  configPath,
120
118
  modelSpec,
@@ -3,7 +3,7 @@ export interface BuildSessionRuntimeOptions {
3
3
  /** Model spec override (the CLI --model flag). Precedence: this > FASTAGENT_MODEL > config.model. */
4
4
  model?: string;
5
5
  /** Credentials file override (the CLI --auth-path flag). Precedence: this > FASTAGENT_AUTH_PATH >
6
- * the workspace default `<stateRoot>/auth.json`. */
6
+ * the agent's default `<agentDir>/.secrets/auth.json`. */
7
7
  authPath?: string;
8
8
  }
9
9
  /**
@@ -11,6 +11,6 @@ export interface BuildSessionRuntimeOptions {
11
11
  * skills, and auth resolved exactly as the serving opener does). Split from the TUI launcher so the
12
12
  * assembly — the fidelity-critical part — is inspectable and reusable without launching a TUI.
13
13
  */
14
- export declare function buildWorkspaceSessionRuntime(dir: string, options?: BuildSessionRuntimeOptions,
14
+ export declare function buildAgentSessionRuntime(dir: string, options?: BuildSessionRuntimeOptions,
15
15
  /** Session backend. Defaults to pi's project-scoped store; tests inject SessionManager.inMemory(). */
16
16
  sessionManager?: SessionManager): Promise<AgentSessionRuntime>;