@fastagent-sh/fastagent 0.15.0 → 0.16.1

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 (181) hide show
  1. package/README.md +14 -4
  2. package/dist/bind.d.ts +34 -0
  3. package/dist/bind.js +74 -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 +266 -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 +5 -0
  13. package/dist/channels/context-buffer.js +5 -0
  14. package/dist/channels/feishu/context-buffer.d.ts +13 -8
  15. package/dist/channels/feishu/context-buffer.js +60 -6
  16. package/dist/channels/feishu/feishu-api.js +4 -1
  17. package/dist/channels/feishu/feishu.d.ts +0 -12
  18. package/dist/channels/feishu/feishu.js +143 -54
  19. package/dist/channels/feishu/invoke-turn.js +35 -18
  20. package/dist/channels/feishu/model.d.ts +0 -1
  21. package/dist/channels/feishu/normalize.js +0 -1
  22. package/dist/channels/feishu/parse.d.ts +21 -7
  23. package/dist/channels/feishu/parse.js +24 -7
  24. package/dist/channels/feishu/preview.js +3 -2
  25. package/dist/channels/feishu/scaffold/channel.ts +9 -8
  26. package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
  27. package/dist/channels/feishu/setup-mode.d.ts +30 -1
  28. package/dist/channels/feishu/setup-mode.js +27 -1
  29. package/dist/channels/github/github.js +8 -1
  30. package/dist/channels/http.js +1 -1
  31. package/dist/channels/lark/scaffold/channel.ts +9 -8
  32. package/dist/channels/lark/scaffold/lark-send.ts +6 -4
  33. package/dist/channels/preview-kit.d.ts +7 -1
  34. package/dist/channels/preview-kit.js +3 -2
  35. package/dist/channels/slack/parse.d.ts +16 -1
  36. package/dist/channels/slack/parse.js +46 -3
  37. package/dist/channels/slack/preview.d.ts +1 -2
  38. package/dist/channels/slack/preview.js +68 -24
  39. package/dist/channels/slack/scaffold/channel.ts +5 -5
  40. package/dist/channels/slack/slack-api.d.ts +3 -23
  41. package/dist/channels/slack/slack-api.js +6 -22
  42. package/dist/channels/slack/slack.d.ts +13 -20
  43. package/dist/channels/slack/slack.js +95 -50
  44. package/dist/channels/state.d.ts +11 -4
  45. package/dist/channels/state.js +19 -12
  46. package/dist/channels/tasks.d.ts +0 -6
  47. package/dist/channels/tasks.js +16 -1
  48. package/dist/channels/telegram/parse.d.ts +0 -7
  49. package/dist/channels/telegram/parse.js +4 -2
  50. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
  51. package/dist/channels/telegram/telegram.js +1 -1
  52. package/dist/channels/text.d.ts +14 -0
  53. package/dist/channels/text.js +14 -0
  54. package/dist/channels/thread-participants.d.ts +21 -0
  55. package/dist/channels/thread-participants.js +132 -0
  56. package/dist/channels/turn-queue.js +7 -0
  57. package/dist/cli/add-feishu.d.ts +7 -4
  58. package/dist/cli/add-feishu.js +57 -37
  59. package/dist/cli/add-slack.d.ts +2 -1
  60. package/dist/cli/add-slack.js +6 -11
  61. package/dist/cli/commands/add.js +50 -51
  62. package/dist/cli/commands/attach.js +8 -4
  63. package/dist/cli/commands/chat.js +8 -8
  64. package/dist/cli/commands/deploy.d.ts +14 -1
  65. package/dist/cli/commands/deploy.js +330 -78
  66. package/dist/cli/commands/dev.d.ts +1 -0
  67. package/dist/cli/commands/dev.js +38 -25
  68. package/dist/cli/commands/fire.js +15 -16
  69. package/dist/cli/commands/info.js +36 -29
  70. package/dist/cli/commands/init.d.ts +1 -1
  71. package/dist/cli/commands/init.js +65 -53
  72. package/dist/cli/commands/invoke.js +9 -6
  73. package/dist/cli/commands/login.js +35 -21
  74. package/dist/cli/commands/schedule.js +6 -8
  75. package/dist/cli/commands/start.d.ts +1 -0
  76. package/dist/cli/commands/start.js +101 -37
  77. package/dist/cli/commands/tool.js +30 -18
  78. package/dist/cli/fail.d.ts +17 -0
  79. package/dist/cli/fail.js +24 -0
  80. package/dist/cli/program.js +66 -36
  81. package/dist/cli/serve.d.ts +52 -8
  82. package/dist/cli/serve.js +133 -32
  83. package/dist/cli/shared.d.ts +21 -2
  84. package/dist/cli/shared.js +44 -18
  85. package/dist/deploy/agentcore/plan.d.ts +117 -0
  86. package/dist/deploy/agentcore/plan.js +721 -0
  87. package/dist/deploy/agentcore/run.d.ts +73 -0
  88. package/dist/deploy/agentcore/run.js +412 -0
  89. package/dist/deploy/agentcore/zip.d.ts +17 -0
  90. package/dist/deploy/agentcore/zip.js +68 -0
  91. package/dist/deploy/container.d.ts +26 -25
  92. package/dist/deploy/container.js +93 -89
  93. package/dist/deploy/docker/plan.d.ts +1 -1
  94. package/dist/deploy/docker/plan.js +12 -17
  95. package/dist/deploy/fly/plan.d.ts +2 -0
  96. package/dist/deploy/fly/plan.js +27 -19
  97. package/dist/deploy/fly/run.d.ts +12 -1
  98. package/dist/deploy/fly/run.js +36 -2
  99. package/dist/deploy/preflight.d.ts +11 -5
  100. package/dist/deploy/preflight.js +235 -65
  101. package/dist/deploy/railway/plan.d.ts +7 -0
  102. package/dist/deploy/railway/plan.js +41 -16
  103. package/dist/deploy/railway/run.d.ts +8 -1
  104. package/dist/deploy/railway/run.js +7 -2
  105. package/dist/deploy/runner.d.ts +5 -2
  106. package/dist/deploy/runner.js +9 -3
  107. package/dist/dev-supervisor.d.ts +11 -8
  108. package/dist/dev-supervisor.js +53 -51
  109. package/dist/engines/pi/auth.d.ts +8 -7
  110. package/dist/engines/pi/auth.js +12 -10
  111. package/dist/engines/pi/channel.d.ts +1 -1
  112. package/dist/engines/pi/channel.js +5 -5
  113. package/dist/engines/pi/chat.js +2 -2
  114. package/dist/engines/pi/config.d.ts +12 -48
  115. package/dist/engines/pi/config.js +29 -110
  116. package/dist/engines/pi/create.d.ts +32 -24
  117. package/dist/engines/pi/create.js +47 -23
  118. package/dist/engines/pi/definition.d.ts +7 -26
  119. package/dist/engines/pi/definition.js +8 -54
  120. package/dist/engines/pi/harness.d.ts +19 -5
  121. package/dist/engines/pi/harness.js +3 -5
  122. package/dist/engines/pi/login.d.ts +1 -1
  123. package/dist/engines/pi/models.d.ts +3 -3
  124. package/dist/engines/pi/models.js +1 -1
  125. package/dist/engines/pi/{workspace.d.ts → open.d.ts} +31 -25
  126. package/dist/engines/pi/{workspace.js → open.js} +27 -29
  127. package/dist/engines/pi/read-image.d.ts +4 -0
  128. package/dist/engines/pi/read-image.js +62 -0
  129. package/dist/engines/pi/search-tools.d.ts +6 -4
  130. package/dist/engines/pi/search-tools.js +3 -1
  131. package/dist/engines/pi/session-builder.d.ts +2 -2
  132. package/dist/engines/pi/session-builder.js +18 -13
  133. package/dist/engines/pi/tool.d.ts +13 -5
  134. package/dist/engines/pi/tool.js +4 -0
  135. package/dist/engines/pi/wake-tool.d.ts +3 -3
  136. package/dist/env.d.ts +16 -4
  137. package/dist/env.js +43 -5
  138. package/dist/host/node.d.ts +4 -2
  139. package/dist/host/node.js +2 -1
  140. package/dist/loader.d.ts +2 -2
  141. package/dist/loader.js +3 -3
  142. package/dist/log.d.ts +1 -1
  143. package/dist/log.js +1 -1
  144. package/dist/paths.d.ts +138 -0
  145. package/dist/paths.js +326 -0
  146. package/dist/pi.d.ts +2 -2
  147. package/dist/pi.js +2 -2
  148. package/dist/runtime.d.ts +7 -5
  149. package/dist/runtime.js +2 -2
  150. package/dist/scaffold/add-channel.d.ts +7 -3
  151. package/dist/scaffold/add-channel.js +55 -29
  152. package/dist/scaffold/init.d.ts +32 -41
  153. package/dist/scaffold/init.js +161 -185
  154. package/dist/scaffold/templates/env.example +15 -6
  155. package/dist/scaffold/templates/fastagent.config.mjs +1 -1
  156. package/dist/scaffold/templates/gitignore +14 -6
  157. package/dist/scaffold/templates/persona.md +4 -2
  158. package/dist/scaffold/templates/secrets.gitignore +5 -0
  159. package/dist/scaffold/templates.d.ts +1 -7
  160. package/dist/scaffold/templates.js +3 -25
  161. package/dist/scaffold/vendor-skill.d.ts +2 -2
  162. package/dist/scaffold/vendor-skill.js +13 -13
  163. package/dist/schedule/discover.js +4 -4
  164. package/dist/schedule/scheduler.d.ts +40 -1
  165. package/dist/schedule/scheduler.js +89 -56
  166. package/dist/schedule/state.js +1 -1
  167. package/dist/schedule/wake-alarm.d.ts +47 -0
  168. package/dist/schedule/wake-alarm.js +136 -0
  169. package/dist/schedule/wakeups.d.ts +1 -0
  170. package/dist/schedule/wakeups.js +18 -0
  171. package/dist/tunnel.d.ts +3 -3
  172. package/dist/tunnel.js +7 -7
  173. package/package.json +7 -4
  174. package/dist/channels/feishu/owned-threads.d.ts +0 -7
  175. package/dist/channels/feishu/owned-threads.js +0 -47
  176. package/dist/channels/slack/owned-threads.d.ts +0 -6
  177. package/dist/channels/slack/owned-threads.js +0 -43
  178. package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
  179. package/dist/scaffold/templates/gitignore.kit +0 -2
  180. package/dist/workspace.d.ts +0 -9
  181. package/dist/workspace.js +0 -45
