@fastagent-sh/fastagent 0.14.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 (277) hide show
  1. package/README.md +16 -6
  2. package/dist/agent.d.ts +20 -0
  3. package/dist/agent.js +9 -0
  4. package/dist/channels/agentcore-limits.d.ts +7 -0
  5. package/dist/channels/agentcore-limits.js +9 -0
  6. package/dist/channels/agentcore-state.d.ts +83 -0
  7. package/dist/channels/agentcore-state.js +258 -0
  8. package/dist/channels/agentcore.d.ts +98 -0
  9. package/dist/channels/agentcore.js +260 -0
  10. package/dist/channels/busy.d.ts +23 -0
  11. package/dist/channels/busy.js +53 -0
  12. package/dist/channels/context-buffer.d.ts +35 -0
  13. package/dist/channels/context-buffer.js +103 -0
  14. package/dist/channels/control.d.ts +28 -0
  15. package/dist/channels/control.js +214 -0
  16. package/dist/channels/feishu/card.js +1 -1
  17. package/dist/channels/feishu/context-buffer.d.ts +15 -13
  18. package/dist/channels/feishu/context-buffer.js +62 -69
  19. package/dist/channels/feishu/feishu-api.d.ts +11 -3
  20. package/dist/channels/feishu/feishu-api.js +24 -3
  21. package/dist/channels/feishu/feishu.d.ts +27 -27
  22. package/dist/channels/feishu/feishu.js +318 -207
  23. package/dist/channels/feishu/invoke-turn.d.ts +7 -20
  24. package/dist/channels/feishu/invoke-turn.js +44 -79
  25. package/dist/channels/feishu/model.d.ts +2 -2
  26. package/dist/channels/feishu/normalize.d.ts +2 -1
  27. package/dist/channels/feishu/normalize.js +0 -1
  28. package/dist/channels/feishu/parse.d.ts +22 -8
  29. package/dist/channels/feishu/parse.js +24 -7
  30. package/dist/channels/feishu/preview.d.ts +4 -7
  31. package/dist/channels/feishu/preview.js +27 -142
  32. package/dist/channels/feishu/register-app.d.ts +2 -1
  33. package/dist/channels/feishu/register-webhook.d.ts +2 -1
  34. package/dist/channels/feishu/scaffold/channel.ts +9 -8
  35. package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
  36. package/dist/channels/feishu/setup-mode.d.ts +37 -0
  37. package/dist/channels/feishu/setup-mode.js +28 -0
  38. package/dist/channels/feishu/ws-ingress.d.ts +28 -0
  39. package/dist/channels/feishu/ws-ingress.js +136 -0
  40. package/dist/channels/github/github.js +16 -7
  41. package/dist/channels/http.d.ts +14 -0
  42. package/dist/channels/http.js +36 -3
  43. package/dist/channels/invoke-turn-kit.d.ts +65 -0
  44. package/dist/channels/invoke-turn-kit.js +87 -0
  45. package/dist/channels/lark/lark.d.ts +4 -2
  46. package/dist/channels/lark/lark.js +4 -1
  47. package/dist/channels/lark/onboard.d.ts +8 -4
  48. package/dist/channels/lark/onboard.js +8 -0
  49. package/dist/channels/lark/scaffold/channel.ts +9 -8
  50. package/dist/channels/lark/scaffold/lark-send.ts +6 -4
  51. package/dist/channels/preview-kit.d.ts +115 -0
  52. package/dist/channels/preview-kit.js +184 -0
  53. package/dist/channels/seen.d.ts +5 -0
  54. package/dist/channels/seen.js +35 -0
  55. package/dist/channels/slack/bot-auth.d.ts +15 -0
  56. package/dist/channels/slack/bot-auth.js +146 -0
  57. package/dist/channels/slack/config-api.d.ts +60 -0
  58. package/dist/channels/slack/config-api.js +149 -0
  59. package/dist/channels/slack/context-buffer.d.ts +24 -0
  60. package/dist/channels/slack/context-buffer.js +37 -0
  61. package/dist/channels/slack/invoke-turn.d.ts +19 -0
  62. package/dist/channels/slack/invoke-turn.js +63 -0
  63. package/dist/channels/slack/manifest.d.ts +49 -0
  64. package/dist/channels/slack/manifest.js +69 -0
  65. package/dist/channels/slack/model.d.ts +67 -0
  66. package/dist/channels/slack/model.js +2 -0
  67. package/dist/channels/slack/onboard.d.ts +41 -0
  68. package/dist/channels/slack/onboard.js +120 -0
  69. package/dist/channels/slack/onboarding-state.d.ts +31 -0
  70. package/dist/channels/slack/onboarding-state.js +69 -0
  71. package/dist/channels/slack/parse.d.ts +38 -0
  72. package/dist/channels/slack/parse.js +124 -0
  73. package/dist/channels/slack/preview.d.ts +23 -0
  74. package/dist/channels/slack/preview.js +403 -0
  75. package/dist/channels/slack/reaction.d.ts +24 -0
  76. package/dist/channels/slack/reaction.js +62 -0
  77. package/dist/channels/slack/register-webhook.d.ts +10 -0
  78. package/dist/channels/slack/register-webhook.js +49 -0
  79. package/dist/channels/slack/scaffold/channel.ts +33 -0
  80. package/dist/channels/slack/scaffold/slack-send.ts +171 -0
  81. package/dist/channels/slack/setup-server.d.ts +17 -0
  82. package/dist/channels/slack/setup-server.js +103 -0
  83. package/dist/channels/slack/slack-api.d.ts +57 -0
  84. package/dist/channels/slack/slack-api.js +399 -0
  85. package/dist/channels/slack/slack.d.ts +51 -0
  86. package/dist/channels/slack/slack.js +496 -0
  87. package/dist/channels/slack/welcomed.d.ts +5 -0
  88. package/dist/channels/slack/welcomed.js +32 -0
  89. package/dist/channels/state.d.ts +11 -4
  90. package/dist/channels/state.js +22 -15
  91. package/dist/channels/stop-command.d.ts +6 -0
  92. package/dist/channels/stop-command.js +36 -0
  93. package/dist/channels/tasks.d.ts +7 -0
  94. package/dist/channels/tasks.js +25 -0
  95. package/dist/channels/telegram/context-buffer.d.ts +8 -17
  96. package/dist/channels/telegram/context-buffer.js +6 -85
  97. package/dist/channels/telegram/invoke-turn.d.ts +5 -22
  98. package/dist/channels/telegram/invoke-turn.js +11 -58
  99. package/dist/channels/telegram/parse.d.ts +0 -7
  100. package/dist/channels/telegram/parse.js +4 -2
  101. package/dist/channels/telegram/preview.d.ts +4 -7
  102. package/dist/channels/telegram/preview.js +24 -142
  103. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
  104. package/dist/channels/telegram/telegram.js +24 -10
  105. package/dist/channels/{feishu/text.d.ts → text.d.ts} +15 -1
  106. package/dist/channels/{feishu/text.js → text.js} +15 -1
  107. package/dist/channels/thread-participants.d.ts +21 -0
  108. package/dist/channels/thread-participants.js +132 -0
  109. package/dist/channels/turn-queue.js +8 -1
  110. package/dist/channels/turn-store.d.ts +1 -1
  111. package/dist/channels/turn-store.js +2 -3
  112. package/dist/cli/add-feishu.d.ts +30 -0
  113. package/dist/cli/add-feishu.js +366 -0
  114. package/dist/cli/add-slack.d.ts +11 -0
  115. package/dist/cli/add-slack.js +199 -0
  116. package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
  117. package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
  118. package/dist/cli/commands/add.d.ts +4 -0
  119. package/dist/cli/commands/add.js +191 -60
  120. package/dist/cli/commands/attach.d.ts +82 -0
  121. package/dist/cli/commands/attach.js +563 -0
  122. package/dist/cli/commands/chat.d.ts +1 -0
  123. package/dist/cli/commands/chat.js +12 -7
  124. package/dist/cli/commands/deploy.d.ts +3 -1
  125. package/dist/cli/commands/deploy.js +384 -82
  126. package/dist/cli/commands/dev.js +34 -26
  127. package/dist/cli/commands/fire.js +17 -18
  128. package/dist/cli/commands/info.js +36 -29
  129. package/dist/cli/commands/init.d.ts +1 -1
  130. package/dist/cli/commands/init.js +65 -53
  131. package/dist/cli/commands/invoke.js +11 -9
  132. package/dist/cli/commands/login.js +48 -45
  133. package/dist/cli/commands/models.js +1 -1
  134. package/dist/cli/commands/schedule.js +6 -8
  135. package/dist/cli/commands/start.js +102 -39
  136. package/dist/cli/commands/tool.js +23 -18
  137. package/dist/cli/fail.d.ts +17 -2
  138. package/dist/cli/fail.js +25 -1
  139. package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
  140. package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
  141. package/dist/cli/kernel.d.ts +3 -2
  142. package/dist/cli/models-view.d.ts +21 -0
  143. package/dist/cli/models-view.js +66 -0
  144. package/dist/cli/program.js +135 -56
  145. package/dist/cli/serve.d.ts +60 -20
  146. package/dist/cli/serve.js +252 -47
  147. package/dist/cli/shared.d.ts +40 -7
  148. package/dist/cli/shared.js +180 -38
  149. package/dist/collect.d.ts +14 -3
  150. package/dist/collect.js +24 -0
  151. package/dist/core.d.ts +3 -1
  152. package/dist/core.js +2 -0
  153. package/dist/deploy/agentcore/plan.d.ts +117 -0
  154. package/dist/deploy/agentcore/plan.js +721 -0
  155. package/dist/deploy/agentcore/run.d.ts +73 -0
  156. package/dist/deploy/agentcore/run.js +412 -0
  157. package/dist/deploy/agentcore/zip.d.ts +17 -0
  158. package/dist/deploy/agentcore/zip.js +68 -0
  159. package/dist/deploy/container.d.ts +26 -25
  160. package/dist/deploy/container.js +93 -89
  161. package/dist/deploy/docker/plan.d.ts +3 -3
  162. package/dist/deploy/docker/plan.js +20 -23
  163. package/dist/deploy/fly/plan.d.ts +5 -1
  164. package/dist/deploy/fly/plan.js +39 -26
  165. package/dist/deploy/fly/run.d.ts +17 -5
  166. package/dist/deploy/fly/run.js +52 -7
  167. package/dist/deploy/preflight.d.ts +20 -8
  168. package/dist/deploy/preflight.js +267 -71
  169. package/dist/deploy/railway/plan.d.ts +10 -1
  170. package/dist/deploy/railway/plan.js +53 -20
  171. package/dist/deploy/railway/run.d.ts +13 -5
  172. package/dist/deploy/railway/run.js +22 -7
  173. package/dist/deploy/runner.d.ts +7 -3
  174. package/dist/deploy/runner.js +9 -3
  175. package/dist/deploy/secrets.d.ts +2 -1
  176. package/dist/deploy/secrets.js +23 -3
  177. package/dist/dev-supervisor.d.ts +11 -10
  178. package/dist/dev-supervisor.js +56 -50
  179. package/dist/engines/pi/auth.d.ts +8 -7
  180. package/dist/engines/pi/auth.js +12 -10
  181. package/dist/engines/pi/channel.d.ts +22 -16
  182. package/dist/engines/pi/channel.js +92 -62
  183. package/dist/engines/pi/chat.d.ts +4 -16
  184. package/dist/engines/pi/chat.js +8 -261
  185. package/dist/engines/pi/config.d.ts +20 -55
  186. package/dist/engines/pi/config.js +49 -121
  187. package/dist/engines/pi/create.d.ts +30 -10
  188. package/dist/engines/pi/create.js +33 -26
  189. package/dist/engines/pi/definition.d.ts +7 -26
  190. package/dist/engines/pi/definition.js +8 -54
  191. package/dist/engines/pi/harness.d.ts +49 -0
  192. package/dist/engines/pi/harness.js +90 -2
  193. package/dist/engines/pi/invoke.d.ts +55 -2
  194. package/dist/engines/pi/invoke.js +304 -19
  195. package/dist/engines/pi/login.d.ts +12 -1
  196. package/dist/engines/pi/login.js +16 -4
  197. package/dist/engines/pi/models.d.ts +59 -13
  198. package/dist/engines/pi/models.js +62 -24
  199. package/dist/engines/pi/open.d.ts +114 -0
  200. package/dist/engines/pi/open.js +127 -0
  201. package/dist/engines/pi/session-builder.d.ts +16 -0
  202. package/dist/engines/pi/session-builder.js +308 -0
  203. package/dist/engines/pi/session-control.d.ts +50 -0
  204. package/dist/engines/pi/session-control.js +604 -0
  205. package/dist/engines/pi/sessions.d.ts +17 -2
  206. package/dist/engines/pi/sessions.js +9 -0
  207. package/dist/engines/pi/tool-context.d.ts +18 -11
  208. package/dist/engines/pi/tool-context.js +3 -9
  209. package/dist/engines/pi/tool.d.ts +6 -8
  210. package/dist/engines/pi/tool.js +10 -1
  211. package/dist/engines/pi/wake-tool.d.ts +0 -3
  212. package/dist/engines/pi/wake-tool.js +9 -7
  213. package/dist/env.d.ts +16 -4
  214. package/dist/env.js +43 -5
  215. package/dist/feishu.d.ts +1 -1
  216. package/dist/feishu.js +1 -1
  217. package/dist/host/node.d.ts +25 -8
  218. package/dist/host/node.js +5 -4
  219. package/dist/index.d.ts +1 -0
  220. package/dist/index.js +1 -0
  221. package/dist/lark.d.ts +1 -1
  222. package/dist/lark.js +1 -1
  223. package/dist/loader.d.ts +2 -2
  224. package/dist/loader.js +3 -3
  225. package/dist/log.d.ts +1 -1
  226. package/dist/log.js +1 -1
  227. package/dist/observe.js +3 -0
  228. package/dist/paths.d.ts +138 -0
  229. package/dist/paths.js +326 -0
  230. package/dist/pi.d.ts +6 -5
  231. package/dist/pi.js +4 -3
  232. package/dist/runtime.d.ts +7 -5
  233. package/dist/runtime.js +2 -2
  234. package/dist/scaffold/add-channel.d.ts +26 -9
  235. package/dist/scaffold/add-channel.js +175 -40
  236. package/dist/scaffold/init.d.ts +32 -41
  237. package/dist/scaffold/init.js +161 -185
  238. package/dist/scaffold/templates/env.example +15 -6
  239. package/dist/scaffold/templates/fastagent.config.mjs +5 -4
  240. package/dist/scaffold/templates/gitignore +14 -6
  241. package/dist/scaffold/templates/persona.md +4 -2
  242. package/dist/scaffold/templates/secrets.gitignore +5 -0
  243. package/dist/scaffold/templates.d.ts +1 -7
  244. package/dist/scaffold/templates.js +3 -25
  245. package/dist/scaffold/vendor-skill.d.ts +2 -2
  246. package/dist/scaffold/vendor-skill.js +13 -13
  247. package/dist/schedule/discover.js +4 -4
  248. package/dist/schedule/scheduler.d.ts +40 -1
  249. package/dist/schedule/scheduler.js +89 -56
  250. package/dist/schedule/state.js +1 -1
  251. package/dist/schedule/wake-alarm.d.ts +47 -0
  252. package/dist/schedule/wake-alarm.js +136 -0
  253. package/dist/schedule/wakeups.d.ts +1 -3
  254. package/dist/schedule/wakeups.js +19 -1
  255. package/dist/session-remote.d.ts +53 -0
  256. package/dist/session-remote.js +336 -0
  257. package/dist/session.d.ts +265 -0
  258. package/dist/session.js +37 -0
  259. package/dist/slack.d.ts +2 -0
  260. package/dist/slack.js +2 -0
  261. package/dist/tunnel.d.ts +7 -4
  262. package/dist/tunnel.js +23 -12
  263. package/package.json +22 -7
  264. package/dist/channels/feishu/owned-threads.d.ts +0 -11
  265. package/dist/channels/feishu/owned-threads.js +0 -47
  266. package/dist/channels/feishu/seen.d.ts +0 -5
  267. package/dist/channels/feishu/seen.js +0 -47
  268. package/dist/cli-add-feishu.d.ts +0 -8
  269. package/dist/cli-add-feishu.js +0 -223
  270. package/dist/cli-models.d.ts +0 -11
  271. package/dist/cli-models.js +0 -20
  272. package/dist/engines/pi/workspace.d.ts +0 -56
  273. package/dist/engines/pi/workspace.js +0 -72
  274. package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
  275. package/dist/scaffold/templates/gitignore.kit +0 -2
  276. package/dist/workspace.d.ts +0 -9
  277. package/dist/workspace.js +0 -45
