@ashulab/agent-forge 0.4.0 → 0.6.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.
package/README.md CHANGED
@@ -9,7 +9,7 @@ On each run the launcher:
9
9
  - detects installed provider CLIs on your PATH
10
10
  - asks which provider to use (or takes it from a flag)
11
11
  - starts the provider with the GitHub token and a per-agent git identity injected into the environment
12
- - injects the agent's `systemPrompt` per process (`claude --append-system-prompt`, `codex -c developer_instructions`; antigravity falls back to an `AGENTS.md` block)
12
+ - injects an identity prompt per process — a fixed block naming the git author identity and the `GH_TOKEN` / `GITHUB_TOKEN` in the environment, with the agent's optional `systemPrompt` appended (`claude --append-system-prompt`, `codex -c developer_instructions`; antigravity via a global custom agent selected with `--agent`) — no repo file is touched
13
13
 
14
14
  Provider auth stays in the provider CLI — the launcher never handles provider API keys.
15
15
 
@@ -134,7 +134,7 @@ Before launching, it prints a summary:
134
134
  │ expires ~59m ← installation tokens last ~1h
135
135
  ```
136
136
 
137
- (The `antigravity` provider adds a `memory` row pointing at the `AGENTS.md` it wrote.)
137
+ (The `antigravity` provider adds an `agent` row showing the `--agent` id and the custom-agent file it wrote.)
138
138
 
139
139
  Other commands:
140
140
 
@@ -146,11 +146,11 @@ agent-forge --help
146
146
 
147
147
  ### Provider-specific prompt behavior
148
148
 
149
- `systemPrompt` is injected per process — no repo file is touched:
149
+ The identity prompt (identity block + the agent's optional `systemPrompt`) is injected per process — no repo file is touched:
150
150
 
151
151
  - `claude`: `--append-system-prompt` (lands in the real system prompt)
152
152
  - `codex`: `-c developer_instructions=…` (appends a developer message; unlike `model_instructions_file` it does not replace codex's base prompt)
153
- - `antigravity` (`agy`): no such flag — the launcher writes `systemPrompt` into `AGENTS.md` in the working directory, inside a managed `agent-forge:identity` block. Do not commit it. Run one agent per worktree.
153
+ - `antigravity` (`agy`): no prompt flag — the launcher writes the identity prompt as a global antigravity custom agent at `~/.gemini/config/agents/<name>/agent.md` (frontmatter `name`, `mainAgent: true`, `agentForge: true`; prompt under an `# Identity` heading) and passes `--agent <name>`. The file is keyed by the agent's registry `name`, upserted on each run, and never deleted. If a file with that name exists without our `agentForge` marker, the launch fails rather than overwrite it rename the agent or remove that file.
154
154
 
155
155
  ## Token refresh
156
156
 
package/dist/launcher.cjs CHANGED
@@ -4016,7 +4016,6 @@ var require_jsonwebtoken = __commonJS({
4016
4016
 
4017
4017
  // src/launcher.js
4018
4018
  var import_node_child_process3 = require("node:child_process");
4019
- var import_node_path4 = require("node:path");
4020
4019
  var import_node_util4 = require("node:util");
4021
4020
 
4022
4021
  // node_modules/.pnpm/@clack+core@1.4.3/node_modules/@clack/core/dist/index.mjs
@@ -5220,6 +5219,7 @@ var import_node_fs = require("node:fs");
5220
5219
  var import_node_os = require("node:os");
5221
5220
  var import_node_path = require("node:path");
5222
5221
  var REQUIRED_FIELDS = ["name", "appId", "privateKeyPath"];
5222
+ var VALID_NAME = /^[a-z0-9][a-z0-9-]*$/;
5223
5223
  var SCHEMA_URL = "https://raw.githubusercontent.com/AshuLab/agent-forge/main/schema/agents.schema.json";
5224
5224
  var CONFIG_HOME = process.env.XDG_CONFIG_HOME || (0, import_node_path.join)((0, import_node_os.homedir)(), ".config");
5225
5225
  var GLOBAL_CONFIG = (0, import_node_path.join)(CONFIG_HOME, "agent-forge", "agents.json");
@@ -5264,6 +5264,11 @@ function validateAgents(agents) {
5264
5264
  throw new Error(`agents.json: agent ${ref} is missing "${field}"`);
5265
5265
  }
5266
5266
  }
5267
+ if (!VALID_NAME.test(agent.name)) {
5268
+ throw new Error(
5269
+ `agents.json: agent ${ref} has an invalid name "${agent.name}" (lowercase letters, digits, hyphens)`
5270
+ );
5271
+ }
5267
5272
  if (seen.has(agent.name)) {
5268
5273
  throw new Error(`agents.json: duplicate agent name "${agent.name}"`);
5269
5274
  }
@@ -5343,10 +5348,17 @@ async function githubApi(path, { jwtToken, body } = {}) {
5343
5348
  });