@@ -1,54 +1,45 @@
1
+ /** The agent directory name for a raw `--agent-dir` value: the default when unset, and `./bot` read as
2
+ * `bot` — `basename` already says that is what it means, so rejecting the spelling would be pedantry. */
3
+ export declare function agentDirName(raw: string | undefined): string;
4
+ /** Why `name` cannot be an agent directory name, or undefined when it can. It must stay ONE segment
5
+ * inside the target: anything else (a separator, `..`, an absolute path) would land the agent where the
6
+ * one-level lookup cannot see it — an agent nothing would ever serve.
7
+ *
8
+ * Returns the CONSTRAINT, not a sentence: the CLI prefixes the flag it owns and reports it as the usage
9
+ * error it is (exit 2), while {@link scaffoldAgent} prefixes the option name for a programmatic caller,
10
+ * who never passed a flag and should not be told to fix one. */
11
+ export declare function agentDirNameError(name: string): string | undefined;
1
12
  export interface ScaffoldOptions {
2
13
  /** Scaffold the markdown-only unit (no package.json, no tool, no install) instead of a complete agent. */
3
14
  minimal?: boolean;
4
- /**
5
- * Place the agent kit (persona.md/skills/tools/package.json) in this subdirectory (e.g. "./agent")
6
- * instead of flat in `dir`; the config is written at the root with `agentDir` pointing there.
7
- * Undefined = flat. The CLI decides (jurisdiction detection + flags); this stays mechanical.
8
- */
15
+ /** The agent directory's name inside `dir` — default {@link DEFAULT_AGENT_DIRNAME}, `"."` for `dir`
16
+ * itself. ONE path segment: a separator or `..` would put the agent outside the directory the author
17
+ * named, where the one-level lookup could never find it. */
9
18
  agentDir?: string;
10
19
  }
