auggy 0.3.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 (121) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +201 -0
  3. package/README.md +161 -0
  4. package/package.json +76 -0
  5. package/src/agent-card.ts +39 -0
  6. package/src/agent.ts +283 -0
  7. package/src/agentmail-client.ts +138 -0
  8. package/src/augments/bash/index.ts +463 -0
  9. package/src/augments/bash/skill/SKILL.md +156 -0
  10. package/src/augments/budgets/budget-store.ts +513 -0
  11. package/src/augments/budgets/index.ts +134 -0
  12. package/src/augments/budgets/preamble.ts +93 -0
  13. package/src/augments/budgets/types.ts +89 -0
  14. package/src/augments/file-memory/index.ts +71 -0
  15. package/src/augments/filesystem/index.ts +533 -0
  16. package/src/augments/filesystem/skill/SKILL.md +142 -0
  17. package/src/augments/filesystem/skill/references/mount-permissions.md +81 -0
  18. package/src/augments/layered-memory/extractor/buffer.ts +56 -0
  19. package/src/augments/layered-memory/extractor/frequency.ts +79 -0
  20. package/src/augments/layered-memory/extractor/inject-handler.ts +103 -0
  21. package/src/augments/layered-memory/extractor/parse.ts +75 -0
  22. package/src/augments/layered-memory/extractor/prompt.md +26 -0
  23. package/src/augments/layered-memory/index.ts +757 -0
  24. package/src/augments/layered-memory/skill/SKILL.md +153 -0
  25. package/src/augments/layered-memory/storage/migrations/README.md +16 -0
  26. package/src/augments/layered-memory/storage/migrations/supabase-add-fact-fields.sql +9 -0
  27. package/src/augments/layered-memory/storage/sqlite-store.ts +352 -0
  28. package/src/augments/layered-memory/storage/supabase-store.ts +263 -0
  29. package/src/augments/layered-memory/storage/types.ts +98 -0
  30. package/src/augments/link/index.ts +489 -0
  31. package/src/augments/link/translate.ts +261 -0
  32. package/src/augments/notify/adapters/agentmail.ts +70 -0
  33. package/src/augments/notify/adapters/telegram.ts +60 -0
  34. package/src/augments/notify/adapters/webhook.ts +55 -0
  35. package/src/augments/notify/index.ts +284 -0
  36. package/src/augments/notify/skill/SKILL.md +150 -0
  37. package/src/augments/org-context/index.ts +721 -0
  38. package/src/augments/org-context/skill/SKILL.md +96 -0
  39. package/src/augments/skills/index.ts +103 -0
  40. package/src/augments/supabase-memory/index.ts +151 -0
  41. package/src/augments/telegram-transport/index.ts +312 -0
  42. package/src/augments/telegram-transport/polling.ts +55 -0
  43. package/src/augments/telegram-transport/webhook.ts +56 -0
  44. package/src/augments/turn-control/index.ts +61 -0
  45. package/src/augments/turn-control/skill/SKILL.md +155 -0
  46. package/src/augments/visitor-auth/email-validation.ts +66 -0
  47. package/src/augments/visitor-auth/index.ts +779 -0
  48. package/src/augments/visitor-auth/rate-limiter.ts +90 -0
  49. package/src/augments/visitor-auth/skill/SKILL.md +55 -0
  50. package/src/augments/visitor-auth/storage/sqlite-store.ts +398 -0
  51. package/src/augments/visitor-auth/storage/types.ts +164 -0
  52. package/src/augments/visitor-auth/types.ts +123 -0
  53. package/src/augments/visitor-auth/verify-page.ts +179 -0
  54. package/src/augments/web-fetch/index.ts +331 -0
  55. package/src/augments/web-fetch/skill/SKILL.md +100 -0
  56. package/src/cli/agent-index.ts +289 -0
  57. package/src/cli/augment-catalog.ts +320 -0
  58. package/src/cli/augment-resolver.ts +597 -0
  59. package/src/cli/commands/add-skill.ts +194 -0
  60. package/src/cli/commands/add.ts +87 -0
  61. package/src/cli/commands/chat.ts +207 -0
  62. package/src/cli/commands/create.ts +462 -0
  63. package/src/cli/commands/dev.ts +139 -0
  64. package/src/cli/commands/eval.ts +180 -0
  65. package/src/cli/commands/ls.ts +66 -0
  66. package/src/cli/commands/remove.ts +95 -0
  67. package/src/cli/commands/restart.ts +40 -0
  68. package/src/cli/commands/start.ts +123 -0
  69. package/src/cli/commands/status.ts +104 -0
  70. package/src/cli/commands/stop.ts +84 -0
  71. package/src/cli/commands/visitors-revoke.ts +155 -0
  72. package/src/cli/commands/visitors.ts +101 -0
  73. package/src/cli/config-parser.ts +1034 -0
  74. package/src/cli/engine-resolver.ts +68 -0
  75. package/src/cli/index.ts +178 -0
  76. package/src/cli/model-picker.ts +89 -0
  77. package/src/cli/pid-registry.ts +146 -0
  78. package/src/cli/plist-generator.ts +117 -0
  79. package/src/cli/resolve-config.ts +56 -0
  80. package/src/cli/scaffold-skills.ts +158 -0
  81. package/src/cli/scaffold.ts +291 -0
  82. package/src/cli/skill-frontmatter.ts +51 -0
  83. package/src/cli/skill-validator.ts +151 -0
  84. package/src/cli/types.ts +228 -0
  85. package/src/cli/yaml-helpers.ts +66 -0
  86. package/src/engines/_shared/cost.ts +55 -0
  87. package/src/engines/_shared/schema-normalize.ts +75 -0
  88. package/src/engines/anthropic/pricing.ts +117 -0
  89. package/src/engines/anthropic.ts +483 -0
  90. package/src/engines/openai/pricing.ts +67 -0
  91. package/src/engines/openai.ts +446 -0
  92. package/src/engines/openrouter/pricing.ts +83 -0
  93. package/src/engines/openrouter.ts +185 -0
  94. package/src/helpers.ts +24 -0
  95. package/src/http.ts +387 -0
  96. package/src/index.ts +165 -0
  97. package/src/kernel/capability-table.ts +172 -0
  98. package/src/kernel/context-allocator.ts +161 -0
  99. package/src/kernel/history-manager.ts +198 -0
  100. package/src/kernel/lifecycle-manager.ts +106 -0
  101. package/src/kernel/output-validator.ts +35 -0
  102. package/src/kernel/preamble.ts +23 -0
  103. package/src/kernel/route-collector.ts +97 -0
  104. package/src/kernel/timeout.ts +21 -0
  105. package/src/kernel/tool-selector.ts +47 -0
  106. package/src/kernel/trace-emitter.ts +66 -0
  107. package/src/kernel/transport-queue.ts +147 -0
  108. package/src/kernel/turn-loop.ts +1148 -0
  109. package/src/memory/context-synthesis.ts +83 -0
  110. package/src/memory/memory-bus.ts +61 -0
  111. package/src/memory/registry.ts +80 -0
  112. package/src/memory/tools.ts +320 -0
  113. package/src/memory/types.ts +8 -0
  114. package/src/parts.ts +30 -0
  115. package/src/scaffold-templates/identity.md +31 -0
  116. package/src/telegram-client.ts +145 -0
  117. package/src/tokenizer.ts +14 -0
  118. package/src/transports/ag-ui-events.ts +253 -0
  119. package/src/transports/visitor-token.ts +82 -0
  120. package/src/transports/web-transport.ts +948 -0
  121. package/src/types.ts +1009 -0
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Scaffold-skill helpers — copy bundled `src/augments/<name>/skill/` folders
3
+ * into a scaffolded agent's `<agent-dir>/skills/<augment-name>/` directory,
4
+ * and render identity.md from the bundled template.
5
+ *
6
+ * Per ADR-025 (augment-as-folder + skill bundling) Decision 3 and ADR-030
7
+ * (model-facing skill surface separation): identity.md no longer carries
8
+ * the skill manifest. The runtime's `skills` augment surfaces the listing
9
+ * from each SKILL.md's YAML frontmatter; this module only handles disk-copy
10
+ * + template substitution for the three identity-level placeholders
11
+ * (AGENT_NAME, PURPOSE, OPERATOR_NAME).
12
+ */
13
+
14
+ import { cpSync, existsSync, readFileSync } from "node:fs";
15
+ import { join, resolve } from "node:path";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Augment-name resolution
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /**
22
+ * Map an augment `type` (the YAML `type:` field, e.g. `webFetch`) to the
23
+ * folder name under `src/augments/`. The folder convention is kebab-case;
24
+ * the type identifier is camelCase. This lookup is the inverse of the
25
+ * augment-resolver dispatch.
26
+ *
27
+ * Returned name doubles as the `<agent-dir>/skills/<name>/` directory name
28
+ * so the scaffolded layout matches what the runtime `skills` augment scans.
29
+ */
30
+ const TYPE_TO_AUGMENT_FOLDER: Record<string, string> = {
31
+ filesystem: "filesystem",
32
+ layeredMemory: "layered-memory",
33
+ webFetch: "web-fetch",
34
+ orgContext: "org-context",
35
+ bash: "bash",
36
+ notify: "notify",
37
+ turnControl: "turn-control",
38
+ visitorAuth: "visitor-auth",
39
+ // Augments below intentionally have no skill folder today (no model-callable
40
+ // tools, transport-only, or legacy). Listed for completeness so a lookup
41
+ // never silently returns undefined for a known type.
42
+ fileMemory: "file-memory",
43
+ supabaseMemory: "supabase-memory",
44
+ budgets: "budgets",
45
+ webTransport: "web-transport",
46
+ telegramTransport: "telegram-transport",
47
+ // ADR-030: the 'skills' augment is the runtime skill surface itself; it
48
+ // carries no SKILL.md of its own (the augment IS the listing).
49
+ skills: "skills",
50
+ };
51
+
52
+ /** Return the augment folder name for a YAML `type:` value, or `null` if unknown. */
53
+ export function augmentFolderForType(type: string): string | null {
54
+ return TYPE_TO_AUGMENT_FOLDER[type] ?? null;
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Bundled-skill source resolution
59
+ // ---------------------------------------------------------------------------
60
+
61
+ /**
62
+ * Resolve the on-disk location of the bundled `src/augments/<folder>/skill/`
63
+ * directory relative to this module. Returns `null` if the augment has no
64
+ * bundled skill folder.
65
+ */
66
+ function bundledSkillDir(folder: string): string | null {
67
+ const dir = resolve(import.meta.dir, "../augments", folder, "skill");
68
+ return existsSync(dir) ? dir : null;
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Skill copy
73
+ // ---------------------------------------------------------------------------
74
+
75
+ /**
76
+ * Copy the bundled skill folder for the given augment type into the agent
77
+ * directory. No-op when the augment has no bundled skill (e.g. budgets,
78
+ * file-memory, transport-only augments, the `skills` augment itself) or when
79
+ * the source folder is not present on disk.
80
+ *
81
+ * Idempotent: re-running overwrites existing files (per ADR-025 Decision 2 —
82
+ * operators opt into updates by re-scaffolding).
83
+ */
84
+ export function copyBundledSkill(type: string, agentDir: string): boolean {
85
+ const folder = TYPE_TO_AUGMENT_FOLDER[type];
86
+ if (!folder) return false;
87
+ const src = bundledSkillDir(folder);
88
+ if (!src) return false;
89
+ const dest = join(agentDir, "skills", folder);
90
+ cpSync(src, dest, { recursive: true });
91
+ return true;
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // identity.md template substitution
96
+ // ---------------------------------------------------------------------------
97
+
98
+ /**
99
+ * The on-disk path to the identity.md template shipped under
100
+ * `src/scaffold-templates/`. Resolved relative to this module so it works
101
+ * both from source (Bun) and from a bundle that preserves the layout.
102
+ */
103
+ const IDENTITY_TEMPLATE_PATH = resolve(import.meta.dir, "../scaffold-templates/identity.md");
104
+
105
+ /**
106
+ * Read the identity.md template once. The template is part of the source
107
+ * tree, never user-supplied, so failure to read it is a programming error.
108
+ */
109
+ function readIdentityTemplate(): string {
110
+ return readFileSync(IDENTITY_TEMPLATE_PATH, "utf-8");
111
+ }
112
+
113
+ export interface IdentityTemplateValues {
114
+ /** The agent's name as written in agent.yaml. */
115
+ agentName: string;
116
+ /** A one-sentence agent purpose ("a helpful assistant" by default). */
117
+ purpose: string;
118
+ /** The operator's name ("the operator" by default). */
119
+ operatorName: string;
120
+ }
121
+
122
+ /**
123
+ * Render identity.md from the bundled template using the provided values.
124
+ *
125
+ * Substitution targets three placeholder tokens by exact name (`{AGENT_NAME}`,
126
+ * `{PURPOSE}`, `{OPERATOR_NAME}`) — not a generic `{...}` regex — so
127
+ * operator-supplied values containing literal braces pass through unmodified.
128
+ * Single-pass replacement means an operator-supplied value containing
129
+ * `{AGENT_NAME}` or any other placeholder is emitted verbatim and is NOT
130
+ * re-scanned by a subsequent substitution.
131
+ *
132
+ * Per ADR-030: the previous `{SKILL_MANIFEST}` placeholder is gone. Skill
133
+ * discovery is the runtime `skills` augment's responsibility, not the
134
+ * identity file's.
135
+ *
136
+ * Limitation: operator values land inside markdown text. An operator who
137
+ * names their agent something that looks like a heading (`# Sneaky`) will
138
+ * break the document's outline — but only for their own identity.md, with
139
+ * no security impact (security rules sit outside the substitution targets
140
+ * and the `{OPERATOR_NAME}` reference is inline prose, not a structural
141
+ * element). Documented for posterity; not mitigated at v1.0.
142
+ */
143
+ export function renderIdentityFromTemplate(values: IdentityTemplateValues): string {
144
+ const template = readIdentityTemplate();
145
+
146
+ return template.replace(/\{(AGENT_NAME|PURPOSE|OPERATOR_NAME)\}/g, (match, token: string) => {
147
+ switch (token) {
148
+ case "AGENT_NAME":
149
+ return values.agentName;
150
+ case "PURPOSE":
151
+ return values.purpose;
152
+ case "OPERATOR_NAME":
153
+ return values.operatorName;
154
+ default:
155
+ return match;
156
+ }
157
+ });
158
+ }
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Scaffold — generates a new agent directory with auggy create.
3
+ *
4
+ * Creates the standard agent directory convention:
5
+ * <name>/
6
+ * agent.yaml Config (source of truth) — uses `identity:` shorthand
7
+ * .env Secrets template (gitignored)
8
+ * identity.md Who the agent is — security rules + skill manifest
9
+ * learned.md What the agent has learned (mutable)
10
+ * skills/ Skill folders (read-only fs mount), one per
11
+ * tool-providing augment, copied from src/augments/<name>/skill/
12
+ * workspace/ Agent's mutable workspace
13
+ * augments/ Custom augments directory
14
+ * .gitignore Ignores .env, workspace/, *.log, *.db, memory.sqlite
15
+ *
16
+ * Per ADR-025 (augment-as-folder + skill bundling) and the PR α foundation
17
+ * spec: scaffold copies bundled skills, uses the `identity:` YAML shorthand,
18
+ * includes layeredMemory by default, and writes identity.md from a template
19
+ * with the four security rules baked in.
20
+ */
21
+
22
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
23
+ import { join, resolve } from "node:path";
24
+ import { randomUUID } from "node:crypto";
25
+ import { copyBundledSkill, renderIdentityFromTemplate } from "./scaffold-skills";
26
+
27
+ export interface ScaffoldOptions {
28
+ /** Agent name. */
29
+ name: string;
30
+ /** Target directory (defaults to ./<name>). */
31
+ targetDir?: string;
32
+ /** Optional purpose string for the agent (default: "a helpful assistant"). */
33
+ purpose?: string;
34
+ /**
35
+ * Operator name used to populate the operator-identity reference in the
36
+ * scaffolded identity.md security rules and the agent.yaml `operators[]`
37
+ * array. Defaults to "the operator" — matches the security-eval test
38
+ * fixture's fallback behavior so non-interactive scaffolding stays
39
+ * compatible with the canonical eval suite.
40
+ */
41
+ operatorName?: string;
42
+ }
43
+
44
+ /** Default values used when prompts are skipped (non-interactive). */
45
+ const DEFAULT_OPERATOR_NAME = "the operator";
46
+ const DEFAULT_PURPOSE = "a helpful assistant";
47
+
48
+ /**
49
+ * Scaffold a new agent directory.
50
+ * Throws if the target directory already exists.
51
+ */
52
+ export function scaffoldAgent(opts: ScaffoldOptions): string {
53
+ const dir = resolve(opts.targetDir ?? `./${opts.name}`);
54
+
55
+ if (existsSync(dir)) {
56
+ throw new Error(`Directory already exists: ${dir}`);
57
+ }
58
+
59
+ const id = `aug1_${randomUUID()}`;
60
+ const purpose = opts.purpose ?? DEFAULT_PURPOSE;
61
+ const operatorName = opts.operatorName ?? DEFAULT_OPERATOR_NAME;
62
+
63
+ // The augment types this scaffold installs by default. Drives the bundled-
64
+ // skill copy (which copies SKILL.md files into <agent>/skills/<augment>/).
65
+ // The runtime `skills` augment surfaces them to the model from disk at
66
+ // every context() call — no longer threaded into identity.md per ADR-030.
67
+ const augmentTypes = [
68
+ "fileMemory", // identity (mounted via shorthand) + learned.md
69
+ "filesystem",
70
+ "layeredMemory",
71
+ "budgets",
72
+ "webFetch",
73
+ "turnControl",
74
+ "webTransport",
75
+ ];
76
+
77
+ // Tool-providing types whose bundled skills should be copied. Subset of
78
+ // augmentTypes — fileMemory / budgets / webTransport contribute no tools.
79
+ const skillProvidingTypes = augmentTypes.filter((t) =>
80
+ ["filesystem", "layeredMemory", "webFetch", "turnControl"].includes(t),
81
+ );
82
+
83
+ // Create directory structure.
84
+ mkdirSync(dir, { recursive: true });
85
+ mkdirSync(join(dir, "skills"), { recursive: true });
86
+ mkdirSync(join(dir, "workspace"), { recursive: true });
87
+ mkdirSync(join(dir, "augments"), { recursive: true });
88
+
89
+ // Copy bundled skill folders for each tool-providing augment. Idempotent —
90
+ // re-running the scaffold overwrites; per ADR-025 Decision 2 operators opt
91
+ // into updates by re-scaffolding.
92
+ for (const type of skillProvidingTypes) {
93
+ copyBundledSkill(type, dir);
94
+ }
95
+
96
+ // Write identity.md from the bundled template (security rules only —
97
+ // skill manifest moved out per ADR-030).
98
+ writeFileSync(
99
+ join(dir, "identity.md"),
100
+ renderIdentityFromTemplate({
101
+ agentName: opts.name,
102
+ purpose,
103
+ operatorName,
104
+ }),
105
+ );
106
+
107
+ // Write learned.md (empty, agent appends as it learns).
108
+ writeFileSync(join(dir, "learned.md"), "");
109
+
110
+ // Write agent.yaml using the identity: shorthand (per α-5).
111
+ writeFileSync(join(dir, "agent.yaml"), agentYamlTemplate(id, opts.name, purpose, operatorName));
112
+
113
+ // Write .env with empty values — operator fills in secrets before first run.
114
+ writeFileSync(join(dir, ".env"), ENV_TEMPLATE);
115
+
116
+ // Write .gitignore.
117
+ writeFileSync(join(dir, ".gitignore"), GITIGNORE_TEMPLATE);
118
+
119
+ return dir;
120
+ }
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Templates
124
+ // ---------------------------------------------------------------------------
125
+
126
+ /**
127
+ * Render a string as a YAML-safe scalar. Operator-supplied free-text values
128
+ * (purpose, operatorName) reach this function; without escaping, a value
129
+ * containing a quote, newline, or backslash would corrupt the generated
130
+ * agent.yaml. JSON-encoded strings are valid YAML scalars in flow shape,
131
+ * so JSON.stringify produces a double-quoted, escaped form that YAML parses
132
+ * back to the original string.
133
+ *
134
+ * The interactive `auggy create` flow already routes operator input through
135
+ * yaml.stringify; this helper closes the parallel gap in `scaffoldAgent`,
136
+ * the programmatic entry point used by tests and any third-party scaffolder.
137
+ */
138
+ function yamlScalar(s: string): string {
139
+ return JSON.stringify(s);
140
+ }
141
+
142
+ function agentYamlTemplate(
143
+ id: string,
144
+ name: string,
145
+ purpose: string,
146
+ operatorName: string,
147
+ ): string {
148
+ return `# Agent configuration — the source of truth for this Auggy agent.
149
+ # See docs at augment-1/docs/ for field reference.
150
+
151
+ id: ${yamlScalar(id)}
152
+ name: ${yamlScalar(name)}
153
+ purpose: ${yamlScalar(purpose)}
154
+ operators:
155
+ - ${yamlScalar(operatorName)}
156
+
157
+ # identity shorthand — synthesizes a fileMemory@system entry from ./identity.md
158
+ # at parse time. Operators wanting non-default options (e.g. mutable: true)
159
+ # should drop this and add an explicit fileMemory augment instead.
160
+ identity: ./identity.md
161
+
162
+ engine:
163
+ provider: anthropic # or: openai, openrouter
164
+ model: claude-sonnet-4-6 # openai: gpt-5 | openrouter: qwen/qwen3.5-397b-a17b
165
+ maxContextTokens: 200000 # for openrouter, set per-model — defaults vary
166
+ maxTokens: 4096 # sent as max_completion_tokens for openai/openrouter
167
+ # reasoningEffort: medium # optional: none|minimal|low|medium|high|xhigh
168
+ # providerRouting: # openrouter only — slugs not semantically validated
169
+ # only: [OpenAI]
170
+ # sort: price
171
+
172
+ settings:
173
+ compactionStrategy: truncate
174
+ maxInferenceLoops: 10
175
+
176
+ augments:
177
+ - name: learned
178
+ type: fileMemory
179
+ options:
180
+ label: learned
181
+ source: ./learned.md
182
+ mutable: true
183
+ origin: system
184
+ priority: high
185
+ placement: preamble
186
+ eviction: drop
187
+
188
+ - name: memory
189
+ type: layeredMemory
190
+ options:
191
+ backend: sqlite
192
+ namespace: ${yamlScalar(name)}
193
+ dbPath: ./memory.sqlite
194
+ retentionDays: 90
195
+
196
+ - name: budgets
197
+ type: budgets
198
+ options:
199
+ dbPath: ./budgets.db
200
+ caps:
201
+ public:
202
+ recognized:
203
+ maxTurnsPerThread: 20
204
+ maxTurnsPerDay: 50
205
+ maxUsdPerDay: 1
206
+ anonymous:
207
+ maxTurnsPerThread: 5
208
+ anonymousGlobalLimit: 30
209
+ dailyBudgetUsd: 5
210
+
211
+ - name: files
212
+ type: filesystem
213
+ options:
214
+ mounts:
215
+ - name: skills
216
+ path: ./skills
217
+ writable: false
218
+ - name: workspace
219
+ path: ./workspace
220
+ writable: true
221
+ deletable: true
222
+
223
+ # ADR-030: surfaces the skill manifest to the model (name + description from
224
+ # each SKILL.md's YAML frontmatter). Activation is fs_read via the filesystem
225
+ # mount above. Required for the model to discover its skills.
226
+ - name: skills
227
+ type: skills
228
+ options:
229
+ dir: ./skills
230
+
231
+ - name: fetch
232
+ type: webFetch
233
+ options:
234
+ timeoutMs: 15000
235
+
236
+ - name: turn-control
237
+ type: turnControl
238
+
239
+ - name: web
240
+ type: webTransport
241
+ options:
242
+ port: 8080
243
+ auth:
244
+ type: bearer
245
+ token: \${AUGGY_WEB_TOKEN}
246
+ visitorTokens:
247
+ # signingKey is NOT set here — visitorAuth is the single source of truth
248
+ # and the resolver injects it at boot. Setting it here would trigger the
249
+ # duplicate-key warning on every start.
250
+ agentBinding: \${AUGGY_AGENT_ID}
251
+ `;
252
+ }
253
+
254
+ const ENV_TEMPLATE = `# Auggy agent secrets — this file is gitignored.
255
+ # Add your API keys and tokens here. Only the key matching the
256
+ # configured engine.provider in agent.yaml needs to be filled in.
257
+
258
+ ANTHROPIC_API_KEY=
259
+ # OPENAI_API_KEY=
260
+ # OPENROUTER_API_KEY=
261
+ AUGGY_WEB_TOKEN=
262
+ # Uncomment when visitorAuth augment is added (signingKey is owned by visitorAuth,
263
+ # injected into webTransport at boot — no need to set it in webTransport's config).
264
+ # VISITOR_SIGNING_KEY=
265
+ # Stable identifier for visitor-auth tokens — must be unique per agent
266
+ # if multiple agents share VISITOR_SIGNING_KEY (otherwise tokens are
267
+ # cross-replayable). Pattern: short slug or the agent's id.
268
+ AUGGY_AGENT_ID=
269
+ # SUPABASE_URL=
270
+ # SUPABASE_SERVICE_KEY=
271
+ `;
272
+
273
+ const GITIGNORE_TEMPLATE = `.env
274
+ .env.local
275
+ workspace/
276
+ *.log
277
+ *.err
278
+ node_modules/
279
+ memory.sqlite
280
+ memory.sqlite-journal
281
+ memory.sqlite-wal
282
+ memory.sqlite-shm
283
+ memory.db
284
+ memory.db-journal
285
+ memory.db-wal
286
+ memory.db-shm
287
+ budgets.db
288
+ budgets.db-journal
289
+ budgets.db-wal
290
+ budgets.db-shm
291
+ `;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Skill frontmatter reader for ADR-030.
3
+ *
4
+ * Reads the YAML frontmatter block from a SKILL.md file. Returns null when the
5
+ * frontmatter is absent, malformed, or missing required fields — callers
6
+ * always have a graceful-fallback path (skill is not listed) rather than a
7
+ * crash. The boot-time skill validator (`skill-validator.ts`) catches the
8
+ * "tool-providing augment with no parseable frontmatter" case separately.
9
+ *
10
+ * Source of truth for skill `name` + `description` per agentskills.io.
11
+ */
12
+
13
+ import { readFileSync } from "node:fs";
14
+ import { parse as parseYaml } from "yaml";
15
+
16
+ export interface SkillFrontmatter {
17
+ name: string;
18
+ description: string;
19
+ }
20
+
21
+ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n/;
22
+
23
+ export function parseSkillFrontmatter(content: string): SkillFrontmatter | null {
24
+ const match = content.match(FRONTMATTER_RE);
25
+ if (!match) return null;
26
+ const raw = match[1]!;
27
+
28
+ let parsed: unknown;
29
+ try {
30
+ parsed = parseYaml(raw);
31
+ } catch {
32
+ return null;
33
+ }
34
+
35
+ if (parsed === null || typeof parsed !== "object") return null;
36
+ const obj = parsed as Record<string, unknown>;
37
+ if (typeof obj.name !== "string" || obj.name.length === 0) return null;
38
+ if (typeof obj.description !== "string" || obj.description.length === 0) return null;
39
+
40
+ return { name: obj.name, description: obj.description };
41
+ }
42
+
43
+ export function readSkillFrontmatter(path: string): SkillFrontmatter | null {
44
+ let content: string;
45
+ try {
46
+ content = readFileSync(path, "utf-8");
47
+ } catch {
48
+ return null;
49
+ }
50
+ return parseSkillFrontmatter(content);
51
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Boot-time skill validator.
3
+ *
4
+ * Per ADR-025 Decision 5 + PR α foundation spec §H. After augments are
5
+ * resolved at agent boot, scan: for each augment that contributes tools
6
+ * to the model but lacks a corresponding `<agent-dir>/skills/<augment-
7
+ * folder>/SKILL.md`, emit a one-line warning. Operators see the gap at
8
+ * startup, not in production-failure mode where the model guesses.
9
+ *
10
+ * Discriminator (model-perspective): an augment contributes tools if EITHER
11
+ * (a) `augment.tools.length > 0` — tools declared on the factory return,
12
+ * (b) `augment.memory?.owns?.kind === "namespace"` — namespace memory
13
+ * provider; the kernel-synthesized memory-bus exposes 5 generic
14
+ * tools (memory_read / memory_write / memory_search / memory_list /
15
+ * memory_forget — see src/memory/tools.ts) keyed off its prefix.
16
+ *
17
+ * The model can't tell where the tools came from; from its perspective
18
+ * both routes produce model-callable tools that need teaching. The strict
19
+ * spec wording ("non-empty tools[]") would have missed (b), and (b) is
20
+ * the most common real case — layered-memory is the default-scaffold
21
+ * memory augment.
22
+ *
23
+ * Tool-less augments (fileMemory + supabaseMemory static providers,
24
+ * transports, budgets) intentionally do NOT trigger the warning — they
25
+ * contribute only `context()` blocks or admission gates, no model-
26
+ * callable tools.
27
+ *
28
+ * Warning, not error. The agent still boots successfully. An opt-out
29
+ * flag is deferred per spec §Decision 7.
30
+ */
31
+
32
+ /**
33
+ * Number of tools the kernel memory-bus synthesizes for a namespace
34
+ * memory provider. Source: src/memory/tools.ts (memory_read / memory_write
35
+ * / memory_search / memory_list / memory_forget). If that surface changes,
36
+ * update this constant — tests asserting the count will catch drift.
37
+ */
38
+ const NAMESPACE_MEMORY_TOOL_COUNT = 5;
39
+
40
+ import { statSync } from "node:fs";
41
+ import { join } from "node:path";
42
+ import type { Augment } from "../types";
43
+ import type { AugmentConfig } from "./types";
44
+ import { augmentFolderForType } from "./scaffold-skills";
45
+
46
+ /**
47
+ * Filesystem probe outcome for the skill SKILL.md file.
48
+ *
49
+ * - "present": file exists and is readable as a regular file.
50
+ * - "missing": parent dir is reachable; SKILL.md is absent (ENOENT). The
51
+ * normal "operator hasn't installed this skill" case.
52
+ * - "unreadable": stat surfaced a non-ENOENT error (e.g. EACCES). The
53
+ * skill MAY be present on disk but the runtime cannot confirm; surfaced
54
+ * as a different warning class so an operator with a misconfigured
55
+ * permissions setup doesn't get fooled into thinking the skill is there.
56
+ */
57
+ type SkillProbe =
58
+ | { kind: "present" }
59
+ | { kind: "missing" }
60
+ | { kind: "unreadable"; reason: string };
61
+
62
+ function probeSkillFile(skillPath: string): SkillProbe {
63
+ try {
64
+ const stats = statSync(skillPath);
65
+ if (stats.isFile()) return { kind: "present" };
66
+ // SKILL.md exists but is a directory or other non-file — surface as
67
+ // unreadable rather than silently treating as present. Rare misconfig.
68
+ return { kind: "unreadable", reason: `path is not a regular file` };
69
+ } catch (err) {
70
+ const code = (err as NodeJS.ErrnoException).code;
71
+ if (code === "ENOENT") return { kind: "missing" };
72
+ return {
73
+ kind: "unreadable",
74
+ reason: `${code ?? "unknown"}: ${(err as Error).message}`,
75
+ };
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Scan resolved augments paired with their source configs and warn for
81
+ * any tool-providing augment whose skill is missing or unreadable. Walks
82
+ * configs and augments in lockstep — `resolveAugments` preserves order
83
+ * and never drops entries, so index alignment is exact.
84
+ *
85
+ * Hook point: called from `resolveAugments` after all augment factories
86
+ * have run, so the agent's tool surface is finalized before the warning
87
+ * decision is made.
88
+ */
89
+ export function validateBundledSkills(
90
+ configs: AugmentConfig[],
91
+ augments: Augment[],
92
+ agentDir: string,
93
+ ): void {
94
+ // Defensive: if the caller passed mismatched arrays, skip validation
95
+ // entirely rather than emit confusing warnings against the wrong
96
+ // augment. resolveAugments today always produces matched pairs.
97
+ if (configs.length !== augments.length) return;
98
+
99
+ for (let i = 0; i < augments.length; i++) {
100
+ const aug = augments[i]!;
101
+ const cfg = configs[i]!;
102
+
103
+ const factoryToolCount = aug.tools?.length ?? 0;
104
+ const isNamespaceMemory = aug.memory?.owns?.kind === "namespace";
105
+ const toolCount = factoryToolCount + (isNamespaceMemory ? NAMESPACE_MEMORY_TOOL_COUNT : 0);
106
+ if (toolCount === 0) continue;
107
+
108
+ // Custom augments (operator-authored) do not have a bundled skill folder
109
+ // by convention; the operator owns their own teaching. Don't warn —
110
+ // the `auggy add-skill` command is built-in-only too.
111
+ if (cfg.type === "custom") continue;
112
+
113
+ const folder = augmentFolderForType(cfg.type);
114
+ if (!folder) {
115
+ // Unknown built-in type. resolveAugments would have thrown earlier;
116
+ // this branch only fires if the type-to-folder map drifts from
117
+ // the resolver's switch. Emit a diagnostic so the drift is visible.
118
+ console.warn(
119
+ `[augment-resolver] augment "${aug.name}" (type "${cfg.type}") exposes ${toolCount} ` +
120
+ `tool${toolCount === 1 ? "" : "s"} but has no folder mapping in scaffold-skills. ` +
121
+ `Skill validation skipped — file an issue if this augment ships skill content.`,
122
+ );
123
+ continue;
124
+ }
125
+
126
+ const skillPath = join(agentDir, "skills", folder, "SKILL.md");
127
+ const probe = probeSkillFile(skillPath);
128
+ if (probe.kind === "present") continue;
129
+
130
+ if (probe.kind === "missing") {
131
+ console.warn(
132
+ `[augment-resolver] augment "${folder}" exposes ${toolCount} tool${
133
+ toolCount === 1 ? "" : "s"
134
+ } with no skill mounted at\n` +
135
+ ` ${skillPath}. Model will guess at tool usage.\n` +
136
+ ` Run \`auggy add-skill ${folder}\` to install the bundled teaching, OR copy\n` +
137
+ ` src/augments/${folder}/skill/* into the agent's skills/${folder}/ directory.`,
138
+ );
139
+ continue;
140
+ }
141
+
142
+ // probe.kind === "unreadable"
143
+ console.warn(
144
+ `[augment-resolver] augment "${folder}" exposes ${toolCount} tool${
145
+ toolCount === 1 ? "" : "s"
146
+ } and a skill file at\n` +
147
+ ` ${skillPath} is unreadable (${probe.reason}).\n` +
148
+ ` Skill teaching cannot be confirmed; check filesystem permissions.`,
149
+ );
150
+ }
151
+ }