5344
5349
  const data = await response.json();
5345
5350
  if (!response.ok) {
5346
- throw new Error(`GitHub API ${path}: ${response.status} ${data?.message || "Unknown error"}`);
5351
+ throw new Error(githubApiError(path, response.status, data?.message, Boolean(jwtToken)));
5347
5352
  }
5348
5353
  return data;
5349
5354
  }
5355
+ function githubApiError(path, status, message, signedWithJwt) {
5356
+ let text2 = `GitHub API ${path}: ${status} ${message || "Unknown error"}`;
5357
+ if (status === 401 && signedWithJwt) {
5358
+ text2 += " \u2014 the App JWT was rejected. Check that appId matches privateKeyPath (this key must belong to that App), and that the system clock is accurate.";
5359
+ }
5360
+ return text2;
5361
+ }
5350
5362
  async function resolveBotId(agent) {
5351
5363
  if (agent.botId) return String(agent.botId);
5352
5364
  const handle = `${agent.botName || agent.name}[bot]`;
@@ -5469,46 +5481,82 @@ function getProviderInfo(providerName, agent) {
5469
5481
  const resolvedCommand = isCommandInstalled(command) ? command : (provider.aliases || []).find((alias) => isCommandInstalled(alias)) || command;
5470
5482
  return { provider, command: resolvedCommand };
5471
5483
  }
5472
- function getProviderPromptArgs(providerName, agent) {
5473
- const prompt = agent.systemPrompt || agent.instructions || agent.identityPrompt;
5474
- if (!prompt) {
5475
- return [];
5476
- }
5484
+ function getProviderPromptArgs(providerName, prompt) {
5477
5485
  if (providerName === "claude") return ["--append-system-prompt", prompt];
5478
5486
  if (providerName === "codex") return ["-c", `developer_instructions=${JSON.stringify(prompt)}`];
5479
5487
  return [];
5480
5488
  }
5489
+ function buildProviderArgs(providerName, prompt, antigravityAgent) {
5490
+ return [
5491
+ ...getProviderPromptArgs(providerName, prompt),
5492
+ ...antigravityAgent ? ["--agent", antigravityAgent.name] : []
5493
+ ];
5494
+ }
5481
5495
 
5482
- // src/identity.js
5496
+ // src/agent-file.js
5483
5497
  var import_node_fs3 = require("node:fs");
5498
+ var import_node_os3 = require("node:os");
5484
5499
  var import_node_path3 = require("node:path");
5485
- var START = "<!-- agent-forge:identity:start -->";
5486
- var END = "<!-- agent-forge:identity:end -->";
5487
- var MEMORY_FILE = { antigravity: "AGENTS.md" };
5488
- function upsertIdentityBlock(content, prompt) {
5489
- const block2 = `${START}
5490
- ${prompt}
5491
- ${END}`;
5492
- const start = content.indexOf(START);
5493
- const end = content.indexOf(END);
5494
- if (start !== -1 && end > start) {
5495
- return content.slice(0, start) + block2 + content.slice(end + END.length);
5496
- }
5497
- return content ? `${content.trimEnd()}
5498
-
5499
- ${block2}
5500
- ` : `${block2}
5501
- `;
5500
+ var MARKER = "agentForge: true";
5501
+ var MARKER_LINE = /^agentForge:\s*true\s*$/m;
5502
+ function isManagedByAgentForge(text2) {
5503
+ const end = text2.startsWith("---\n") ? text2.indexOf("\n---", 4) : -1;
5504
+ return end !== -1 && MARKER_LINE.test(text2.slice(0, end));
5505
+ }
5506
+ var AGENTS_HOME = (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".gemini", "config", "agents");
5507
+ var VALID_NAME2 = /^[a-z0-9][a-z0-9-]*$/;
5508
+ function renderAgentFile(name, prompt) {
5509
+ return [
5510
+ "---",
5511
+ `name: ${name}`,
5512
+ "mainAgent: true",
5513
+ MARKER,
5514
+ "---",
5515
+ "",
5516
+ "# Identity",
5517
+ "",
5518
+ prompt.trimEnd(),
5519
+ ""
5520
+ ].join("\n");
5502
5521
  }
5503
- function syncIdentityFile(providerName, agent, cwd = process.cwd()) {
5504
- const prompt = agent.systemPrompt || agent.instructions || agent.identityPrompt;
5505
- const name = MEMORY_FILE[providerName];
5506
- if (!prompt || !name) return null;
5507
- const file = (0, import_node_path3.join)(cwd, name);
5508
- const current = (0, import_node_fs3.existsSync)(file) ? (0, import_node_fs3.readFileSync)(file, "utf8") : "";
5509
- const next = upsertIdentityBlock(current, prompt);
5510
- if (next !== current) (0, import_node_fs3.writeFileSync)(file, next);
5511
- return file;
5522
+ function syncAntigravityAgent(agent, prompt, agentsHome = AGENTS_HOME) {
5523
+ const name = agent.name;
5524
+ if (!VALID_NAME2.test(name)) {
5525
+ throw new Error(
5526
+ `Agent name "${name}" is not a valid antigravity agent id (lowercase letters, digits, hyphens).`
5527
+ );
5528
+ }
5529
+ const dir = (0, import_node_path3.join)(agentsHome, name);
5530
+ const file = (0, import_node_path3.join)(dir, "agent.md");
5531
+ const next = renderAgentFile(name, prompt);
5532
+ if ((0, import_node_fs3.existsSync)(file)) {
5533
+ const current = (0, import_node_fs3.readFileSync)(file, "utf8");
5534
+ if (!isManagedByAgentForge(current)) {
5535
+ throw new Error(
5536
+ `${file} already exists and is not managed by agent-forge.
5537
+ Rename the agent in your registry or remove that file.`
5538
+ );
5539
+ }
5540
+ if (next !== current) (0, import_node_fs3.writeFileSync)(file, next);
5541
+ return { file, name };
5542
+ }
5543
+ (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
5544
+ (0, import_node_fs3.writeFileSync)(file, next);
5545
+ return { file, name };
5546
+ }
5547
+
5548
+ // src/identity-prompt.js
5549
+ function buildIdentityPrompt({ name, email }, agentPrompt) {
5550
+ const base = [
5551
+ "You are operating under a GitHub App identity that agent-forge set up for this session; a human is driving it interactively.",
5552
+ "",
5553
+ `Your commits are authored as \`${name}\` <${email}> \u2014 already configured via GIT_AUTHOR_*/GIT_COMMITTER_*, do not change it.`,
5554
+ "A scoped, short-lived GitHub App installation token is available in the environment as GH_TOKEN and GITHUB_TOKEN; use it for `gh` and authenticated `git` operations."
5555
+ ].join("\n");
5556
+ const own = agentPrompt?.trim();
5557
+ return own ? `${base}
5558
+
5559
+ ${own}` : base;
5512
5560
  }
5513
5561
 
5514
5562
  // src/format.js
@@ -5534,7 +5582,7 @@ var import_node_util3 = require("node:util");
5534
5582
  // package.json
5535
5583
  var package_default = {
5536
5584
  name: "@ashulab/agent-forge",
5537
- version: "0.4.0",
5585
+ version: "0.6.0",
5538
5586
  description: "Give claude/codex/antigravity their own GitHub identity: a bot account, a scoped token minted per run, git attribution to match",
5539
5587
  license: "MIT",
5540
5588
  author: "Diego Ghersi <ghersidl@gmail.com>",
@@ -5658,8 +5706,23 @@ async function addAgentWizard({ embedded = false } = {}) {
5658
5706
  }
5659
5707
  const where = meta.installations.length === 1 ? `${meta.installations[0].account} \u2014 resolved automatically each run` : `${meta.installations.map((i2) => i2.account).join(", ")} \u2014 resolved from the repo you launch in`;
5660
5708
  note(where, "Installation");
5661
- const name = (keep(await text({ message: "Agent name", placeholder: meta.slug, defaultValue: meta.slug })) || meta.slug).trim();
5709
+ const name = (keep(await text({
5710
+ message: "Agent name",
5711
+ placeholder: meta.slug,
5712
+ defaultValue: meta.slug,
5713
+ validate: (v) => /^[a-z0-9][a-z0-9-]*$/.test(v.trim()) ? void 0 : "Lowercase letters, digits, hyphens"
5714
+ })) || meta.slug).trim();
5662
5715
  const label = (keep(await text({ message: "Label", placeholder: meta.name, defaultValue: meta.name })) || meta.name).trim();
5716
+ note(
5717
+ [
5718
+ "agent-forge already tells every launched agent:",
5719
+ " \xB7 its bot git identity (GIT_AUTHOR_*/GIT_COMMITTER_*)",
5720
+ " \xB7 that a scoped GH_TOKEN / GITHUB_TOKEN is in the environment",
5721
+ "",
5722
+ "Add only what is specific to this agent \u2014 role, style, guardrails."
5723
+ ].join("\n"),
5724
+ "Identity is handled for you"
5725
+ );
5663
5726
  const systemPrompt = keep(await text({ message: "System prompt (optional)", placeholder: "You are ..." })).trim();
5664
5727
  const agent = {
5665
5728
  name,
@@ -5787,7 +5850,11 @@ async function launchAgent(agentName, providerName) {
5787
5850
  const botId = await resolveBotId(agent);
5788
5851
  const gitIdentity = buildGitIdentity(agent, botId);
5789
5852
  s.stop(`Token ready for ${(0, import_node_util4.styleText)("green", gitIdentity.GIT_AUTHOR_NAME)}`);
5790
- const identityFile = syncIdentityFile(providerName, agent);
5853
+ const identityPrompt = buildIdentityPrompt(
5854
+ { name: gitIdentity.GIT_AUTHOR_NAME, email: gitIdentity.GIT_AUTHOR_EMAIL },
5855
+ agent.systemPrompt || agent.instructions || agent.identityPrompt
5856
+ );
5857
+ const antigravityAgent = providerName === "antigravity" ? syncAntigravityAgent(agent, identityPrompt) : null;
5791
5858
  const runtimeEnv = {
5792
5859
  ...process.env,
5793
5860
  GITHUB_TOKEN: githubToken,
@@ -5808,11 +5875,11 @@ async function launchAgent(agentName, providerName) {
5808
5875
  row("scope", (0, import_node_util4.styleText)("dim", repositorySelection === "selected" ? `${scopeText} \xB7 selected repos` : scopeText))
5809
5876
  );
5810
5877
  if (expiresAt) summary.push(row("expires", (0, import_node_util4.styleText)("dim", formatExpiry(expiresAt))));
5811
- if (identityFile) {
5812
- summary.push(row("memory", (0, import_node_util4.styleText)("dim", (0, import_node_path4.relative)(process.cwd(), identityFile) || identityFile)));
5878
+ if (antigravityAgent) {
5879
+ summary.push(row("agent", (0, import_node_util4.styleText)("dim", `--agent ${antigravityAgent.name} \xB7 ${antigravityAgent.file}`)));
5813
5880
  }
5814
5881
  note(summary.join("\n"), (0, import_node_util4.styleText)("bold", agent.label || agent.name));
5815
- const providerArgs = getProviderPromptArgs(providerName, agent);
5882
+ const providerArgs = buildProviderArgs(providerName, identityPrompt, antigravityAgent);
5816
5883
  const child = (0, import_node_child_process3.spawn)(providerInfo.command, providerArgs, {
5817
5884
  stdio: "inherit",
5818
5885
  env: runtimeEnv,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ashulab/agent-forge",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Give claude/codex/antigravity their own GitHub identity: a bot account, a scoped token minted per run, git attribution to match",
5
5
  "license": "MIT",
6
6
  "author": "Diego Ghersi <ghersidl@gmail.com>",
@@ -15,7 +15,8 @@
15
15
  "properties": {
16
16
  "name": {
17
17
  "type": "string",
18
- "description": "Unique agent id, used with --agent"
18
+ "pattern": "^[a-z0-9][a-z0-9-]*$",
19
+ "description": "Unique agent id, used with --agent. Lowercase letters, digits, hyphens (also the antigravity custom-agent id)."
19
20
  },
20
21
  "label": {
21
22
  "type": "string",
@@ -43,15 +44,15 @@
43
44
  },
44
45
  "systemPrompt": {
45
46
  "type": "string",
46
- "description": "Identity / system prompt injected into the provider and its memory file"
47
+ "description": "Optional agent-specific system prompt, appended to the identity block agent-forge always injects. Per process: claude --append-system-prompt, codex developer_instructions, antigravity a global custom agent selected with --agent"
47
48
  },
48
49
  "instructions": {
49
50
  "type": "string",
50
- "description": "Fallback used as the prompt when systemPrompt is absent"
51
+ "description": "Fallback for systemPrompt when it is absent; same identity-block + append behavior"
51
52
  },
52
53
  "identityPrompt": {
53
54
  "type": "string",
54
- "description": "Fallback used as the prompt when systemPrompt and instructions are absent"
55
+ "description": "Fallback for systemPrompt and instructions when both are absent; same identity-block + append behavior"
55
56
  },
56
57
  "repositories": {
57
58
  "type": "array",