11
20
  export interface ScaffoldResult {
12
21
  dir: string;
13
22
  /** Whether a complete (code-tool) agent was scaffolded (false for --minimal). */
14
23
  complete: boolean;
15
- /** The kit subdirectory in effect (relative, e.g. "./agent"); undefined = flat. */
16
- agentDir?: string;
17
- /** Files written by this run (relative paths). */
24
+ /** The agent dir relative to `dir`: the {@link ScaffoldOptions.agentDir} that was used. */
25
+ agentDir: string;
26
+ /** Files written by this run (relative to `dir`). */
18
27
  created: string[];
19
- /** Files that already existed and were kept untouched (e.g. a pre-existing .gitignore). */
20
- skipped: string[];
21
- /** Kept ignore files appended with missing fastagent excludes (root .gitignore: .env/.fastagent, plus
22
- * node_modules/ in the flat layout; kit .gitignore in the agentDir layout: node_modules/). */
23
- patched: string[];
24
- /** True if the target already had content before this run (init into an existing/non-empty dir). */
25
- intoNonEmpty: boolean;
26
- /** Non-fatal advisories the caller MUST surface. */
27
- warnings: string[];
28
+ /** Files that already existed and were KEPT untouched. Only reachable with `agentDir: "."` (a
29
+ * subdirectory target is proven empty first): adopting a directory means its `.gitignore`/
30
+ * `package.json` are the author's. The caller surfaces them silently skipping a file the user
31
+ * expected would be worse. */
32
+ kept: string[];
28
33
  }