@@ -1,161 +1,118 @@
1
1
  /**
2
- * Init: scaffold a runnable fastagent workspace, offline. Default = a COMPLETE agent (persona.md +
2
+ * Init: scaffold a runnable fastagent agent, offline. Default = a COMPLETE agent (persona.md +
3
3
  * the writing-great-skills skill + a fetch-url code tool + fastagent.config.mjs + package.json +
4
- * .gitignore); `--minimal` drops the code tool and package.json. persona.md is the agent's identity
5
- * (prompt segment ①); an existing AGENTS.md is never written or touched — it is project context (②),
6
- * kept as-is. skills/ and tools/ are the agent's self-editable capabilities (re-read each turn).
4
+ * .gitignore + .secrets/); `--minimal` drops the code tool and package.json. persona.md is the agent's
5
+ * identity (prompt segment ①); an existing AGENTS.md is never written or touched — it is project
6
+ * context (②), kept as-is. skills/ and tools/ are the agent's self-editable capabilities (re-read each
7
+ * turn).
7
8
  *
8
- * Layout (the jurisdiction rule core.md scenario grid): the agent kit goes FLAT into `dir` ("a
9
- * directory is an agent") UNLESS an existing system already CLAIMS the tree — a toolchain config that
10
- * sweeps files by pattern (tsconfig/framework configs), a deploy manifest (Dockerfile/fly/railway/…),
11
- * or fastagent's own convention names already occupied (non-empty tools//channels//skills/). Any such
12
- * signal the kit defaults into `./agent` (its own namespace; `config.agentDir` points there), so the
13
- * host's toolchain and the agent's surface never sweep each other. {@link detectHostSignals} detects;
14
- * the CLI decides (flags override) and reports the reason.
9
+ * Placement no detection and no prompt, just a default and one flag. By DEFAULT the whole agent —
10
+ * definition, config, `.secrets/`, machinery lands in `<dir>/fastagent/`; the surrounding tree gets
11
+ * ZERO writes and becomes the workspace the agent works on. `--agent-dir <name>` picks another name for
12
+ * that directory (`fastagent.config.*` is the marker of what IS an agent, never the name), and `--agent-dir .`
13
+ * (spelled `--flat`) lands the identical shape in `dir` itself, for the case where the directory IS the
14
+ * agent (a standalone agent repo, a monorepo package).
15
+ *
16
+ * Which of the two a served agent turns out to be is NOT decided here: the workspace is whatever
17
+ * fastagent is later pointed at (resolvePlacement). This module only chooses where the files land — so
18
+ * its one placement duty is to refuse a target that the lookup would not return (see below).
15
19
  *
16
20
  * Scope: init is best-effort atomic for ORDINARY inputs — it never overwrites existing files,
17
- * preflights non-directory scaffold parents, and rolls back a partial write (one exception: the
18
- * .gitignore APPEND is not rolled back idempotent, harmless residue). It does not defend against
21
+ * preflights non-directory scaffold parents, and rolls back a partial write (files AND the
22
+ * `fastagent/` tree it created for them, so a retry sees a clean slate). It does not defend against
19
23
  * every pathological target state (TOCTOU, FIFOs, disk-full): recover by delete-and-retry.
20
24
  *
21
25
  * Sibling scaffold modules: add-channel.ts (`add <channel>`), vendor-skill.ts (`add skill`). The files
22
26
  * this module writes are real templates under templates/, read through templates.ts.
23
27
  */
