@agentplate/cli 1.0.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 (139) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/LICENSE +21 -0
  3. package/README.md +206 -0
  4. package/agents/architect.md +108 -0
  5. package/agents/builder.md +97 -0
  6. package/agents/coordinator.md +113 -0
  7. package/agents/deployer.md +117 -0
  8. package/agents/devops.md +114 -0
  9. package/agents/lead.md +107 -0
  10. package/agents/merger.md +103 -0
  11. package/agents/reviewer.md +90 -0
  12. package/agents/scout.md +95 -0
  13. package/agents/verifier.md +106 -0
  14. package/package.json +64 -0
  15. package/src/agents/guard-rules.ts +55 -0
  16. package/src/agents/identity.test.ts +161 -0
  17. package/src/agents/identity.ts +229 -0
  18. package/src/agents/manifest.test.ts +260 -0
  19. package/src/agents/manifest.ts +286 -0
  20. package/src/agents/overlay.test.ts +190 -0
  21. package/src/agents/overlay.ts +212 -0
  22. package/src/agents/system-prompt.test.ts +53 -0
  23. package/src/agents/system-prompt.ts +95 -0
  24. package/src/agents/turn-runner.ts +79 -0
  25. package/src/commands/coordinator.test.ts +75 -0
  26. package/src/commands/coordinator.ts +259 -0
  27. package/src/commands/deploy.test.ts +504 -0
  28. package/src/commands/deploy.ts +874 -0
  29. package/src/commands/doctor.test.ts +106 -0
  30. package/src/commands/doctor.ts +208 -0
  31. package/src/commands/init.ts +71 -0
  32. package/src/commands/log.ts +51 -0
  33. package/src/commands/mail.ts +197 -0
  34. package/src/commands/merge.ts +127 -0
  35. package/src/commands/model.ts +58 -0
  36. package/src/commands/prime.ts +61 -0
  37. package/src/commands/reap.ts +87 -0
  38. package/src/commands/serve.ts +61 -0
  39. package/src/commands/setup.ts +48 -0
  40. package/src/commands/ship.test.ts +106 -0
  41. package/src/commands/ship.ts +202 -0
  42. package/src/commands/skill.test.ts +458 -0
  43. package/src/commands/skill.ts +730 -0
  44. package/src/commands/sling.ts +365 -0
  45. package/src/commands/status.ts +60 -0
  46. package/src/commands/stop.ts +56 -0
  47. package/src/commands/tui.ts +199 -0
  48. package/src/commands/worktree.ts +77 -0
  49. package/src/config.test.ts +92 -0
  50. package/src/config.ts +202 -0
  51. package/src/db/sqlite.test.ts +77 -0
  52. package/src/db/sqlite.ts +102 -0
  53. package/src/deploy/audit.test.ts +233 -0
  54. package/src/deploy/audit.ts +245 -0
  55. package/src/deploy/context.test.ts +243 -0
  56. package/src/deploy/context.ts +72 -0
  57. package/src/deploy/registry.test.ts +101 -0
  58. package/src/deploy/registry.ts +86 -0
  59. package/src/deploy/secrets.test.ts +129 -0
  60. package/src/deploy/secrets.ts +69 -0
  61. package/src/deploy/targets/docker-gha.test.ts +323 -0
  62. package/src/deploy/targets/docker-gha.ts +841 -0
  63. package/src/deploy/types.ts +153 -0
  64. package/src/errors.test.ts +42 -0
  65. package/src/errors.ts +69 -0
  66. package/src/events/store.test.ts +183 -0
  67. package/src/events/store.ts +201 -0
  68. package/src/index.ts +137 -0
  69. package/src/insights/quality-gates.ts +73 -0
  70. package/src/json.test.ts +28 -0
  71. package/src/json.ts +50 -0
  72. package/src/logging/color.ts +62 -0
  73. package/src/logging/logger.ts +60 -0
  74. package/src/logging/sanitizer.test.ts +36 -0
  75. package/src/logging/sanitizer.ts +57 -0
  76. package/src/mail/client.test.ts +192 -0
  77. package/src/mail/client.ts +188 -0
  78. package/src/mail/store.test.ts +279 -0
  79. package/src/mail/store.ts +311 -0
  80. package/src/merge/lock.test.ts +88 -0
  81. package/src/merge/lock.ts +84 -0
  82. package/src/merge/queue.test.ts +136 -0
  83. package/src/merge/queue.ts +177 -0
  84. package/src/merge/resolver.test.ts +219 -0
  85. package/src/merge/resolver.ts +274 -0
  86. package/src/paths.ts +36 -0
  87. package/src/providers/apply.test.ts +90 -0
  88. package/src/providers/apply.ts +66 -0
  89. package/src/providers/registry.test.ts +74 -0
  90. package/src/providers/registry.ts +254 -0
  91. package/src/runtimes/claude.ts +313 -0
  92. package/src/runtimes/codex.ts +280 -0
  93. package/src/runtimes/cursor.ts +247 -0
  94. package/src/runtimes/gemini.ts +173 -0
  95. package/src/runtimes/mock.ts +71 -0
  96. package/src/runtimes/opencode.ts +259 -0
  97. package/src/runtimes/registry.test.ts +924 -0
  98. package/src/runtimes/registry.ts +63 -0
  99. package/src/runtimes/resolve.ts +45 -0
  100. package/src/runtimes/types.ts +97 -0
  101. package/src/scaffold.ts +68 -0
  102. package/src/secrets.test.ts +51 -0
  103. package/src/secrets.ts +78 -0
  104. package/src/serve/api.ts +667 -0
  105. package/src/serve/server.test.ts +433 -0
  106. package/src/serve/server.ts +271 -0
  107. package/src/serve/system.ts +90 -0
  108. package/src/serve/weather.ts +140 -0
  109. package/src/sessions/reaper.test.ts +162 -0
  110. package/src/sessions/reaper.ts +149 -0
  111. package/src/sessions/store.test.ts +351 -0
  112. package/src/sessions/store.ts +350 -0
  113. package/src/skills/distiller.test.ts +498 -0
  114. package/src/skills/distiller.ts +426 -0
  115. package/src/skills/feedback.test.ts +300 -0
  116. package/src/skills/feedback.ts +168 -0
  117. package/src/skills/lifecycle.ts +169 -0
  118. package/src/skills/retrieval.test.ts +421 -0
  119. package/src/skills/retrieval.ts +365 -0
  120. package/src/skills/safety.test.ts +335 -0
  121. package/src/skills/safety.ts +216 -0
  122. package/src/skills/store.test.ts +425 -0
  123. package/src/skills/store.ts +684 -0
  124. package/src/skills/types.ts +107 -0
  125. package/src/types.ts +442 -0
  126. package/src/utils/detect.test.ts +35 -0
  127. package/src/utils/detect.ts +82 -0
  128. package/src/version.test.ts +19 -0
  129. package/src/version.ts +7 -0
  130. package/src/wizard/setup.ts +254 -0
  131. package/src/worktree/manager.test.ts +181 -0
  132. package/src/worktree/manager.ts +229 -0
  133. package/templates/overlay.md.tmpl +102 -0
  134. package/ui/dist/assets/index-C7rXIMER.css +1 -0
  135. package/ui/dist/assets/index-W4kbr4by.js +4526 -0
  136. package/ui/dist/favicon.svg +21 -0
  137. package/ui/dist/index.html +16 -0
  138. package/ui/dist/logo-clay.svg +21 -0
  139. package/ui/dist/logo.svg +18 -0
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Self-improving skills — domain types.
3
+ *
4
+ * A **Skill** is a reusable, versioned, executable playbook distilled from a
5
+ * successful task: a goal, when-to-use signals, ordered steps (prose + concrete
6
+ * command snippets), file-pattern hints, gotchas, and an earned confidence track
7
+ * record. Skills are the unit of Agentplate's closed learning loop:
8
+ *
9
+ * retrieve (at spawn) → apply (agent uses them) → distill (at session-end,
10
+ * gated on quality gates) → feedback (append outcome, evolve confidence).
11
+ *
12
+ * On disk each skill is a directory `.agentplate/skills/<slug>/` containing
13
+ * `skill.md` (YAML frontmatter + markdown body — the source of truth) and
14
+ * `outcomes.jsonl` (append-only outcome log). A derived, gitignored SQLite FTS
15
+ * index (`skills.db`) accelerates retrieval and is rebuildable from the files.
16
+ */
17
+
18
+ import type { OutcomeStatus } from "../types.ts";
19
+
20
+ /** Lifecycle status. `quarantined`/`deprecated` skills are excluded from retrieval. */
21
+ export type SkillStatus = "active" | "deprecated" | "quarantined";
22
+
23
+ /** Provenance of a distilled skill. */
24
+ export interface SkillProvenance {
25
+ taskId: string | null;
26
+ agent: string;
27
+ commit: string | null;
28
+ }
29
+
30
+ /** A reusable, versioned playbook. */
31
+ export interface Skill {
32
+ /** Stable content-independent id. */
33
+ id: string;
34
+ /** URL/dir-safe identifier (the directory name). */
35
+ slug: string;
36
+ title: string;
37
+ /** Bumped on every UPDATE distillation. */
38
+ version: number;
39
+ status: SkillStatus;
40
+ /** One-line statement of what applying this skill accomplishes. */
41
+ goal: string;
42
+ /** Preconditions / retrieval signals ("when should an agent use this?"). */
43
+ whenToUse: string[];
44
+ /** Glob patterns of files this skill is relevant to (file-overlap ranking). */
45
+ filePatterns: string[];
46
+ tags: string[];
47
+ created: string;
48
+ updatedAt: string;
49
+ distilledFrom?: SkillProvenance;
50
+ /** Slugs of related skills. */
51
+ relatesTo: string[];
52
+ /** Slugs this skill structurally replaces. */
53
+ supersedes: string[];
54
+ /** Markdown body after the frontmatter (steps / gotchas / verification). */
55
+ body: string;
56
+
57
+ // --- derived (computed from outcomes.jsonl; never hand-edited) ---
58
+ /** 0..1 confidence (Wilson lower bound over weighted outcomes). */
59
+ confidence: number;
60
+ appliedCount: number;
61
+ successCount: number;
62
+ lastOutcome: OutcomeStatus | null;
63
+ }
64
+
65
+ /** One appended line in a skill's `outcomes.jsonl`. */
66
+ export interface SkillOutcome {
67
+ status: OutcomeStatus;
68
+ agent: string;
69
+ taskId: string | null;
70
+ /** Quality-gate status that produced this outcome (null if gates not run). */
71
+ gates: OutcomeStatus | null;
72
+ ts: string;
73
+ note?: string;
74
+ }
75
+
76
+ /**
77
+ * Output of the AI distiller, pre-validation. `skip` is first-class — most
78
+ * sessions should NOT mint a skill, preventing skill spam.
79
+ */
80
+ export interface SkillDraft {
81
+ action: "create" | "update" | "skip";
82
+ /** Required when action is "update": the slug to update. */
83
+ targetSlug?: string;
84
+ title?: string;
85
+ goal?: string;
86
+ whenToUse?: string[];
87
+ filePatterns?: string[];
88
+ tags?: string[];
89
+ body?: string;
90
+ }
91
+
92
+ /** A skill plus its relevance score (retrieval output). */
93
+ export interface RankedSkill {
94
+ skill: Skill;
95
+ score: number;
96
+ }
97
+
98
+ /**
99
+ * Written to `.agentplate/agents/<name>/applied-skills.json` at spawn so the
100
+ * session-end feedback step knows which skills to score.
101
+ */
102
+ export interface AppliedSkillsRecord {
103
+ taskId: string;
104
+ agent: string;
105
+ capability: string;
106
+ skills: Array<{ id: string; slug: string; injected: "full" | "summary" }>;
107
+ }
package/src/types.ts ADDED
@@ -0,0 +1,442 @@
1
+ /**
2
+ * Shared types and interfaces for Agentplate.
3
+ *
4
+ * ALL cross-module types live here so there is a single import site and no
5
+ * circular dependencies between feature modules. Feature-local types that are
6
+ * never shared may live in their own module, but anything referenced by more
7
+ * than one subsystem belongs here.
8
+ */
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Configuration
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /** Top-level Agentplate project configuration (`.agentplate/config.yaml`). */
15
+ export interface AgentplateConfig {
16
+ project: ProjectConfig;
17
+ runtime: RuntimeConfig;
18
+ /** Id of the active provider (a key into {@link AgentplateConfig.providers}). */
19
+ activeProvider: string;
20
+ providers: Record<string, ProviderConfig>;
21
+ agents: AgentsConfig;
22
+ merge: MergeConfig;
23
+ skills: SkillsConfig;
24
+ deploy: DeployConfig;
25
+ logging: LoggingConfig;
26
+ }
27
+
28
+ /** Build → CI/CD → Deploy configuration. */
29
+ export interface DeployConfig {
30
+ /** Default deploy target id (e.g. "docker-gha"); empty until configured. */
31
+ default: string;
32
+ /** Per-target non-secret settings + secret env-var bindings. */
33
+ targets: Record<string, DeployTargetConfig>;
34
+ /** Per-environment gate policy: "confirm" requires approval, "auto" does not. */
35
+ gates: Record<string, "confirm" | "auto">;
36
+ }
37
+
38
+ /** Non-secret settings + secret bindings for one deploy target. */
39
+ export interface DeployTargetConfig {
40
+ /** Arbitrary non-secret settings (region, registry, cluster, app name…). */
41
+ settings: Record<string, string | number | boolean>;
42
+ /** Secret bindings: logical key → { fromEnv: ENV_VAR_NAME } (no values). */
43
+ secretEnv: Record<string, { fromEnv: string }>;
44
+ /** Environments this target deploys to. */
45
+ environments: string[];
46
+ }
47
+
48
+ /** Self-improving skills behavior. */
49
+ export interface SkillsConfig {
50
+ /** Master switch for retrieval + distillation. */
51
+ enabled: boolean;
52
+ /** Retrieval budget + count caps. */
53
+ retrieval: {
54
+ /** Max characters of skills injected into an overlay. */
55
+ budgetChars: number;
56
+ /** Max number of full-body skills injected (rest become summaries). */
57
+ maxFull: number;
58
+ };
59
+ /** Distillation behavior. */
60
+ distill: {
61
+ /** Only distill when quality gates pass (recommended). */
62
+ onlyOnGatesPass: boolean;
63
+ /** Model override for the distiller (null = runtime default). */
64
+ model: string | null;
65
+ };
66
+ /** Auto-pruning thresholds. */
67
+ prune: {
68
+ /** Quarantine when confidence drops below this (with >= minSamples). */
69
+ quarantineBelow: number;
70
+ minSamples: number;
71
+ /** Delete quarantined skills older than this many days. */
72
+ maxAgeDays: number;
73
+ };
74
+ }
75
+
76
+ /** Identity and git context for the project being orchestrated. */
77
+ export interface ProjectConfig {
78
+ /** Human-readable project name (auto-detected at init). */
79
+ name: string;
80
+ /** Absolute path to the project root. */
81
+ root: string;
82
+ /** Branch agent work is ultimately merged into (e.g. "main"). */
83
+ canonicalBranch: string;
84
+ /** Commands run at session-end to score an agent's work (test/lint/typecheck). */
85
+ qualityGates?: QualityGate[];
86
+ }
87
+
88
+ /** A single quality gate: a named command whose exit code determines pass/fail. */
89
+ export interface QualityGate {
90
+ name: string;
91
+ command: string;
92
+ description?: string;
93
+ }
94
+
95
+ /** Which coding-agent runtime drives workers, and per-capability overrides. */
96
+ export interface RuntimeConfig {
97
+ /** Default runtime adapter id (e.g. "claude"). */
98
+ default: string;
99
+ /** Optional per-capability runtime overrides. */
100
+ capabilities?: Partial<Record<Capability, string>>;
101
+ }
102
+
103
+ /**
104
+ * How a provider's credentials are obtained:
105
+ * - `subscription` — the runtime CLI's own login (e.g. Claude Pro/Max OAuth,
106
+ * `codex login`, `gcloud`/`gemini` auth). Agentplate stores NO key; auth is
107
+ * delegated to the already-logged-in CLI.
108
+ * - `api-key` — a key Agentplate stores in the gitignored secrets file.
109
+ * - `env` — a key Agentplate reads from an existing environment
110
+ * variable at run time (never stored).
111
+ * - `none` — local/keyless provider (e.g. Ollama).
112
+ */
113
+ export type AuthMode = "subscription" | "api-key" | "env" | "none";
114
+
115
+ /**
116
+ * An AI provider (LLM backend). `native` providers are reached through the
117
+ * runtime's own auth; `gateway` providers route through a base URL with a
118
+ * bearer token read from the named environment variable.
119
+ */
120
+ export interface ProviderConfig {
121
+ type: "native" | "gateway";
122
+ /** How credentials are obtained for this provider. */
123
+ authMode?: AuthMode;
124
+ /** Base URL for gateway providers. */
125
+ baseUrl?: string;
126
+ /** Name of the env var holding the auth token (value never stored in config). */
127
+ authTokenEnv?: string;
128
+ /** Default model id for this provider. */
129
+ model?: string;
130
+ }
131
+
132
+ /** Orchestration limits and agent registry locations. */
133
+ export interface AgentsConfig {
134
+ /** Path to the agent manifest, relative to project root. */
135
+ manifestPath: string;
136
+ /** Directory holding deployed base agent definitions, relative to root. */
137
+ baseDir: string;
138
+ /** Maximum agents running concurrently across the whole fleet. */
139
+ maxConcurrent: number;
140
+ /** Maximum delegation depth (coordinator → lead → worker = 2). */
141
+ maxDepth: number;
142
+ /** Maximum children a single lead may spawn. */
143
+ maxAgentsPerLead: number;
144
+ /**
145
+ * Terminate a worker after this many minutes with no activity (no streamed
146
+ * events and not between-turn progress) — the session is marked `stopped`, its
147
+ * process killed, and its worktree removed. The coordinator is never reaped.
148
+ * `0` disables idle reaping. Default 10.
149
+ */
150
+ idleTimeoutMinutes: number;
151
+ }
152
+
153
+ /** Conflict-resolution behavior for merging agent branches. */
154
+ export interface MergeConfig {
155
+ /** Allow AI-assisted resolution of semantic conflicts. */
156
+ aiResolveEnabled: boolean;
157
+ }
158
+
159
+ /** Logging behavior. */
160
+ export interface LoggingConfig {
161
+ /** Emit verbose diagnostic output. */
162
+ verbose: boolean;
163
+ /** Redact secrets from logs and captured output. */
164
+ redactSecrets: boolean;
165
+ }
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // Agents & capabilities
169
+ // ---------------------------------------------------------------------------
170
+
171
+ /**
172
+ * The role an agent plays. The orchestration core (Phase 2) uses the first
173
+ * group; the delivery pipeline (Phase 4) adds `architect`/`devops`/`deployer`/
174
+ * `verifier`. Declared here up front so config typing is stable across phases.
175
+ */
176
+ export type Capability =
177
+ | "scout"
178
+ | "builder"
179
+ | "reviewer"
180
+ | "lead"
181
+ | "merger"
182
+ | "coordinator"
183
+ | "architect"
184
+ | "devops"
185
+ | "deployer"
186
+ | "verifier";
187
+
188
+ /** Every supported capability, in canonical order. */
189
+ export const SUPPORTED_CAPABILITIES: readonly Capability[] = [
190
+ "scout",
191
+ "builder",
192
+ "reviewer",
193
+ "lead",
194
+ "merger",
195
+ "coordinator",
196
+ "architect",
197
+ "devops",
198
+ "deployer",
199
+ "verifier",
200
+ ];
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // Outcomes (shared by quality gates, skills, and identity)
204
+ // ---------------------------------------------------------------------------
205
+
206
+ /** Result classification for a unit of work, threaded from quality gates. */
207
+ export type OutcomeStatus = "success" | "partial" | "failure";
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // Model resolution (runtime ↔ provider bridge)
211
+ // ---------------------------------------------------------------------------
212
+
213
+ /** A concrete model id plus the env vars needed to reach it (API keys, base URLs). */
214
+ export interface ResolvedModel {
215
+ /** Concrete model id passed to the runtime CLI. */
216
+ model: string;
217
+ /** Provider env vars to inject into the spawned process. */
218
+ env?: Record<string, string>;
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // Runs & sessions
223
+ // ---------------------------------------------------------------------------
224
+
225
+ /** Lifecycle state of an agent session. */
226
+ export type SessionState = "booting" | "working" | "idle" | "completed" | "failed" | "stopped";
227
+
228
+ /** A run groups all agent sessions started by one coordinator session. */
229
+ export interface RunRecord {
230
+ /** Run id, e.g. "run-20260531-140000". */
231
+ id: string;
232
+ createdAt: string;
233
+ status: "active" | "completed";
234
+ label?: string;
235
+ }
236
+
237
+ /** A single spawned agent worker (one row per agent in the sessions store). */
238
+ export interface AgentSession {
239
+ /** Unique session id. */
240
+ id: string;
241
+ agentName: string;
242
+ capability: Capability;
243
+ taskId: string;
244
+ runId: string;
245
+ worktreePath: string;
246
+ branchName: string;
247
+ state: SessionState;
248
+ /** Parent agent name (null for top-level spawns). */
249
+ parentAgent: string | null;
250
+ /** Delegation depth (coordinator = 0). */
251
+ depth: number;
252
+ /** OS process id of the most recent turn, if known. */
253
+ pid: number | null;
254
+ /** Runtime session id used for `--resume` across turns. */
255
+ runtimeSessionId: string | null;
256
+ startedAt: string;
257
+ lastActivity: string;
258
+ }
259
+
260
+ // ---------------------------------------------------------------------------
261
+ // Mail
262
+ // ---------------------------------------------------------------------------
263
+
264
+ /** Protocol + semantic message types carried on the mail bus. */
265
+ export type MailType =
266
+ | "dispatch"
267
+ | "worker_done"
268
+ | "worker_died"
269
+ | "merge_ready"
270
+ | "merged"
271
+ | "merge_failed"
272
+ | "escalation"
273
+ | "health_check"
274
+ | "assign"
275
+ | "status"
276
+ | "question"
277
+ | "result"
278
+ | "error"
279
+ // Delivery-pipeline protocol messages (Phase 4).
280
+ | "pipeline_ready"
281
+ | "deploy_gate"
282
+ | "deploy_done"
283
+ | "deploy_failed"
284
+ | "verify_done"
285
+ | "verify_failed";
286
+
287
+ export type MailPriority = "low" | "normal" | "high" | "urgent";
288
+
289
+ /** A message on the SQLite mail bus. */
290
+ export interface MailMessage {
291
+ id: string;
292
+ from: string;
293
+ to: string;
294
+ subject: string;
295
+ body: string;
296
+ type: MailType;
297
+ priority: MailPriority;
298
+ /** Thread id for grouping replies (null starts a new thread). */
299
+ threadId: string | null;
300
+ /** Structured JSON payload for protocol messages (null otherwise). */
301
+ payload: string | null;
302
+ read: boolean;
303
+ createdAt: string;
304
+ }
305
+
306
+ /** Fields accepted when sending a message (id/createdAt/read are assigned by the store). */
307
+ export interface NewMail {
308
+ from: string;
309
+ to: string;
310
+ subject: string;
311
+ body: string;
312
+ type: MailType;
313
+ priority?: MailPriority;
314
+ threadId?: string | null;
315
+ payload?: string | null;
316
+ }
317
+
318
+ // ---------------------------------------------------------------------------
319
+ // Events (observability)
320
+ // ---------------------------------------------------------------------------
321
+
322
+ /** A recorded agent event (tool call, lifecycle transition, error). */
323
+ export interface EventRecord {
324
+ id: string;
325
+ agentName: string;
326
+ runId: string | null;
327
+ /** e.g. "tool-start" | "tool-end" | "session-end" | "error". */
328
+ type: string;
329
+ /** Tool name for tool events (null otherwise). */
330
+ tool: string | null;
331
+ /** JSON-encoded detail (null otherwise). */
332
+ detail: string | null;
333
+ createdAt: string;
334
+ }
335
+
336
+ // ---------------------------------------------------------------------------
337
+ // Agent manifest
338
+ // ---------------------------------------------------------------------------
339
+
340
+ /** Static definition of one capability: which base .md, model, tools, constraints. */
341
+ export interface AgentDefinition {
342
+ /** Base definition filename under the agent base dir (e.g. "builder.md"). */
343
+ file: string;
344
+ /** Model alias ("opus"/"sonnet"/"haiku") or a concrete model id. */
345
+ model: string;
346
+ /** Tools the agent is permitted to use. */
347
+ tools: string[];
348
+ /** Capabilities this definition provides. */
349
+ capabilities: Capability[];
350
+ /** May this agent spawn children? */
351
+ canSpawn: boolean;
352
+ /** Hard constraints injected into the overlay. */
353
+ constraints: string[];
354
+ }
355
+
356
+ /** The agent registry, loaded from `.agentplate/agent-manifest.json`. */
357
+ export interface AgentManifest {
358
+ version: string;
359
+ agents: Partial<Record<Capability, AgentDefinition>>;
360
+ /** Maps each capability to the definitions that provide it. */
361
+ capabilityIndex: Partial<Record<Capability, Capability[]>>;
362
+ }
363
+
364
+ // ---------------------------------------------------------------------------
365
+ // Overlay (per-spawn instruction assembly)
366
+ // ---------------------------------------------------------------------------
367
+
368
+ /** Inputs to the dynamic overlay generator (the per-task instruction file). */
369
+ export interface OverlayConfig {
370
+ agentName: string;
371
+ capability: Capability;
372
+ taskId: string;
373
+ specPath?: string;
374
+ branchName: string;
375
+ worktreePath: string;
376
+ parentAgent: string | null;
377
+ depth: number;
378
+ /** Exclusive file scope the agent owns (empty = no restriction). */
379
+ fileScope: string[];
380
+ /** Contents of the base .md definition (the HOW). */
381
+ baseDefinition: string;
382
+ canSpawn: boolean;
383
+ qualityGates: QualityGate[];
384
+ constraints: string[];
385
+ /** Parallel sibling agent names (for rebase-before-merge guidance). */
386
+ siblings?: string[];
387
+ /** Reserved for Phase 3: retrieved skills block. */
388
+ skillsOverlay?: string;
389
+ }
390
+
391
+ // ---------------------------------------------------------------------------
392
+ // Merge
393
+ // ---------------------------------------------------------------------------
394
+
395
+ /** Conflict-resolution tier applied (or predicted) for a merge. */
396
+ export type MergeTier = "clean-merge" | "auto-resolve" | "ai-resolve" | "reimagine";
397
+
398
+ export type MergeStatus = "pending" | "merged" | "failed";
399
+
400
+ /** A queued merge of an agent branch into a target branch. */
401
+ export interface MergeEntry {
402
+ id: string;
403
+ branchName: string;
404
+ agentName: string;
405
+ taskId: string;
406
+ targetBranch: string;
407
+ status: MergeStatus;
408
+ createdAt: string;
409
+ }
410
+
411
+ /** Outcome of attempting (or predicting) a merge. */
412
+ export interface MergeResult {
413
+ branchName: string;
414
+ status: MergeStatus;
415
+ /** The tier that resolved (or would resolve) the merge. */
416
+ tier: MergeTier | null;
417
+ conflictFiles: string[];
418
+ message: string;
419
+ }
420
+
421
+ // ---------------------------------------------------------------------------
422
+ // Deploy audit
423
+ // ---------------------------------------------------------------------------
424
+
425
+ /** One append-only row in the deploy audit log (`deploys.db`). No secrets, ever. */
426
+ export interface DeployAuditRow {
427
+ id: string;
428
+ runId: string | null;
429
+ agentName: string;
430
+ target: string;
431
+ environment: string;
432
+ action: "deploy" | "rollback";
433
+ dryRun: boolean;
434
+ gateDecision: "auto" | "approved" | "denied" | "n/a";
435
+ approvedBy: string | null;
436
+ status: "success" | "failed";
437
+ deploymentId: string | null;
438
+ urls: string[];
439
+ outputs: Record<string, string>;
440
+ commitSha: string;
441
+ createdAt: string;
442
+ }
@@ -0,0 +1,35 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { commandOnPath, resolveArgv } from "./detect.ts";
3
+
4
+ describe("commandOnPath", () => {
5
+ test("finds a binary that is on PATH (cross-platform via Bun.which)", async () => {
6
+ // `node` ships with the Bun/CI toolchain on every OS we run on.
7
+ expect(await commandOnPath("node")).toBe(true);
8
+ });
9
+
10
+ test("returns false for a definitely-absent command", async () => {
11
+ expect(await commandOnPath("definitely-not-a-real-cli-xyz")).toBe(false);
12
+ });
13
+ });
14
+
15
+ describe("resolveArgv", () => {
16
+ test("returns the argv unchanged on POSIX (the proven path)", () => {
17
+ // On macOS/Linux (where this suite runs) the argv is passed through verbatim.
18
+ if (process.platform !== "win32") {
19
+ const argv = ["gemini", "--model", "x", "--prompt", "hi"];
20
+ expect(resolveArgv(argv)).toEqual(argv);
21
+ }
22
+ });
23
+
24
+ test("handles an empty argv without throwing", () => {
25
+ expect(resolveArgv([])).toEqual([]);
26
+ });
27
+
28
+ test("on Windows, resolves argv[0] to a real path and keeps the rest", () => {
29
+ // The win32 branch can only run on Windows; here we just assert the contract
30
+ // holds for the args tail regardless of OS (argv[0] is resolved or left as-is).
31
+ const out = resolveArgv(["gemini", "--prompt", "hi"]);
32
+ expect(out.slice(1)).toEqual(["--prompt", "hi"]);
33
+ expect(out.length).toBe(3);
34
+ });
35
+ });
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Environment detection helpers used by `init`/`setup`.
3
+ *
4
+ * All detection is best-effort and side-effect-free: failures fall back to
5
+ * sensible defaults rather than throwing, so initialization never blocks on a
6
+ * missing remote or an absent CLI.
7
+ */
8
+
9
+ import { basename } from "node:path";
10
+
11
+ /** Runtime adapter ids Agentplate knows how to detect by their CLI name. */
12
+ const RUNTIME_CLIS: ReadonlyArray<{ runtime: string; cli: string }> = [
13
+ { runtime: "claude", cli: "claude" },
14
+ { runtime: "opencode", cli: "opencode" },
15
+ { runtime: "codex", cli: "codex" },
16
+ { runtime: "gemini", cli: "gemini" },
17
+ { runtime: "cursor", cli: "cursor-agent" },
18
+ ];
19
+
20
+ async function runGit(root: string, args: string[]): Promise<string | null> {
21
+ try {
22
+ const proc = Bun.spawn(["git", ...args], { cwd: root, stdout: "pipe", stderr: "pipe" });
23
+ const code = await proc.exited;
24
+ if (code !== 0) return null;
25
+ return (await new Response(proc.stdout).text()).trim();
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * True if `cli` is resolvable on PATH. Uses `Bun.which`, which is cross-platform
33
+ * and honors Windows `PATHEXT` (so `.cmd`/`.exe` shims are found). The previous
34
+ * `which` subprocess was Unix-only, so on Windows every CLI looked "not installed"
35
+ * — the wizard hid them and `detectDefaultRuntime` always fell back to claude.
36
+ */
37
+ export async function commandOnPath(cli: string): Promise<boolean> {
38
+ return Bun.which(cli) !== null;
39
+ }
40
+
41
+ /**
42
+ * Resolve an argv so its executable can be launched on every OS. On Windows,
43
+ * npm-installed CLIs (gemini, codex, opencode, cursor-agent) are `.cmd`/`.ps1`
44
+ * shims that `Bun.spawn` will NOT launch by bare name (it looks for an exact
45
+ * `name`/`name.exe`), causing ENOENT; `Bun.which` resolves the real shim path via
46
+ * `PATHEXT`. On POSIX the argv is returned unchanged so the proven path is kept.
47
+ */
48
+ export function resolveArgv(argv: string[]): string[] {
49
+ if (process.platform !== "win32" || argv.length === 0) return argv;
50
+ const resolved = Bun.which(argv[0] as string);
51
+ return resolved ? [resolved, ...argv.slice(1)] : argv;
52
+ }
53
+
54
+ /** Detect the project name from the git remote URL, falling back to the dir name. */
55
+ export async function detectProjectName(root: string): Promise<string> {
56
+ const remote = await runGit(root, ["config", "--get", "remote.origin.url"]);
57
+ if (remote) {
58
+ const match = remote.match(/([^/:]+?)(?:\.git)?$/);
59
+ if (match?.[1]) return match[1];
60
+ }
61
+ return basename(root);
62
+ }
63
+
64
+ /** Detect the canonical branch (origin HEAD, else current branch, else "main"). */
65
+ export async function detectCanonicalBranch(root: string): Promise<string> {
66
+ const originHead = await runGit(root, ["symbolic-ref", "refs/remotes/origin/HEAD"]);
67
+ if (originHead) {
68
+ const name = originHead.split("/").pop();
69
+ if (name) return name;
70
+ }
71
+ const current = await runGit(root, ["branch", "--show-current"]);
72
+ if (current) return current;
73
+ return "main";
74
+ }
75
+
76
+ /** Detect the first installed coding-agent runtime, defaulting to "claude". */
77
+ export async function detectDefaultRuntime(): Promise<string> {
78
+ for (const { runtime, cli } of RUNTIME_CLIS) {
79
+ if (await commandOnPath(cli)) return runtime;
80
+ }
81
+ return "claude";
82
+ }
@@ -0,0 +1,19 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { join } from "node:path";
3
+ import { VERSION } from "./version.ts";
4
+
5
+ // The CLI version is intentionally a plain constant (so it survives bundling and
6
+ // needs no filesystem access at runtime), but that makes it easy to bump
7
+ // package.json and forget version.ts — which silently ships a wrong --version.
8
+ // This guard reads the REAL package.json and fails the gates on any drift.
9
+ describe("VERSION", () => {
10
+ test("matches the version in package.json", async () => {
11
+ const pkgPath = join(import.meta.dir, "..", "package.json");
12
+ const pkg = (await Bun.file(pkgPath).json()) as { version: string };
13
+ expect(VERSION).toBe(pkg.version);
14
+ });
15
+
16
+ test("is a valid semver string", () => {
17
+ expect(VERSION).toMatch(/^\d+\.\d+\.\d+(?:-[\w.]+)?$/);
18
+ });
19
+ });
package/src/version.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Single source of truth for the Agentplate CLI version.
3
+ *
4
+ * Kept as a plain constant (not read from package.json at runtime) so the value
5
+ * is available without filesystem access and survives bundling.
6
+ */
7
+ export const VERSION = "1.0.0";