29
34
  /**
30
- * Jurisdiction signals: evidence that an existing system already claims this tree, so a flat agent kit
31
- * would put each side's files under the other's jurisdiction (host tsc sweeps agent .ts; fastagent
32
- * scans host tools/). Three classes, derived from the actual failure modes — a toolchain config, a
33
- * deploy manifest, or fastagent's convention names already occupied. Any hit → the kit defaults into
34
- * `./agent`. Deliberately NOT signals: "dir is non-empty", "has package.json", "has src/" — markdown
35
- * and loose scripts are claimed by nobody, and "a directory is an agent" stays the default. Known
36
- * tradeoff, decided for visibility: a HAND-BUILT agent dir (skills//tools/ authored for the agent, no
37
- * config yet) also hits the occupation signal and defaults to ./agent wrong for that case, but the
38
- * reason is printed and `--flat` overrides; the reverse default would silently mis-scan a host's dirs.
39
- */
40
- export declare function detectHostSignals(dir: string): Promise<string[]>;
41
- /** The `cd` target to show in `init`'s next-steps: the relative path when the target is inside `cwd`,
42
- * the absolute path when it climbs out (a `../../..` is noise), or undefined when already in `cwd`. */
43
- export declare function nextStepCd(cwd: string, dir: string): string | undefined;
44
- /** Does a path exist? (async; shared with the sibling scaffold modules). */
45
- export declare function exists(p: string): Promise<boolean>;
46
- /**
47
- * Scaffold a runnable workspace into {@link dir} (created if missing). Default is a complete agent
48
- * (persona.md + the writing-great-skills skill + a code tool + package.json); `--minimal` drops the
49
- * code tool and package.json. The kit goes flat into `dir`, or into {@link ScaffoldOptions.agentDir}
50
- * (config at the root pointing there). Refuses only an existing fastagent.config.* (the ownership
51
- * marker — already a workspace); every other pre-existing file (AGENTS.md, .gitignore, package.json)
52
- * is kept, never overwritten — an existing AGENTS.md is the project's context, adopted as-is.
35
+ * Scaffold a runnable agent into `<dir>/<agentDir>/` or into `dir` itself when `agentDir` is `"."`
36
+ * (both created if missing). Default is a complete agent (persona.md + the writing-great-skills skill +
37
+ * a code tool + package.json); `--minimal` drops the code tool and package.json.
38
+ *
39
+ * A SUBDIRECTORY target must be empty (any content there is an unfinished agent or something unrelated,
40
+ * and landing persona.md beside it would be a silent mix), while `"."` is a directory being adopted
41
+ * content is expected. So `"."` KEEPS every file that already exists (reported, never overwritten, never
42
+ * verified) and refuses only on a config, which means the directory is already an agent. An existing
43
+ * AGENTS.md is untouched either way: that is the project's context, adopted as-is.
53
44
  */
54
- export declare function scaffoldWorkspace(dir: string, options?: ScaffoldOptions): Promise<ScaffoldResult>;
45
+ export declare function scaffoldAgent(dir: string, options?: ScaffoldOptions): Promise<ScaffoldResult>;
@@ -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,6 +1,6 @@
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
5
  // No model is preset: `fastagent dev` shows the full model catalog (models you already have
6
6
  // credentials for come first; picking one that needs auth logs you in inline) and writes your choice
@@ -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