24
- import { access, appendFile, lstat, mkdir, readdir, rm, writeFile } from "node:fs/promises";
25
- import { dirname, join, relative } from "node:path";
26
- import { WORKSPACE_CONFIG_NAMES } from "../engines/pi/config.js";
27
- import { detectRuntime, readPackageJson } from "../runtime.js";
28
- import { loadRootIgnore } from "../workspace.js";
29
- import { baseTemplate, configTemplate, packageJson, personaTemplate, toPackageName } from "./templates.js";
28
+ import { lstat, mkdir, readdir, rm, rmdir, writeFile } from "node:fs/promises";
29
+ import { basename, dirname, join, resolve, sep } from "node:path";
30
+ import { AGENT_CONFIG_NAMES, DEFAULT_AGENT_DIRNAME, SECRETS_DIRNAME, agentDefinitionOwner, agentsAt, displayPath, exists, } from "../paths.js";
31
+ import { baseTemplate, packageJson, toPackageName } from "./templates.js";
30
32
  import { fastagentVersion } from "../version.js";
31
- /** Filename marks of a system that claims files by pattern (a build toolchain F2 in the jurisdiction
32
- * rule). Not JS-only: a Python/Go/Rust/JVM/Ruby/PHP project's build system claims its tree exactly the
33
- * same way — an AGENTS.md-carrying Go repo must not get a flat kit (with a package.json!) in its root.
34
- * Deliberately absent: `Makefile` (too generic — notes/dotfiles repos carry one without a toolchain). */
35
- const TOOLCHAIN_RE = /^(tsconfig\.json|(next|vite|astro|svelte|nuxt|remix|webpack|rollup)\.config\.[cm]?[jt]s|go\.mod|Cargo\.toml|pyproject\.toml|setup\.py|requirements\.txt|Gemfile|pom\.xml|build\.gradle(\.kts)?|composer\.json|CMakeLists\.txt)$/;
36
- /** Filename marks of a deploy manifest — the tree ships as a non-agent unit (F4). */
37
- const DEPLOY_RE = /^(Dockerfile|fly\.toml|railway\.toml|vercel\.json|netlify\.toml)$/;
38
- /**
39
- * Jurisdiction signals: evidence that an existing system already claims this tree, so a flat agent kit
40
- * would put each side's files under the other's jurisdiction (host tsc sweeps agent .ts; fastagent
41
- * scans host tools/). Three classes, derived from the actual failure modes — a toolchain config, a
42
- * deploy manifest, or fastagent's convention names already occupied. Any hit → the kit defaults into
43
- * `./agent`. Deliberately NOT signals: "dir is non-empty", "has package.json", "has src/" — markdown
44
- * and loose scripts are claimed by nobody, and "a directory is an agent" stays the default. Known
45
- * tradeoff, decided for visibility: a HAND-BUILT agent dir (skills//tools/ authored for the agent, no
46
- * config yet) also hits the occupation signal and defaults to ./agent — wrong for that case, but the
47
- * reason is printed and `--flat` overrides; the reverse default would silently mis-scan a host's dirs.
48
- */
49
- export async function detectHostSignals(dir) {
50
- // Only ENOENT/ENOTDIR mean "nothing there" (fresh dir → flat). A real IO failure (EACCES…) must
51
- // surface, not silently decide the layout — init is about to write into this directory anyway.
52
- const absent = (err) => {
53
- const code = err.code;
54
- if (code === "ENOENT" || code === "ENOTDIR")
55
- return [];
56
- throw err;
57
- };
58
- const signals = [];
59
- const entries = await readdir(dir, { withFileTypes: true }).catch(absent);
60
- for (const e of entries) {
61
- if (e.isFile() && (TOOLCHAIN_RE.test(e.name) || DEPLOY_RE.test(e.name)))
62
- signals.push(e.name);
63
- }
64
- for (const name of ["tools", "channels", "skills"]) {
65
- const st = await lstat(join(dir, name)).catch((err) => {
66
- absent(err);
67
- return undefined;
68
- });
69
- // Dotfiles (.DS_Store, .gitkeep) are not agent surface — the loaders would never scan them, so
70
- // they must not count as "occupied" either (the signal mirrors what fastagent would actually scan).
71
- const occupants = st?.isDirectory()
72
- ? (await readdir(join(dir, name)).catch(absent)).filter((f) => !f.startsWith("."))
73
- : [];
74
- if (occupants.length > 0)
75
- signals.push(`${name}/`);
76
- }
77
- return signals.sort();
33
+ /** The agent directory name for a raw `--agent-dir` value: the default when unset, and `./bot` read as
34
+ * `bot` `basename` already says that is what it means, so rejecting the spelling would be pedantry. */
35
+ export function agentDirName(raw) {
36
+ if (raw === undefined)
37
+ return DEFAULT_AGENT_DIRNAME;
38
+ const trimmed = raw.replace(/^\.[/\\]/, "");
39
+ return trimmed === "" ? raw : trimmed;
78
40
  }
79
- /** The `cd` target to show in `init`'s next-steps: the relative path when the target is inside `cwd`,
80
- * the absolute path when it climbs out (a `../../..` is noise), or undefined when already in `cwd`. */
81
- export function nextStepCd(cwd, dir) {
82
- const rel = relative(cwd, dir);
83
- if (rel === "")
41
+ /** Why `name` cannot be an agent directory name, or undefined when it can. It must stay ONE segment
42
+ * inside the target: anything else (a separator, `..`, an absolute path) would land the agent where the
43
+ * one-level lookup cannot see it — an agent nothing would ever serve.
44
+ *
45
+ * Returns the CONSTRAINT, not a sentence: the CLI prefixes the flag it owns and reports it as the usage
46
+ * error it is (exit 2), while {@link scaffoldAgent} prefixes the option name for a programmatic caller,
47
+ * who never passed a flag and should not be told to fix one. */
48
+ export function agentDirNameError(name) {
49
+ if (name === "." || (name !== "" && name !== ".." && name === basename(name)))
84
50
  return undefined;
85
- // "Climbs out" is a path-SEGMENT check rel is ".." or starts with "../" (or "..\" on Windows). A
86
- // bare startsWith("..") would wrongly flag an in-cwd directory literally named "..agent".
87
- const escapes = rel === ".." || /^\.\.[/\\]/.test(rel);
88
- return escapes ? dir : rel;
89
- }
90
- /** Does a path exist? (async; shared with the sibling scaffold modules). */
91
- export async function exists(p) {
92
- return access(p).then(() => true, () => false);
51
+ return (`must be a single directory name (or "." for the target itself) a path would put the agent outside ` +
52
+ `the target directory, where fastagent would not find it`);
93
53
  }
94
54
  /**
95
- * Scaffold a runnable workspace into {@link dir} (created if missing). Default is a complete agent
96
- * (persona.md + the writing-great-skills skill + a code tool + package.json); `--minimal` drops the
97
- * code tool and package.json. The kit goes flat into `dir`, or into {@link ScaffoldOptions.agentDir}
98
- * (config at the root pointing there). Refuses only an existing fastagent.config.* (the ownership
99
- * marker already a workspace); every other pre-existing file (AGENTS.md, .gitignore, package.json)
100
- * is kept, never overwritten an existing AGENTS.md is the project's context, adopted as-is.
55
+ * Scaffold a runnable agent into `<dir>/<agentDir>/` or into `dir` itself when `agentDir` is `"."`
56
+ * (both created if missing). Default is a complete agent (persona.md + the writing-great-skills skill +
57
+ * a code tool + package.json); `--minimal` drops the code tool and package.json.
58
+ *
59
+ * A SUBDIRECTORY target must be empty (any content there is an unfinished agent or something unrelated,
60
+ * and landing persona.md beside it would be a silent mix), while `"."` is a directory being adopted
61
+ * content is expected. So `"."` KEEPS every file that already exists (reported, never overwritten, never
62
+ * verified) and refuses only on a config, which means the directory is already an agent. An existing
63
+ * AGENTS.md is untouched either way: that is the project's context, adopted as-is.
101
64
  */
102
- export async function scaffoldWorkspace(dir, options = {}) {
65
+ export async function scaffoldAgent(dir, options = {}) {
103
66
  const minimal = options.minimal ?? false;
104
- const kit = options.agentDir ?? "."; // where the agent's own surface lands
67
+ const root = agentDirName(options.agentDir);
68
+ const flat = root === ".";
69
+ const invalid = agentDirNameError(root);
70
+ if (invalid)
71
+ throw new Error(`agentDir "${root}" ${invalid}`);
105
72
  const skill = (name) => ({
106
- rel: join(kit, "skills", "writing-great-skills", name),
73
+ rel: join(root, "skills", "writing-great-skills", name),
107
74
  content: baseTemplate(`skills/writing-great-skills/${name}`),
108
75
  });
109
76
  const files = [
110
77
  // ① identity. AGENTS.md is deliberately NOT scaffolded: a fresh agent has no project context, and
111
- // an existing repo already owns its AGENTS.md (kept untouched, read as ② context from cwd).
112
- { rel: join(kit, "persona.md"), content: personaTemplate(options.agentDir) },
78
+ // an existing repo already owns its AGENTS.md (kept untouched, read as ② context from the workspace).
79
+ { rel: join(root, "persona.md"), content: baseTemplate("persona.md") },
113
80
  // The example skill: how to author skills well — the core of self-iteration. Markdown, so it
114
81
  // ships in --minimal too. Vendored verbatim from mattpocock/skills (MIT); LICENSE sits beside it.
115
82
  skill("SKILL.md"),
116
83
  skill("GLOSSARY.md"),
117
84
  skill("LICENSE"),
118
- // Run-root pieces: the config (carrying agentDir when the kit is a subdir), secrets hygiene, env template.
119
- { rel: "fastagent.config.mjs", content: configTemplate(options.agentDir) },
120
- { rel: ".gitignore", content: baseTemplate(options.agentDir ? "gitignore.agentdir-root" : "gitignore") },
121
- { rel: ".env.example", content: baseTemplate("env.example") },
85
+ { rel: join(root, "fastagent.config.mjs"), content: baseTemplate("fastagent.config.mjs") },
86
+ // Two ignore files, scaffolded ONCE and owned by the author from then on — no command rewrites,
87
+ // reads or verifies them. The agent's own covers node_modules/machinery/a stray .env; `.secrets/`
88
+ // carries its own because the root file is the one the author has reason to edit, and git's
89
+ // nested-ignore precedence keeps the credentials protected whatever happens up there.
90
+ { rel: join(root, ".gitignore"), content: baseTemplate("gitignore") },
91
+ { rel: join(root, SECRETS_DIRNAME, ".gitignore"), content: baseTemplate("secrets.gitignore") },
92
+ { rel: join(root, SECRETS_DIRNAME, ".env.example"), content: baseTemplate("env.example") },
122
93
  ];
123
94
  if (!minimal) {
124
- files.push({ rel: join(kit, "tools", "fetch-url.ts"), content: baseTemplate("tools/fetch-url.ts") },
125
- // The kit's own manifest: in the agentDir layout the agent self-contains its deps, so the host's
126
- // package.json/lockfile is never touched. The name says WHOSE agent it is (the workspace's), not
127
- // which subdirectory it happens to live in.
95
+ files.push({ rel: join(root, "tools", "fetch-url.ts"), content: baseTemplate("tools/fetch-url.ts") },
96
+ // The agent's own manifest, named after the directory it serves (`<dir>-agent`) except when it
97
+ // IS that directory, where it takes the name straight.
128
98
  {
129
- rel: join(kit, "package.json"),
130
- content: packageJson(options.agentDir ? `${toPackageName(dir)}-agent` : toPackageName(dir), await fastagentVersion()),
131
- }, ...(options.agentDir ? [{ rel: join(kit, ".gitignore"), content: baseTemplate("gitignore.kit") }] : []));
99
+ rel: join(root, "package.json"),
100
+ content: packageJson(flat ? toPackageName(dir) : `${toPackageName(dir)}-agent`, await fastagentVersion()),
101
+ });
132
102
  }
133
- // Guard on the ownership marker: a config means "already a fastagent workspace". Fail visibly
134
- // rather than double-initialize. (AGENTS.md is NOT a marker it is context, adopted untouched.)
135
- const conflicts = [];
136
- for (const name of WORKSPACE_CONFIG_NAMES)
137
- if (await exists(join(dir, name)))
138
- conflicts.push(name);
139
- if (conflicts.length > 0) {
140
- throw new Error(`"${dir}" already has ${conflicts.join(", ")} — already a fastagent workspace`);
103
+ // Inside another agent's DEFINITION (its `skills/`, `tools/`, `channels/` or `schedules/`): the outer
104
+ // agent would load the new one as its own content. Note what this deliberately ALLOWS — a package
105
+ // inside an agent's repository, which is the author's tree and not part of what that agent loads.
106
+ const owner = agentDefinitionOwner(dir);
107
+ if (owner) {
108
+ throw new Error(`"${dir}" is inside the definition of the agent at ${owner} — an agent scaffolded here would be ` +
109
+ `part of THAT agent's surface, not one of its own. Init outside it.`);
141
110
  }
142
- // Never merge the kit into an existing NON-EMPTY directory (a host repo may own an unrelated `agent/`
143
- // common in AI products): wx would keep its files, but persona.md/skills/ landing inside someone
144
- // else's code dir is a silent mix. Refuse with the way out. Dotfiles (.DS_Store, .gitkeep) don't
145
- // count same rule as detectHostSignals' occupation check. A SYMLINKED kit path slips past this
146
- // readdir (it follows links) — deliberate: the parent preflight below lstat-rejects it before any write.
147
- if (options.agentDir) {
148
- const occupants = (await readdir(join(dir, kit)).catch(() => [])).filter((f) => !f.startsWith("."));
149
- if (occupants.length > 0) {
150
- throw new Error(`"${kit}" already exists and is not empty — if it is unrelated to the agent, pick another name ` +
151
- `(--agent-dir <name>) or go flat (--flat); to adopt it as the kit, empty it first`);
152
- }
153
- }
154
- // Was the target non-empty BEFORE we wrote anything? (missing dir = empty).
155
- const intoNonEmpty = (await readdir(dir).catch(() => [])).length > 0;
156
- // Preflight scaffold parent dirs: a pre-existing non-directory there would make mkdir fail mid-loop
157
- // AFTER the first write, leaving a half-scaffold. Detect it before any write (lstat, not stat: a
158
- // symlinked parent must be rejected, not followed — it would write outside the workspace).
111
+ // Preflight scaffold parent dirs FIRST: a pre-existing non-directory there would make mkdir fail
112
+ // mid-loop AFTER the first write, leaving a half-scaffold and a file or symlink named `fastagent`
113
+ // must be named as such here, before the occupancy check below tries to read it as a directory
114
+ // (lstat, not stat: a symlinked parent must be rejected, not followed it would write outside the
115
+ // agent dir).
159
116
  const parents = new Set();
160
117
  for (const file of files) {
161
118
  let p = dirname(file.rel);
@@ -170,80 +127,99 @@ export async function scaffoldWorkspace(dir, options = {}) {
170
127
  throw new Error(`cannot scaffold: "${rel}" exists and is not a directory (a regular file or symlink) — remove it, or init elsewhere`);
171
128
  }
172
129
  }
130
+ // Refuse an occupied agent dir. A config inside means it IS an agent already (name the marker so the
131
+ // message is actionable); any other content means an unfinished agent or something unrelated, and
132
+ // landing persona.md beside it would be a silent mix. Only ENOENT reads as "empty" — any other fault
133
+ // (EACCES…) must surface here rather than as a raw errno mid-write.
134
+ // (AGENTS.md outside is NOT a marker — it is context, adopted untouched.)
135
+ // `.DS_Store`/`.gitkeep`/`.keep` are not evidence of anyone's content: Finder noise, and the standard
136
+ // way to commit an empty directory (someone reserving the name in git ahead of init).
137
+ const occupants = (await readdir(join(dir, root)).catch((e) => {
138
+ if (e.code === "ENOENT")
139
+ return [];
140
+ throw e;
141
+ })).filter((f) => ![".DS_Store", ".gitkeep", ".keep"].includes(f));
142
+ // Could `dir` still SELECT the agent this run creates? SIBLINGS are fine — several agents at one level
143
+ // is a supported shape (different roles driving one repository), picked between by FASTAGENT_AGENT or
144
+ // the default name. A SHADOW is not: a config AT `dir` wins over everything inside it, so scaffolding
145
+ // under one (or scaffolding one over existing children) makes an agent the lookup can never return.
146
+ // The target itself is never in the way — a config already there means "already an agent", which the
147
+ // refusal below says in those words.
148
+ const existing = agentsAt(dir).filter((a) => a !== resolve(dir, root));
149
+ const shadowed = flat
150
+ ? existing // the new agent lands AT `dir` and hides everything inside it
151
+ : existing.filter((a) => a === resolve(dir)); // an agent AT `dir` hides the new one inside it
152
+ if (shadowed.length > 0) {
153
+ throw new Error(`"${dir}" already resolves to ${shadowed.map((a) => displayPath(process.cwd(), a) ?? a).join(", ")} — ` +
154
+ `an agent scaffolded ${flat ? "here" : `in ./${root}/`} would be hidden by it and never served ` +
155
+ `from "${dir}" (an agent AT a directory wins over any inside it). Use that agent, move it away, ` +
156
+ `or init in a different directory.`);
157
+ }
158
+ // ONE coordinate system for both refusals — `displayPath` is the shared policy (relative inside the
159
+ // cwd, absolute when it climbs out); the earlier pair mixed an absolute path with a basename-relative
160
+ // one, in adjacent branches of the same command.
161
+ const target = displayPath(process.cwd(), join(dir, root)) ?? join(dir, root);
162
+ const config = occupants.filter((f) => AGENT_CONFIG_NAMES.includes(f));
163
+ if (config.length > 0) {
164
+ throw new Error(`"${target}" already has ${config.join(", ")} — already a fastagent agent`);
165
+ }
166
+ // Only a SUBDIRECTORY target must be empty. `.` is a directory being adopted — content is expected,
167
+ // and every existing file is kept below.
168
+ if (!flat && occupants.length > 0) {
169
+ throw new Error(`"${target}" already holds ${occupants.join(", ")} — move it away first, or run ` +
170
+ `\`fastagent init\` in a different directory`);
171
+ }
172
+ // Which directories were OURS to create? "Empty" is not ownership: a user may have pre-created any of
173
+ // them (`--agent-dir .` adopts a directory that can already carry an empty `skills/` or `.secrets/`),
174
+ // and the rollback below must delete only what THIS run made. Recorded before the first write, since
175
+ // afterwards the two are indistinguishable.
176
+ const preexisting = new Set();
177
+ for (const rel of parents)
178
+ if (await exists(join(dir, rel)))
179
+ preexisting.add(rel);
180
+ const agentDirExisted = await exists(join(dir, root));
173
181
  await mkdir(dir, { recursive: true });
174
182
  const created = [];
175
- const skipped = [];
176
- const patched = [];
177
- const warnings = [];
178
- // ONE rollback scope: any failure removes files written THIS run (guard + wx guarantee they are
179
- // ours), so scaffoldWorkspace is atomic — except the .gitignore APPEND below, which is not rolled
180
- // back (the residue is idempotent, harmless ignore lines; removing someone else's file's tail is riskier).
183
+ const kept = [];
184
+ // ONE rollback scope: any failure removes what THIS run created — files AND the directories it made
185
+ // for them. Leaving the empty dirs behind would be worse than untidy: the occupancy refusal above
186
+ // would then report the next `init` as occupied, blaming the user for our own debris.
187
+ //
188
+ // `wx` never clobbers. A subdirectory target was proven empty, so EEXIST means a concurrent writer —
189
+ // an error. `.` is a directory being adopted, so an existing `.gitignore`/`package.json` is the
190
+ // author's — keep it and report it.
181
191
  try {
182
192
  for (const file of files) {
183
193
  const abs = join(dir, file.rel);
184
194
  await mkdir(dirname(abs), { recursive: true });
185
195
  try {
186
- await writeFile(abs, file.content, { flag: "wx" }); // wx: never clobber
196
+ await writeFile(abs, file.content, { flag: "wx" });
187
197
  created.push(file.rel);
188
198
  }
189
- catch (error) {
190
- if (error.code === "EEXIST")
191
- skipped.push(file.rel);
192
- else
193
- throw error;
199
+ catch (e) {
200
+ if (!flat || e.code !== "EEXIST")
201
+ throw e;
202
+ kept.push(file.rel);
194
203
  }
195
204
  }
196
- // Ignore ownership follows layout jurisdiction. Run-root state/secrets live at `dir`, so the ROOT
197
- // .gitignore owns `.env` + `.fastagent`. The kit's npm deps live where the kit lives: flat → root
198
- // `node_modules/`; agentDir → `<agentDir>/.gitignore` owns its own `node_modules/`. This avoids
199
- // patching a host repo's root ignore with agent-internal dependency paths while still preventing the
200
- // post-init 25k-file untracked flood.
201
- const coveredBy = (ig, p) => (ig?.ignores(p) ?? false) || (ig?.ignores(`${p}/`) ?? false);
202
- const rootIgnore = await loadRootIgnore(dir);
203
- const rootRequired = [
204
- { path: ".env", pattern: ".env" },
205
- { path: ".fastagent", pattern: ".fastagent" },
206
- ...(!minimal && !options.agentDir ? [{ path: "node_modules", pattern: "node_modules/" }] : []),
207
- ];
208
- const rootNeed = rootRequired.filter((e) => !coveredBy(rootIgnore, e.path));
209
- if (rootNeed.length > 0) {
210
- await appendFile(join(dir, ".gitignore"), `\n# fastagent\n${rootNeed.map((e) => e.pattern).join("\n")}\n`);
211
- patched.push(".gitignore");
212
- // A .fastagentignore is applied LAST and can re-include what .gitignore excludes — the append
213
- // cannot fix that, so re-check and keep it a visible warning instead of false assurance.
214
- const after = await loadRootIgnore(dir);
215
- const still = rootNeed.filter((e) => !coveredBy(after, e.path)).map((e) => e.path);
216
- if (still.length > 0) {
217
- warnings.push(`your .gitignore/.fastagentignore does not exclude ${still.map((s) => `"${s}"`).join(", ")} — a deploy that copies the directory may ship secrets/state`);
218
- }
219
- }
220
- if (!minimal && options.agentDir) {
221
- // Deliberately checks ONLY the kit's own ignore, not the root: the kit is the portable agent
222
- // directory, and its dependency ignore must travel WITH it (a fresh kit .gitignore carries
223
- // node_modules/ unconditionally for the same reason) — a root-level `node_modules/` that happens
224
- // to cover it today doesn't survive the kit being copied out of the host repo.
225
- const kitIgnore = await loadRootIgnore(join(dir, kit));
226
- if (!coveredBy(kitIgnore, "node_modules")) {
227
- const rel = join(kit, ".gitignore");
228
- await appendFile(join(dir, rel), `\n# fastagent\nnode_modules/\n`);
229
- patched.push(rel);
230
- }
231
- }
232
- // A kept package.json won't carry the tool's deps — the example tool would not resolve. The install
233
- // command matches the workspace's runtime (bun.lock → bun add).
234
- const keptPkg = join(kit, "package.json");
235
- if (!minimal && skipped.includes(keptPkg)) {
236
- const kitAbs = join(dir, kit);
237
- const add = detectRuntime(kitAbs, await readPackageJson(kitAbs)).runtime === "bun" ? "bun add" : "npm install";
238
- warnings.push(`kept the existing ${keptPkg} — run \`${add} @fastagent-sh/fastagent\` there so the example tool resolves`);
239
- }
240
205
  }
241
206
  catch (error) {
242
- // Best-effort rollback of a partial scaffold: a file that won't delete is left behind (the original
243
- // error below is the one worth surfacing — a cleanup failure must not mask it).
207
+ // Best-effort rollback of a partial scaffold: anything that won't delete is left behind (the
208
+ // original error below is the one worth surfacing — a cleanup failure must not mask it). Files
209
+ // first, then the directories THIS RUN created, deepest first; `rmdir` additionally removes only
210
+ // what is empty, so a pre-existing sibling inside one of ours is never touched. The agent root goes
211
+ // last, and only when this run created it — "empty" was never proof of ownership. (Reaching here at all takes a
212
+ // real fs fault: the occupancy refusal above proved the target empty, so nothing else can fail
213
+ // mid-loop. The covered case is a permission fault before the first write.)
244
214
  for (const rel of created.reverse())
245
215
  await rm(join(dir, rel), { force: true }).catch(() => { });
216
+ for (const rel of [...parents].sort((a, b) => b.split(sep).length - a.split(sep).length)) {
217
+ if (rel !== root && !preexisting.has(rel))
218
+ await rmdir(join(dir, rel)).catch(() => { });
219
+ }
220
+ if (!agentDirExisted)
221
+ await rmdir(join(dir, root)).catch(() => { });
246
222
  throw error;
247
223
  }
248
- return { dir, complete: !minimal, agentDir: options.agentDir, created, skipped, patched, intoNonEmpty, warnings };
224
+ return { dir, complete: !minimal, agentDir: root, created, kept };
249
225
  }
@@ -1,5 +1,5 @@
1
- # Environment for this agent. Copy to .env (gitignored) and uncomment what you need.
2
- # Everything here is OPTIONAL — the defaults work without a .env.
1
+ # Environment for this agent. Copy to .env IN THIS DIRECTORY (.secrets/.env gitignored, managed by
2
+ # fastagent) and uncomment what you need. Everything here is OPTIONAL — the defaults work without a .env.
3
3
 
4
4
  # --- Model auth ---
5
5
  # Pick auth once: `fastagent login` (a subscription/OAuth provider like openai-codex, or an API key),
@@ -8,6 +8,11 @@
8
8
  # OPENAI_API_KEY=
9
9
  # ANTHROPIC_API_KEY=
10
10
 
11
+ # --- Which agent (only when a workspace holds several) ---
12
+ # FASTAGENT_AGENT names the agent directory to serve. It CANNOT be set here: placement resolves before
13
+ # this file is read (it is what decides whose .env this is). Put it in your shell, an .envrc, or the
14
+ # command: `FASTAGENT_AGENT=pm fastagent dev`.
15
+
11
16
  # --- Model selection (overrides fastagent.config) ---
12
17
  # Precedence: --model flag > FASTAGENT_MODEL > config. `fastagent dev` writes your first-run pick to
13
18
  # fastagent.config; set it here to override per-environment.
@@ -16,9 +21,13 @@
16
21
  # --- Serving (fastagent start) ---
17
22
  # Port precedence: --port > PORT > config.http.port > 8787
18
23
  # PORT=8787
19
- # Machine-state root: sessions, auth.json, and channel state all derive from it.
20
- # Default: <dir>/.fastagent. Point it at a mounted volume so a redeploy that replaces the
21
- # directory keeps conversations and credentials.
22
- # FASTAGENT_STATE_DIR=/data/fastagent
24
+ # Machine-state root: sessions, schedule state, and channel state derive from it.
25
+ # Default: <agent dir>/.state. Point it at a mounted volume so a redeploy that replaces the
26
+ # directory keeps conversations.
27
+ # FASTAGENT_STATE_DIR=/data/.state
28
+ # Secrets home (this .env + auth.json). Default: <agent dir>/.secrets. Deployments point it at
29
+ # the volume so a rotated OAuth credential persists across restarts. Set HERE it moves only
30
+ # auth.json — a value inside .env cannot relocate the .env being read.
31
+ # FASTAGENT_SECRETS_DIR=/data/.secrets
23
32
  # Override just the sessions path (default: <state root>/sessions):
24
33
  # FASTAGENT_SESSIONS_DIR=/data/sessions
@@ -1,16 +1,17 @@
1
1
  // fastagent.config.mjs — deployment choices only (model / http; code tools auto-discover from tools/).
2
2
  // Your agent's identity lives in persona.md; its capabilities in skills/ + tools/ — never here.
3
- // An AGENTS.md at the workspace root (yours or the host repo's) is read as project context.
3
+ // An AGENTS.md in the WORKSPACE (the directory the agent is started in) is read as project context.
4
4
  // Model precedence: `--model` flag > FASTAGENT_MODEL env > this default.
5
- // No model is preset: `fastagent dev` prompts you to pick one from the providers you're logged into
6
- // (run `fastagent login` first) and writes your choice below. Or set it by hand to a "provider/modelId"
7
- // you have access to (`fastagent models` lists them).
5
+ // No model is preset: `fastagent dev` shows the full model catalog (models you already have
6
+ // credentials for come first; picking one that needs auth logs you in inline) and writes your choice
7
+ // below. Or set it by hand to a "provider/modelId" (`fastagent models` lists them).
8
8
  export default {
9
9
  // model: "openai-codex/gpt-5.5",
10
10
  // thinkingLevel: "high", // reasoning effort (off|minimal|low|medium|high|xhigh|max); default "medium" (pi TUI parity)
11
11
  http: { port: 8787 },
12
12
  // selfSchedule: true, // mount the built-in `wake` tool: the agent schedules its own follow-up turns
13
13
  // // ("check the deploy in 10 min"). Cron jobs need no opt-in — drop a schedules/<name>.ts.
14
+ // sessionControl: true, // serve /control/* for remote observation + steering (fastagent attach / Web panel)
14
15
  // deploy: what the agent needs on the box (so `fastagent deploy` doesn't need a hand-written Dockerfile
15
16
  // or hand-set host variables). Uncomment as needed:
16
17
  // deploy: {
@@ -1,8 +1,16 @@
1
- # secretsnever commit (kept out of git and any deploy copy)
2
- .env
1
+ # This file is yours fastagent writes it once, at `init`, and never touches git again.
2
+ # (`.secrets/` carries its own, so the credentials stay protected however you edit this one.)
3
+
4
+ # dependencies (reinstalled at deploy). No trailing slash on purpose: `node_modules/` matches only a
5
+ # DIRECTORY, so a symlinked one (pnpm, a shared store) would show up as untracked.
6
+ node_modules
3
7
 
4
- # dependencies (reinstalled at deploy)
5
- node_modules/
8
+ # machine state: sessions, channel state, schedule state
9
+ .state
10
+ .cache
6
11
 
7
- # fastagent machine state (dev/start sessions + project-level auth.json credentials)
8
- .fastagent/
12
+ # fastagent reads the agent's env from .secrets/.env (which un-ignores its own .env.example). A bare
13
+ # .env at THIS level is the file habit puts here — `dev` says so when it finds one; this keeps it
14
+ # uncommittable meanwhile.
15
+ .env
16
+ .env.*
@@ -1,10 +1,12 @@
1
1
  # Persona
2
2
 
3
- You are this workspace's agent. This file is your identity — it overrides the engine's default identity line, and it is re-read every turn along with the rest of your definition (`skills/` — capabilities you load when a task calls for them; `tools/` — code tools your author added, in the same directory as this file). An edit to any of them takes effect on your next message, no restart. Your `read` / `write` / `edit` / `bash` tools operate at the workspace root you were started in; if the workspace has an `AGENTS.md`, it is project context — read it to learn the project's conventions.
3
+ You are this workspace's agent. This file is your identity — it overrides the engine's default identity line, and it is re-read every turn along with the rest of your definition (`skills/` — capabilities you load when a task calls for them; `tools/` — code tools your author added, in the same directory as this file). An edit to any of them takes effect on your next message, no restart.
4
+
5
+ Your definition is this directory: `persona.md`, `skills/`, `tools/`, and the config beside them. Your WORKSPACE is the directory you were started in — the project you work on, and where your `read` / `write` / `edit` / `bash` tools operate. It may be this same directory, or the one containing it; `fastagent info` prints both. If the workspace has an `AGENTS.md`, it is project context — read it to learn the project's conventions.
4
6
 
5
7
  You can improve yourself. When a task reveals something durable — a repeatable process, a standing preference, a hard-won fact — write it into your definition instead of losing it:
6
8
 
7
- - A repeatable process or capability → a new skill beside this file: `skills/<name>/SKILL.md`. Read `skills/writing-great-skills/SKILL.md` first; it is the guide to authoring skills well.
9
+ - A repeatable process or capability → a new skill beside this file: `skills/<name>/SKILL.md`. Only the `skills/` next to this file is scanned. Read `skills/writing-great-skills/SKILL.md` first; it is the guide to authoring skills well.
8
10
  - A standing instruction or fact → edit this file.
9
11
 
10
12
  Keep both lean: include only what changes your behavior, and delete what no longer earns its place.
@@ -0,0 +1,5 @@
1
+ # fastagent-managed secrets: everything here stays on this machine —
2
+ # except the template, which travels with the agent.
3
+ *
4
+ !.gitignore
5
+ !.env.example
@@ -1,11 +1,5 @@
1
- /** Read a base workspace template (src/scaffold/templates/<name>). */
1
+ /** Read a base agent template (src/scaffold/templates/<name>). */
2
2
  export declare const baseTemplate: (name: string) => string;
3
- /** The persona template; in the agentDir layout a locator note is appended (anchor-free — no silent-miss
4
- * risk) so the self-iteration guidance ("write skills beside this file") points into the kit, not the
5
- * run root — a skill written to the root `skills/` would never be scanned. */
6
- export declare function personaTemplate(agentDir?: string): string;
7
- /** The config template; when the kit is placed in a subdirectory, `agentDir` is injected as the first key. */
8
- export declare function configTemplate(agentDir?: string): string;
9
3
  /** Read one file from a channel's scaffold bundle (src/channels/<kind>/scaffold/<name>). */
10
4
  export declare const channelTemplate: (kind: string, name: string) => string;
11
5
  /** The .ts files in a channel's bundle: `channel.ts` is the channel adapter; the rest are companion tools. */
@@ -1,36 +1,14 @@
1
1
  /**
2
- * Scaffold template ACCESS (data, not logic): readers for the real files `init`/`add` write into a
3
- * workspace, plus the parametric pieces. Base workspace templates live under ./templates/; each
2
+ * Scaffold template ACCESS (data, not logic): readers for the real files `init`/`add` write into an
3
+ * agent dir, plus the parametric pieces. Base agent templates live under ./templates/; each
4
4
  * channel's bundle lives WITH the channel at ../channels/<kind>/scaffold/ (so a channel owns its
5
5
  * starter kit and could ship as its own package). Both trees are excluded from this package's tsc +
6
6
  * biome (they import the published @fastagent-sh/fastagent, not this source) and copied into dist/ by the build.
7
7
  */
8
8
  import { readFileSync, readdirSync } from "node:fs";
9
9
  import { basename, resolve } from "node:path";
10
- /** Read a base workspace template (src/scaffold/templates/<name>). */
10
+ /** Read a base agent template (src/scaffold/templates/<name>). */
11
11
  export const baseTemplate = (name) => readFileSync(new URL(`./templates/${name}`, import.meta.url), "utf8");
12
- /** The persona template; in the agentDir layout a locator note is appended (anchor-free — no silent-miss
13
- * risk) so the self-iteration guidance ("write skills beside this file") points into the kit, not the
14
- * run root — a skill written to the root `skills/` would never be scanned. */
15
- export function personaTemplate(agentDir) {
16
- const base = baseTemplate("persona.md");
17
- if (!agentDir)
18
- return base;
19
- return `${base}\nNote: your definition lives under \`${agentDir}/\` relative to the workspace root — this file is \`${agentDir}/persona.md\`, and a new skill goes to \`${agentDir}/skills/<name>/SKILL.md\` (a \`skills/\` at the root is not scanned).\n`;
20
- }
21
- /** The config template; when the kit is placed in a subdirectory, `agentDir` is injected as the first key. */
22
- export function configTemplate(agentDir) {
23
- const base = baseTemplate("fastagent.config.mjs");
24
- if (!agentDir)
25
- return base;
26
- const out = base.replace("export default {\n", `export default {\n agentDir: ${JSON.stringify(agentDir)}, // the agent's own surface (persona.md / skills / tools / channels) lives there\n`);
27
- // Fail visibly if the template drifted and the anchor no longer matches — a config that silently
28
- // doesn't point at the kit would assemble an EMPTY agent with no error. Check the OPERATION happened
29
- // (out !== base), not a substring — a commented `agentDir:` example in the template would fool that.
30
- if (out === base)
31
- throw new Error("configTemplate: anchor not found in fastagent.config.mjs template");
32
- return out;
33
- }
34
12
  const channelScaffoldDir = (kind) => new URL(`../channels/${kind}/scaffold/`, import.meta.url);
35
13
  /** Read one file from a channel's scaffold bundle (src/channels/<kind>/scaffold/<name>). */
36
14
  export const channelTemplate = (kind, name) => readFileSync(new URL(name, channelScaffoldDir(kind)), "utf8");
@@ -13,12 +13,12 @@ export interface VendoredSkill {
13
13
  overwritten: boolean;
14
14
  }
15
15
  /**
16
- * Vendor an Agent Skills skill into `<workspace>/skills/<name>/` from a giget ref (github default), a
16
+ * Vendor an Agent Skills skill into `<agent dir>/skills/<name>/` from a giget ref (github default), a
17
17
  * local path, or a bare name (resolved against the local global skill dirs). Copy-in, git-tracked.
18
18
  * Refuses to overwrite unless `options.update` (then a plain git-tracked overwrite, never a merge).
19
19
  * Validates a staging copy with the runtime loader BEFORE replacing, so a bad fetch never destroys an
20
20
  * existing skill.
21
21
  */
22
- export declare function vendorSkill(workspaceDir: string, source: string, options?: {
22
+ export declare function vendorSkill(agentDir: string, source: string, options?: {
23
23
  update?: boolean;
24
24
  }): Promise<VendoredSkill>;