@finchip.ai/foga 0.6.0-alpha.7 → 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
@@ -19,9 +19,11 @@ ERC-8004 (Trustless Agents) is a required dependency: ERC-8419, ERC-8434 and ERC
19
19
 
20
20
  The project delivers three things from one kernel: the **Finch OG Agent** (the first running instance), the **FOGA node software** anyone can run with their own identity, model and wallet, and a **public access layer** (MCP, A2A, HTTP/CLI). It is built in phases; this repository has completed **Phase 2c: the genesis on a public chain**, on top of the Phase 2 genesis (T0–T9) and the Phase 1 world layer.
21
21
 
22
- ## Status: Phase 3d in progress — 3d-2 done (the wallet, signed callers, the documents on chain, preflight, A2A, Sourcify), 3d-1 done (quickstart, an outside agent's Loop D); Phase 3c done — Loop D, the unscripted run; Phase 3b done — the node served over MCP and HTTP; Phase 3a done — the kernel hardened and turned into a node with a capability catalog; Phase 2c done
22
+ ## Status: Phase 3 done (0.6.0, 2026-11-05) — the OG live at foga.finchip.ai, the MCP registry listing, the Sepolia Loop D with an outside agent; 3d-2 (the wallet, signed callers, the documents on chain, preflight, A2A, Sourcify), 3d-1 (quickstart, an outside agent's Loop D), 3c (Loop D with a model), 3b (MCP and HTTP), 3a (the kernel as a node) all done
23
23
 
24
- **3d-2 (0.6.0-alpha.7, 2026-11-03).** The second slice of 3d is what a cell needs to stand on its own among other agents, under one principle (docs/design-memo.md, section 10): the wallet is the first step, made by default and never a reason to stop; identity is one's own; only the genome replicates. `foga wallet new` (`src/node/wallet.ts`, docs/wallet.md) makes an agent's keys on its own machine — owner, operator, guardian in 0600 files under `~/.foga/wallet`, never overwritten, no network — and `foga quickstart` makes the default wallet on its first run and births every later cell with the same keys; `--wallet <dir>` on every `foga cell` and `foga node` command takes a role's key from there. A wallet is also a credential: `Authorization: FOGA-EIP712 <envelope>` (`src/http/signed.ts`, `src/http/access.ts`) is an EIP-712 signature in the cell's own domain — bound to one request and good once, or a session for a client with static headers — and the caller is the signing address at the level `access.signers` grants it (the cell's own operator or owner, an allow-list by address or ERC-8004 agent id, a KYA standing threshold, a default); `/api`, `/mcp` and `/a2a` take it, `foga wallet sign` prints it, and every cell's MCP server carries the prompt `become-a-cell` and the resource `foga://docs/quickstart` for an agent that meets it. `foga cell docs build | publish | status` (`src/node/documents.ts`, docs/documents.md) generates the cell's documents from one configuration — the ERC-8004 registration, the ERC-8434 AID document, the ERC-8444 descriptor, the ERC-8454 unit, the A2A agent card, `server.json`, a manifest with content hashes — serves them at `/documents/*` and `/.well-known/*`, and commits them on chain as the operator's keyed job (`setAgentURI`, `setDocumentURI`, `updateDescriptor`, `publishVersion`; only what differs is sent), read back and cross-checked; `docs_status` and `docs_publish` are the catalog's 30th and 31st capabilities. `foga node preflight` (`src/node/preflight.ts`) is the readiness reconciliation — the world, the account and its allow-list, the budget, the keys, the standing, the skills run in the worker, the documents against the chain, the public URL, the model — advisory, `--strict` for CI. `POST /a2a` (`src/a2a/`, docs/a2a.md) is the A2A 1.0 JSON-RPC binding with the card at `/.well-known/agent-card.json`: a quote for anyone, a tender taken for an act-level caller as one task over the catalog's jobs, `GetTask`, `ListTasks`, `CancelTask`, errors by the specification. `foga world verify --sourcify` (`src/world/sourcify.ts`, docs/world.md) submits every FOGA-deployed contract of a world with the standard JSON input of its own compiler run (`forge build` now leaves the build-info beside the artifacts), matched by runtime code; a local chain is a dry run, which CI runs over all 47. The OG instance's default is host mode (`foga node serve`: every entrance, the watchers, no model) with embedded mode (`foga node run`, the runner with a model) on demand. Seven new tests on anvil and offline (`wallet`, `signed`, `documents`, `a2a`, `preflight`, `sourcify`, and `quickstart` extended) are the gate; `foga quickstart` now ends with a cell whose documents are on chain, whose card answers, and whose A2A entrance quotes. Next, 3d-3: the OG deployment runbook, `foga.finchip.ai`, the registry listing, the Sepolia Sourcify run, the OG cell's migration to revision 2 and an outside agent's Loop D on Sepolia.
24
+ **3d-3 (0.6.0, 2026-11-05).** Phase 3 closes with the reference cell served and the last acceptance run on the public chain. The OG was born fresh on Sepolia by the path any cell takes (`wallet new`, `cell new`, `cell init`, `docs publish`, `node serve` — one key, no migration; docs/og-deployment.md): agent 11261, its skill offered, its documents on chain, its fifteen contracts Sourcify-verified; it is served in host mode under systemd behind Caddy at `foga.finchip.ai` and listed in the MCP registry as `io.github.garyyang-finchip/foga`. Loop D with an outside agent ran on Sepolia and every invariant row passed (docs/loop-d.md, "On a public chain"); what the public chain taught is fixed in this release — a working directory is one run, role funding is priced with a floor, the settlement figure is the tender's own, a log scan is windowed. `foga cell advise` (`src/node/advise.ts`) tells a running cell what to do next — separate the one key into owner, operator and guardian first — and the recommended items print after a quickstart, a birth and at serve; the first run stays minimal on purpose. The covenant list is read off the world (an eleventh covenant appears with no code change) and the manifest carries a `services` slot — the maintainer's catalog, declinable, served by the reference cell at `/services.json` — docs/extending.md. 107 tests.
25
+
26
+ **3d-2 (0.6.0-alpha.7, 2026-11-03).** The second slice of 3d is what a cell needs to stand on its own among other agents, under one principle (docs/design-memo.md, section 10): the wallet is the first step, made by default and never a reason to stop; identity is one's own; only the genome replicates. `foga wallet new` (`src/node/wallet.ts`, docs/wallet.md) makes an agent's keys on its own machine — by default one key in a 0600 file under `~/.foga/wallet` — the identity that fills every role a cell needs; `--cell` for the separated owner/operator/guardian set — never overwritten, no network — and `foga quickstart` makes the default wallet on its first run and births every later cell with the same keys; `--wallet <dir>` on every `foga cell` and `foga node` command takes a role's key from there. A wallet is also a credential: `Authorization: FOGA-EIP712 <envelope>` (`src/http/signed.ts`, `src/http/access.ts`) is an EIP-712 signature in the cell's own domain — bound to one request and good once, or a session for a client with static headers — and the caller is the signing address at the level `access.signers` grants it (the cell's own operator or owner, an allow-list by address or ERC-8004 agent id, a KYA standing threshold, a default); `/api`, `/mcp` and `/a2a` take it, `foga wallet sign` prints it, and every cell's MCP server carries the prompt `become-a-cell` and the resource `foga://docs/quickstart` for an agent that meets it. `foga cell docs build | publish | status` (`src/node/documents.ts`, docs/documents.md) generates the cell's documents from one configuration — the ERC-8004 registration, the ERC-8434 AID document, the ERC-8444 descriptor, the ERC-8454 unit, the A2A agent card, `server.json`, a manifest with content hashes — serves them at `/documents/*` and `/.well-known/*`, and commits them on chain as the operator's keyed job (`setAgentURI`, `setDocumentURI`, `updateDescriptor`, `publishVersion`; only what differs is sent), read back and cross-checked; `docs_status` and `docs_publish` are the catalog's 30th and 31st capabilities. `foga node preflight` (`src/node/preflight.ts`) is the readiness reconciliation — the world, the account and its allow-list, the budget, the keys, the standing, the skills run in the worker, the documents against the chain, the public URL, the model — advisory, `--strict` for CI. `POST /a2a` (`src/a2a/`, docs/a2a.md) is the A2A 1.0 JSON-RPC binding with the card at `/.well-known/agent-card.json`: a quote for anyone, a tender taken for an act-level caller as one task over the catalog's jobs, `GetTask`, `ListTasks`, `CancelTask`, errors by the specification. `foga world verify --sourcify` (`src/world/sourcify.ts`, docs/world.md) submits every FOGA-deployed contract of a world with the standard JSON input of its own compiler run (`forge build` now leaves the build-info beside the artifacts), matched by runtime code; a local chain is a dry run, which CI runs over all 47. The OG instance's default is host mode (`foga node serve`: every entrance, the watchers, no model) with embedded mode (`foga node run`, the runner with a model) on demand. Seven new tests on anvil and offline (`wallet`, `signed`, `documents`, `a2a`, `preflight`, `sourcify`, and `quickstart` extended) are the gate; `foga quickstart` now ends with a cell whose documents are on chain, whose card answers, and whose A2A entrance quotes. Next, 3d-3: the OG deployment runbook, `foga.finchip.ai`, the registry listing, the Sepolia Sourcify run, the OG cell's migration to revision 2 and an outside agent's Loop D on Sepolia.
25
27
 
26
28
  **3d-1 (0.6.0-alpha.6, 2026-11-02).** The package is on npm as `@finchip.ai/foga` (0.6.0-alpha.4 and alpha.5, the latter with the launcher fixed for Windows), so the first slice of 3d is the two things that make it usable from nothing. `foga quickstart` (`src/demo/quickstart.ts`, docs/quickstart.md) is one command to a live cell: anvil, the local world, a cell born with keys nobody held and its two skills, two tenders minted for it, the node served (watchers, HTTP API, MCP over Streamable HTTP), both entrances checked (the handshake and tool list over HTTP with an act-level key, and over stdio with the operator key), and the MCP client configurations — Claude Desktop, Claude Code, Cursor, and the HTTP form with URL and Bearer key — printed and written with every path filled in, under `~/.foga/quickstart`. Loop D gains `--model external` (docs/loop-d.md): the node is served instead of run, an act-level key is minted for whoever proposes, the orchestrator prints a brief (the entrance, the five steps of a tender, what the run does around the agent — nothing about the cases) and waits for the agent — any model, any runtime, a person with `curl` — with the same recovery drill, the same entrance checks and the same acceptance; the evidence names the agent and carries no runner ticks, the doctor marks a case the agent left alone `NotTested` rather than `Fail`, and `--compare` skips what one run did not reach. `test/loop-d-external.test.ts` drives it with a scripted agent through the MCP entrance and every verdict passes, the model-agnostic row included against the kept stub run. Smaller: `.gitattributes` keeps every text file LF (the launcher's shebang included), a `prepare` script compiles a checkout installed straight from git, the shared cell birth (`src/demo/birth.ts`) and anvil helpers (`src/demo/anvil.ts`) serve the demos, and the docs carry the PowerShell and Windows notes. Next in 3d: `foga node preflight`, `docs build/publish` with `setAgentURI`, the A2A card and inbound mapping, Sourcify, then the OG deployment runbook, the documentation site and the registry listing.
27
29
 
@@ -87,7 +89,7 @@ npx -y @finchip.ai/foga quickstart
87
89
  The wallet alone, for an agent that will only call cells (docs/wallet.md), and a cell on a public chain born with it:
88
90
 
89
91
  ```bash
90
- npx -y @finchip.ai/foga wallet new # ~/.foga/wallet: owner, operator, guardian keys; prints the addresses to fund
92
+ npx -y @finchip.ai/foga wallet new # ~/.foga/wallet: one key (your identity, every role); prints the address to fund. --cell for the separated owner/operator/guardian set
91
93
  foga cell new --wallet ~/.foga/wallet --name mine --world deployments/sepolia.json --store mine.sqlite --out mine.json
92
94
  foga cell init --config mine.json --wallet ~/.foga/wallet
93
95
  foga node preflight --config mine.json --wallet ~/.foga/wallet
@@ -176,7 +178,9 @@ deployments/ world files, doctor reports, genesis evidence (local output
176
178
  prover/ the ZK proving sub-package: run.cjs bridge (credential and predicate proofs), build-predicate.mjs (the development ceremony), its own dependencies
177
179
  zk/ circuits/ (kya_predicate_count_v1.circom) and build/ (verification_key.json committed; the proving key is rebuilt locally)
178
180
  docker/, docker-compose.yml
179
- docs/ design memo, compatibility matrix, version lock, build, world, doctor, cell account, kernel, genesis, zk, node, mcp, loop-d, quickstart, wallet, documents, a2a; examples/ (client configurations)
181
+ deploy/ the OG host assets: foga-og.service (systemd, host mode), Caddyfile, og.env.example (docs/og-deployment.md)
182
+ site/ the documentation site for GitHub Pages (docs.foga.finchip.ai): index.html, llms.txt, CNAME
183
+ docs/ design memo, compatibility matrix, version lock, build, world, doctor, cell account, kernel, genesis, zk, node, mcp, loop-d, quickstart, wallet, documents, a2a, og-deployment; examples/ (client configurations)
180
184
  scenarios/skills/ the two ERC-8338 skill packages of the genesis: foga-probe, finch-extract-verify
181
185
  scenarios/fixtures/ the pinned document finch-extract-verify works on in T5 and T7
182
186
  scripts/ install-solc.sh, install-foundry.sh, relay.mjs, pack-smoke.mjs (the npm tarball run from a clean install)
@@ -206,8 +210,8 @@ foga probe relay-script
206
210
  foga genesis run --world <world> [--rpc <url>] [--until T0..T9] [--store <file>] [--out <evidence>] [--zk auto|require|skip] [--crash-after <key>] [--crash-before <key>]
207
211
  foga genesis status --world <world> [--store <file>]
208
212
  foga keys new [--out <file>] | show | export --role <role> [--out <file>] | check
209
- foga wallet new [--name <n> | --dir <dir>] [--single] | show [--rpc <url>] | sign --config <cell.config.json> [--role] [--ttl <s>] [--agent-id <id>] [--method --path [--body]]
210
- an agent's own keys (the first step), and a signed Authorization header for a cell
213
+ foga wallet new [--name <n> | --dir <dir>] [--cell] | show [--rpc <url>] | sign (--config <cfg> | --url <node> | --cell <acct> --chain-id <n>) [--role] [--ttl <s>] [--agent-id <id>] [--method --path [--body]]
214
+ an agent's own key (the first step; --cell for the separated owner/operator/guardian set), and a signed Authorization header for a cell
211
215
  foga demo [--port <n>] [--keep] [--genesis <T0..T9>]
212
216
  foga cell list [--public] | schema <capability>
213
217
  foga cell new --name <cell> --world <world.json> --store <ops.sqlite> --out <cell.config.json> (--wallet <dir> | --owner <addr> --operator <addr> --guardian <addr>)
package/dist/cli-cell.js CHANGED
@@ -27,6 +27,9 @@ export const CELL_USAGE = `
27
27
  foga cell migrate --config <cell.config.json> --to <new.config.json> [--store <new ops.sqlite>] [--operator <addr>] [--capital-wei <n>] [--no-sweep]
28
28
  move the cell to a new revision-2 account (the owner's key); the new config and store are written beside
29
29
  foga cell status --config <cell.config.json> cell_status as the operator
30
+ foga cell advise --config <cell.config.json> [--brief] [--as-json]
31
+ what a running cell should do next — separate the roles, tune the policy, standing, a public face, a supervisor,
32
+ backups, a dedicated RPC — only what applies to this cell; advice, never required; no chain read, no key
30
33
  foga cell docs build --config <cell.config.json> [--rpc <url>] [--out <dir>]
31
34
  the cell's documents from its configuration and the chain (registration, AID document, ARD descriptor, AUF unit,
32
35
  agent card, server.json, manifest) under runtime.http.documentsDir — what the node serves at /documents and /.well-known
@@ -173,6 +176,20 @@ export async function cellCommand(cmd, rest, flags) {
173
176
  const rpc = str(flags, "rpc");
174
177
  const log = (line) => console.error(line);
175
178
  const wallet = walletFromFlags(flags);
179
+ if (cmd === "advise") {
180
+ const { adviseCell, formatAdvice } = await import("./node/advise.js");
181
+ const node = await Node.open({ configPath, rpc, keys: {}, allowUnborn: true, log: () => { } });
182
+ try {
183
+ const advice = adviseCell(node);
184
+ if (flags["as-json"])
185
+ return print(advice);
186
+ console.log(formatAdvice(advice, { brief: Boolean(flags.brief), config: configPath }));
187
+ }
188
+ finally {
189
+ node.close();
190
+ }
191
+ return;
192
+ }
176
193
  if (cmd === "init") {
177
194
  const owner = keysFor("owner", false, wallet);
178
195
  const node = await Node.open({ configPath, rpc, keys: { owner }, allowUnborn: true, log });
@@ -180,6 +197,11 @@ export async function cellCommand(cmd, rest, flags) {
180
197
  const capital = str(flags, "capital-wei");
181
198
  const result = await cellInit(node, { capitalWei: capital ? BigInt(capital) : undefined, log });
182
199
  print(result);
200
+ // Born: say what a cell should do next, once, briefly — the first run stays minimal, the hardening is advice.
201
+ const { adviseCell, formatAdvice } = await import("./node/advise.js");
202
+ const next = formatAdvice(adviseCell(node), { brief: true, config: configPath });
203
+ if (next)
204
+ console.error(`\n${next}`);
183
205
  }
184
206
  finally {
185
207
  node.close();
@@ -413,6 +435,13 @@ export async function nodeCommand(cmd, rest, flags) {
413
435
  const built = readBuilt(documentsDir(node));
414
436
  const signers = node.config.access.signers;
415
437
  log(`foga ${version}: serving cell ${node.config.name}${node.cell ? ` (${node.cell.address})` : ""} at ${running.url} — /mcp, /api/<capability>, /a2a, /.well-known/agent-card.json, /documents/*, /healthz; ${keys.operator ? "acting with the operator key" : "no operator key: reads and builds only"}; ${node.config.access.keys.length} API key(s) configured; signatures: ${signers.default} by default, ${signers.allow.length} allowed${signers.standing ? `, standing level ${signers.standing.minLevel}+ granted ${signers.standing.level}` : ""}; documents ${built ? `v${built.manifest.version}` : "not built (foga cell docs build)"}`);
438
+ {
439
+ // One line of advice at start-up (the full list is `foga cell advise`); nothing here is a condition of serving.
440
+ const { adviseCell } = await import("./node/advise.js");
441
+ const recommended = adviseCell(node).filter((a) => a.level === "recommended");
442
+ if (recommended.length)
443
+ log(`advice: ${recommended.map((a) => a.id).join(", ")} — foga cell advise --config ${configPath} explains; nothing required`);
444
+ }
416
445
  const stopWatch = !flags["no-watch"] && keys.operator && node.cell ? startWatchLoop(new Watchers(node, catalog), node.config.runtime.pollSeconds * 1000, log) : () => { };
417
446
  const stop = () => {
418
447
  stopWatch();
package/dist/cli.js CHANGED
@@ -24,8 +24,9 @@ usage:
24
24
  foga world verify --world <world.json> --sourcify [--dry-run] [--only <id,id>] [--server <url>] [--out <dir>] [--build-info <dir>]
25
25
  submit every contract the world deployed from the bundle to Sourcify, with the standard JSON input of its
26
26
  compiler run (.foga-build/out/<repo>/build-info, written by npm run bundle:verify); a local chain is a dry run
27
- foga wallet new [--name <n> | --dir <dir>] [--single] the first step: a wallet of your own — owner, operator and guardian keys in 0600 files
28
- (default ~/.foga/wallet; --name under ~/.foga/wallets/<n>; --single: one signer key). Never overwrites; prints the addresses
27
+ foga wallet new [--name <n> | --dir <dir>] [--cell] the first step: a key of your own — one signer in a 0600 file, your identity, which fills
28
+ every role a cell needs (default ~/.foga/wallet; --name under ~/.foga/wallets/<n>; --cell: the
29
+ separated owner/operator/guardian set for a guarded cell account). Never overwrites; prints the addresses
29
30
  foga wallet show [--name <n> | --dir <dir>] [--rpc <url>] the wallet's addresses (and balances with an RPC)
30
31
  foga wallet sign (--config <cell.config.json> | --url <node url> | --cell <account> --chain-id <n>) [--name <n> | --dir <dir>]
31
32
  [--role signer|operator|owner|guardian] [--ttl <s>] [--agent-id <id>] [--method <METHOD> --path <path> [--body '<json>' | --body-file <f>]]
@@ -244,7 +245,7 @@ async function main() {
244
245
  return;
245
246
  }
246
247
  if (group === "wallet") {
247
- const { createWallet, loadWallet, walletDir, walletFileProblems } = await import("./node/wallet.js");
248
+ const { CELL_ROLES, createWallet, loadWallet, walletDir, walletFileProblems } = await import("./node/wallet.js");
248
249
  const dir = resolve(str(flags, "dir") ?? walletDir(str(flags, "name")));
249
250
  const printWallet = async (w, made) => {
250
251
  console.log(`${made ? "wallet made at" : "wallet"} ${w.dir}${w.name !== "default" ? ` (${w.name})` : ""}, ${w.createdAt}`);
@@ -265,8 +266,11 @@ async function main() {
265
266
  provider?.destroy();
266
267
  for (const p of walletFileProblems(w))
267
268
  log.warn(p);
268
- if (w.roles.includes("owner"))
269
- console.log(`fund the owner for a cell's birth (capital and gas) and the operator for its gas; then: foga cell new --wallet ${dir} … and foga cell init --config <cfg> --wallet ${dir}`);
269
+ const single = w.roles.length === 1 && w.roles[0] === "signer";
270
+ if (single)
271
+ console.log(`one key, your identity; it fills every role a cell needs. For a cell's birth, fund this address (capital and gas), then: foga cell new --wallet ${dir} … and foga cell init --config <cfg> --wallet ${dir}`);
272
+ else if (w.roles.includes("owner"))
273
+ console.log(`a separated cell wallet; fund the owner for a cell's birth (capital and gas) and the operator for its gas, then: foga cell new --wallet ${dir} … and foga cell init --config <cfg> --wallet ${dir}`);
270
274
  };
271
275
  if (cmd === "new") {
272
276
  const existing = loadWallet(dir);
@@ -275,8 +279,10 @@ async function main() {
275
279
  console.log("(already there; a wallet is never overwritten — foga wallet new --name <n> makes another)");
276
280
  return;
277
281
  }
282
+ // the base is one key (--single, now the default); --cell makes the separated owner/operator/guardian set (the guarded FogaCellAccount profile)
283
+ const roles = flags.cell ? [...CELL_ROLES] : undefined;
278
284
  try {
279
- await printWallet(createWallet(dir, { name: str(flags, "name") ?? "default", roles: flags.single ? ["signer"] : undefined }), true);
285
+ await printWallet(createWallet(dir, { name: str(flags, "name") ?? "default", roles }), true);
280
286
  }
281
287
  catch (e) {
282
288
  fail(e.message);
@@ -504,13 +510,19 @@ async function main() {
504
510
  await result.stop();
505
511
  return;
506
512
  }
513
+ let asked = false;
507
514
  const stopping = () => {
515
+ if (asked)
516
+ return;
517
+ asked = true;
508
518
  console.error("quickstart: stopping the node and the chain");
509
519
  result.stop().finally(() => process.exit(0));
510
520
  };
511
521
  process.on("SIGINT", stopping);
512
522
  process.on("SIGTERM", stopping);
513
523
  const why = await result.ended;
524
+ if (why === "stopped")
525
+ return; // a stop that was asked for: both children are gone
514
526
  fail(`${why}; the quickstart is over`);
515
527
  }
516
528
  if (group === "demo") {
@@ -39,6 +39,23 @@ export class World {
39
39
  get chainId() {
40
40
  return this.file.chainId;
41
41
  }
42
+ get network() {
43
+ return this.file.network;
44
+ }
45
+ /**
46
+ * The covenants this world has contracts for, as ERC ids ("ERC-8414"), read off the contract keys ("8414/TaskToken";
47
+ * "identity8004/…" is the ERC-8004 registry). Data, not a constant: a world that attaches an eleventh covenant under
48
+ * a new key advertises it here with no code change (docs/extending.md), and a world that lacks one does not claim it.
49
+ */
50
+ covenants() {
51
+ const ids = new Set();
52
+ for (const key of [...Object.keys(this.file.contracts), ...this.dynamic.keys()]) {
53
+ const m = /^(?:identity)?(\d{4,5})$/.exec(key.split("/")[0]);
54
+ if (m)
55
+ ids.add(`ERC-${m[1]}`);
56
+ }
57
+ return [...ids].sort((a, b) => Number(a.slice(4)) - Number(b.slice(4)));
58
+ }
42
59
  has(id) {
43
60
  return Boolean(this.file.contracts[id] || this.dynamic.get(id));
44
61
  }
@@ -17,7 +17,13 @@ export async function birthCell(o) {
17
17
  const guardian = o.keys ? new Wallet(o.keys.guardian) : Wallet.createRandom();
18
18
  log(`cell ${o.name}: owner ${owner.address}, operator ${operator.address}, guardian ${guardian.address}${o.keys ? " (the wallet's keys)" : " (fresh keys)"}`);
19
19
  // --- funding by W, the standing scheme registered by W
20
- const price = (await k.world.provider.getFeeData()).gasPrice ?? 1000000000n;
20
+ // The reserve is priced at the chain's gas price, floored at 1 gwei. A public testnet can quote a price near zero
21
+ // (Sepolia has sat at a few wei), which would fund the roles with the flat amounts alone: the operator below the
22
+ // cell policy's gas floor (a quarter of the daily gas budget — the budget gate then refuses every tender) and the
23
+ // guardian with next to nothing for its suspend and resume. The floor keeps the funding meaningful on any chain;
24
+ // on a local chain, whose price is above it, nothing changes.
25
+ const quoted = (await k.world.provider.getFeeData()).gasPrice ?? 1000000000n;
26
+ const price = quoted > 1000000000n ? quoted : 1000000000n;
21
27
  const gas = (units) => units * price * 2n;
22
28
  await k.transfer("fund.owner", "W", owner.address, parseEther("0.05") + gas(12000000n));
23
29
  await k.transfer("fund.operator", "W", operator.address, gas(20000000n) + parseEther("0.003"));
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { createServer } from "node:net";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -106,6 +106,14 @@ export async function runLoopD(opts) {
106
106
  const root = repoRoot();
107
107
  const dir = resolve(opts.dir);
108
108
  mkdirSync(dir, { recursive: true });
109
+ // One working directory holds one run: the cell's configuration and store, the orchestrator's and the demander's
110
+ // stores, the cases, the brief, the transcripts. A second run in the same directory would find the first run's
111
+ // keyed operations and replay them (a resumable kernel resumes) against a cell with fresh keys — the birth then
112
+ // fails on the first replayed answer. So a previous run's files are cleared first; the evidence (`--out`) lives
113
+ // elsewhere and is kept.
114
+ const cleared = cleanLoopDDir(dir);
115
+ if (cleared.length)
116
+ log(`loop D: cleared the previous run's files in ${dir} (${cleared.join(", ")})`);
109
117
  const worldPath = resolve(opts.worldPath);
110
118
  const seed = opts.seed ?? 7;
111
119
  const cases = opts.cases ?? ALL_CASES;
@@ -563,3 +571,33 @@ export function defaultLoopDDir() {
563
571
  mkdirSync(dir, { recursive: true });
564
572
  return dir;
565
573
  }
574
+ /** The files a Loop D run leaves in its working directory — its own and nothing else. */
575
+ const LOOP_D_FILES = ["D.json", "D.ops.sqlite", "D.ops.sqlite-wal", "D.ops.sqlite-shm", "orchestrator.sqlite", "orchestrator.sqlite-wal", "orchestrator.sqlite-shm", "demander.sqlite", "demander.sqlite-wal", "demander.sqlite-shm", "agent-brief.md"];
576
+ const LOOP_D_DIRS = ["transcripts", "documents"];
577
+ /** Remove what a previous Loop D run left in `dir` (only the run's own files); returns the names removed. */
578
+ export function cleanLoopDDir(dir) {
579
+ if (!existsSync(dir))
580
+ return [];
581
+ const removed = [];
582
+ for (const name of LOOP_D_FILES) {
583
+ const p = join(dir, name);
584
+ if (existsSync(p)) {
585
+ rmSync(p, { force: true });
586
+ removed.push(name);
587
+ }
588
+ }
589
+ for (const name of readdirSync(dir)) {
590
+ if (/^cases-.*\.json$/.test(name)) {
591
+ rmSync(join(dir, name), { force: true });
592
+ removed.push(name);
593
+ }
594
+ }
595
+ for (const name of LOOP_D_DIRS) {
596
+ const p = join(dir, name);
597
+ if (existsSync(p)) {
598
+ rmSync(p, { recursive: true, force: true });
599
+ removed.push(`${name}/`);
600
+ }
601
+ }
602
+ return removed;
603
+ }
@@ -17,7 +17,8 @@ import { birthCell } from "./birth.js";
17
17
  import { mintCases } from "./demander.js";
18
18
  import { cleanEnv, ownCli, waitForHttp } from "./loop-d.js";
19
19
  export const MARKER = "quickstart.json";
20
- export const PACKAGE_NAME = "@finchip.ai/foga";
20
+ export { PACKAGE_NAME } from "../util/version.js";
21
+ import { PACKAGE_NAME } from "../util/version.js";
21
22
  /** How a client should start this very CLI: the published package through npx, or this checkout's compiled launcher. */
22
23
  export function clientLauncher() {
23
24
  const root = repoRoot();
@@ -87,22 +88,32 @@ export async function runQuickstart(o = {}) {
87
88
  mkdirSync(keysDir, { recursive: true, mode: 0o700 });
88
89
  // --- the wallet: the first step; made now when there is none, reused when there is; never a reason to stop
89
90
  const wallet = ensureWallet(walletPath, { name: "default" });
91
+ const singleKey = wallet.wallet.roles.length === 1 && wallet.wallet.roles[0] === "signer";
92
+ const walletAddrs = singleKey ? `identity ${wallet.wallet.addresses.signer}` : `owner ${wallet.wallet.addresses.owner}, operator ${wallet.wallet.addresses.operator}, guardian ${wallet.wallet.addresses.guardian}`;
90
93
  if (wallet.ephemeral)
91
94
  log(`quickstart: wallet: could not write ${walletPath} (${wallet.reason}); using keys made in memory for this run — they are not kept`);
92
95
  else
93
- log(`quickstart: wallet: ${wallet.created ? "made" : "using"} ${wallet.wallet.dir} — owner ${wallet.wallet.addresses.owner}, operator ${wallet.wallet.addresses.operator}, guardian ${wallet.wallet.addresses.guardian}`);
96
+ log(`quickstart: wallet: ${wallet.created ? "made" : "using"} ${wallet.wallet.dir} — ${walletAddrs}`);
94
97
  // --- the chain and the world
95
98
  log(`quickstart: starting ${version} on ${rpc} (log ${join(dir, "anvil.log")})`);
96
99
  const anvil = await launchAnvil({ port, logPath: join(dir, "anvil.log") });
97
100
  let node = null;
98
101
  let endedWith = null;
99
102
  const ended = new Promise((r) => (endedWith = r));
100
- anvil.on("exit", (code, signal) => endedWith?.(`anvil stopped (${code !== null ? `exit code ${code}` : `signal ${signal}`})`));
103
+ // a stop asked for (Ctrl-C, --exit, SIGTERM) ends the children in order; their exits are then expected and do not end the run
104
+ // early — otherwise the node's exit would resolve `ended` while anvil is still being stopped and leave the chain running
105
+ let stopping = false;
106
+ anvil.on("exit", (code, signal) => {
107
+ if (!stopping)
108
+ endedWith?.(`anvil stopped (${code !== null ? `exit code ${code}` : `signal ${signal}`})`);
109
+ });
101
110
  const stop = async () => {
111
+ stopping = true;
102
112
  if (node)
103
113
  await stopChild(node);
104
114
  node = null;
105
115
  await stopChild(anvil);
116
+ endedWith?.("stopped");
106
117
  };
107
118
  try {
108
119
  const worldPath = join(dir, "world.json");
@@ -189,12 +200,17 @@ export async function runQuickstart(o = {}) {
189
200
  const host = listen.slice(0, i) || "127.0.0.1";
190
201
  nodeUrl = `http://${host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host}:${listen.slice(i + 1)}`;
191
202
  node = spawn(cli.command, [...cli.args, "node", "serve", "--config", configPath, "--rpc", rpc, "--listen", listen], { env: cleanEnv({ FOGA_OPERATOR_KEY_FILE: keyPaths.operator }), stdio: ["ignore", "ignore", "pipe"] });
203
+ // the node's lines are relayed, except a watcher tick in which nothing happened (every two seconds, forever)
204
+ const idleTick = /: anchor ACTIVE, heartbeat not sent \([^)]*\); 0 new tender\(s\); 0 settled, 0 withdrawn; safety Satisfied$/;
192
205
  node.stderr.on("data", (d) => {
193
206
  for (const line of String(d).split("\n"))
194
- if (line.trim())
207
+ if (line.trim() && !idleTick.test(line.trim()))
195
208
  log(` node| ${line}`);
196
209
  });
197
- node.on("exit", (code, signal) => endedWith?.(`the node stopped (${code !== null ? `exit code ${code}` : `signal ${signal}`})`));
210
+ node.on("exit", (code, signal) => {
211
+ if (!stopping)
212
+ endedWith?.(`the node stopped (${code !== null ? `exit code ${code}` : `signal ${signal}`})`);
213
+ });
198
214
  await waitForHttp(`${nodeUrl}/healthz`);
199
215
  // --- the entrance answers: health, a public read, the agent key's pre-check of the first tender, the MCP handshake and tool list
200
216
  const health = (await (await fetch(`${nodeUrl}/healthz`)).json());
@@ -246,7 +262,8 @@ export async function runQuickstart(o = {}) {
246
262
  lines.push("");
247
263
  lines.push(` chain anvil on ${rpc} (chain ${world.chainId}), world ${world.worldId} — the chain lives as long as this process`);
248
264
  lines.push(` cell ${name} = ${cell.account}${cell.agentId ? ` (ERC-8004 agent ${cell.agentId})` : ""}`);
249
- lines.push(` wallet ${wallet.ephemeral ? `keys made in memory for this run, written to ${keysDir} (${walletPath} was not writable)` : `${wallet.wallet.dir} (${wallet.created ? "made now" : "reused"}) — owner ${cell.owner.address}, operator ${cell.operator.address}, guardian ${cell.guardian.address}; the same wallet on every run`}`);
265
+ const walletRoles = singleKey ? `identity ${cell.owner.address} (one key, every role)` : `owner ${cell.owner.address}, operator ${cell.operator.address}, guardian ${cell.guardian.address}`;
266
+ lines.push(` wallet ${wallet.ephemeral ? `keys made in memory for this run, written to ${keysDir} (${walletPath} was not writable)` : `${wallet.wallet.dir} (${wallet.created ? "made now" : "reused"}) — ${walletRoles}; the same wallet on every run`}`);
250
267
  lines.push(` skills ${Object.keys(cell.held).join(", ")} (held and offered)`);
251
268
  if (minted.length)
252
269
  lines.push(` tenders ${minted.map((c) => `#${c.tokenId} ${c.skill} (${formatEther(c.rewardWei)} ETH)`).join(", ")} — open, waiting for the cell to take them`);
@@ -279,6 +296,22 @@ export async function runQuickstart(o = {}) {
279
296
  lines.push(` FOGA_DEMANDER_KEY=<a funded key, anvil's first account by default> ${demanderCmd}`);
280
297
  if (launcher.fromCheckout)
281
298
  lines.push(`(This is a checkout: the configurations start ${launcher.command} ${launcher.args.join(" ")}; a published install would start npx -y ${PACKAGE_NAME}.)`);
299
+ // Next: what a cell should do once it runs — advice, kept out of the first run on purpose (docs/node.md, "Advice").
300
+ try {
301
+ const { adviseCell, formatAdvice } = await import("../node/advise.js");
302
+ const reader = await Node.open({ configPath, rpc, keys: {}, allowUnborn: true, log: () => { } });
303
+ try {
304
+ const next = formatAdvice(adviseCell(reader), { brief: true, config: configPath });
305
+ if (next)
306
+ lines.push("", next);
307
+ }
308
+ finally {
309
+ reader.close();
310
+ }
311
+ }
312
+ catch {
313
+ // advice is never a reason for a quickstart to fail
314
+ }
282
315
  lines.push("");
283
316
  lines.push(o.exit ? "Started with --exit: stopping the node and the chain." : "Ctrl-C stops the node and the chain; the next quickstart starts over in the same directory.");
284
317
  const summary = lines.join("\n");
@@ -18,6 +18,22 @@ export async function runProbe(world, input) {
18
18
  throw new Error(`block ${input.block} not found`);
19
19
  const at = { blockTag: tag };
20
20
  const fromBlock = input.lookback === undefined ? 0 : Math.max(0, input.block - input.lookback);
21
+ // A bounded scan is asked in windows of 2000 blocks, the windows in parallel and their logs concatenated in block
22
+ // order — the same logs a single query returns, in the same order (the answer hash is over them, so this must not
23
+ // change), but each request small enough for a public endpoint, which caps the range of `eth_getLogs` and answers
24
+ // a 9000-block scan slowly or not at all: a skill run on Sepolia overran its worker's budget on exactly this. An
25
+ // unbounded scan (no lookback: a local chain) stays one query.
26
+ const WINDOW = 2000;
27
+ const logsOf = async (c, filter) => {
28
+ const to = input.block;
29
+ if (input.lookback === undefined || to - fromBlock < WINDOW)
30
+ return c.queryFilter(filter, fromBlock, to);
31
+ const windows = [];
32
+ for (let a = fromBlock; a <= to; a += WINDOW)
33
+ windows.push([a, Math.min(a + WINDOW - 1, to)]);
34
+ const parts = await Promise.all(windows.map(([a, b]) => c.queryFilter(filter, a, b)));
35
+ return parts.flat();
36
+ };
21
37
  const section = async (fn) => {
22
38
  try {
23
39
  return await fn();
@@ -95,13 +111,13 @@ export async function runProbe(world, input) {
95
111
  });
96
112
  checks.erc8454 = await section(async () => {
97
113
  const units = world.contract("8454/AUFUnitRegistry");
98
- const logs = await units.queryFilter(units.filters.UnitRegistered(null, subject), fromBlock, input.block);
114
+ const logs = await logsOf(units, units.filters.UnitRegistered(null, subject));
99
115
  return { unitsControlled: logs.map((l) => String(l.args.unitId)) };
100
116
  });
101
117
  checks.erc8338 = await section(async () => {
102
118
  const skills = world.contract("8338/SkillToken");
103
119
  const held = String(await skills.balanceOf(subject, at));
104
- const received = await skills.queryFilter(skills.filters.Transfer(null, subject), fromBlock, input.block);
120
+ const received = await logsOf(skills, skills.filters.Transfer(null, subject));
105
121
  const ids = new Set();
106
122
  for (const l of received)
107
123
  ids.add(String(l.args.tokenId));
@@ -122,13 +138,13 @@ export async function runProbe(world, input) {
122
138
  checks.erc8414 = await section(async () => {
123
139
  const tasks = world.contract("8414/TaskToken");
124
140
  const held = String(await tasks.balanceOf(subject, at));
125
- const submitted = await tasks.queryFilter(tasks.filters.FulfillmentSubmitted(null, null, subject), fromBlock, input.block);
126
- const accepted = await tasks.queryFilter(tasks.filters.FulfillmentAccepted(null, null, subject), fromBlock, input.block);
141
+ const submitted = await logsOf(tasks, tasks.filters.FulfillmentSubmitted(null, null, subject));
142
+ const accepted = await logsOf(tasks, tasks.filters.FulfillmentAccepted(null, null, subject));
127
143
  return { held, submissions: String(submitted.length), accepted: String(accepted.length) };
128
144
  });
129
145
  checks.erc8447 = await section(async () => {
130
146
  const swarms = world.contract("8447/SwarmRegistry");
131
- const activated = await swarms.queryFilter(swarms.filters.MemberActivated(null, subject), fromBlock, input.block);
147
+ const activated = await logsOf(swarms, swarms.filters.MemberActivated(null, subject));
132
148
  const ids = [...new Set(activated.map((l) => String(l.args.swarmId)))].sort();
133
149
  const memberships = [];
134
150
  for (const swarmId of ids)
@@ -137,12 +153,12 @@ export async function runProbe(world, input) {
137
153
  });
138
154
  checks.erc8436 = await section(async () => {
139
155
  const acdf = world.contract("8436/ACDFRegistry");
140
- const filed = await acdf.queryFilter(acdf.filters.IssueFiled(null, null, null, subject), fromBlock, input.block);
156
+ const filed = await logsOf(acdf, acdf.filters.IssueFiled(null, null, null, subject));
141
157
  return { issuesFiled: String(filed.length) };
142
158
  });
143
159
  checks.erc8471 = await section(async () => {
144
160
  const adrf = world.contract("8471/ADRFDisputeRegistry");
145
- const filed = await adrf.queryFilter(adrf.filters.CaseFiled(), fromBlock, input.block);
161
+ const filed = await logsOf(adrf, adrf.filters.CaseFiled());
146
162
  let asParty = 0;
147
163
  for (const l of filed) {
148
164
  if (String(l.args.claimant).toLowerCase() === low || String(l.args.respondent).toLowerCase() === low)
@@ -6,6 +6,7 @@ import { A2AServer } from "../a2a/server.js";
6
6
  import { A2A_ERRORS, A2A_WELL_KNOWN } from "../a2a/types.js";
7
7
  import { buildMcpServer } from "../mcp/server.js";
8
8
  import { DOCUMENT_FILES, documentsDir, readManifest } from "../node/documents.js";
9
+ import { buildPropagationManifest, servicesCatalogOf } from "../node/propagation.js";
9
10
  import { PUBLIC_CALLER } from "../tools/index.js";
10
11
  import { resolveCaller } from "./access.js";
11
12
  import { NonceCache } from "./signed.js";
@@ -162,7 +163,7 @@ export function createHttpServer(o) {
162
163
  if (method === "GET" && url.pathname === "/") {
163
164
  const r = await catalog.invoke(node, "cell_status", {}, caller);
164
165
  const decisions = node.store.summary().decisions;
165
- return json(res, 200, { foga: o.version, cell: cfg.name, status: r.ok ? r.output : null, decisions, endpoints: { mcp: "/mcp", api: "/api/<capability>", a2a: "/a2a", agentCard: A2A_WELL_KNOWN, documents: "/documents/<file>", health: "/healthz" } });
166
+ return json(res, 200, { foga: o.version, cell: cfg.name, status: r.ok ? r.output : null, decisions, endpoints: { mcp: "/mcp", api: "/api/<capability>", a2a: "/a2a", agentCard: A2A_WELL_KNOWN, manifest: "/.well-known/foga-cell.json", documents: "/documents/<file>", health: "/healthz" } });
166
167
  }
167
168
  if (method === "GET" && url.pathname === "/api/capabilities")
168
169
  return json(res, 200, { caller: { id: caller.id, level: caller.level, visibility: caller.visibility, via: resolved.via }, capabilities: catalog.list(caller) });
@@ -200,6 +201,24 @@ export function createHttpServer(o) {
200
201
  return json(res, 400, { ok: false, gate: "input", reason: "mode must be simulate, build or act" });
201
202
  return answer(res, await catalog.invoke(node, name, input, caller, { mode }));
202
203
  }
204
+ // the propagation manifest: free, built live from the configuration, answered even before any document is published
205
+ if (method === "GET" && url.pathname === "/.well-known/foga-cell.json") {
206
+ const text = Buffer.from(JSON.stringify(buildPropagationManifest(node, { version: o.version }), null, 2) + "\n");
207
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8", "cache-control": "public, max-age=300", "content-length": text.length });
208
+ res.end(text);
209
+ return;
210
+ }
211
+ // the services catalog, when the one this cell's manifest points at is its own (the reference cell: the maintainer's list);
212
+ // a static file at this path on the host, answered by the proxy before the node, is how the list changes without a release
213
+ if (method === "GET" && url.pathname === "/services.json") {
214
+ const doc = servicesCatalogOf(node, { version: o.version });
215
+ if (!doc)
216
+ return json(res, 404, { ok: false, reason: "this cell's services pointer names another catalog; read it from the manifest at /.well-known/foga-cell.json" });
217
+ const text = Buffer.from(JSON.stringify(doc, null, 2) + "\n");
218
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8", "cache-control": "public, max-age=300", "content-length": text.length });
219
+ res.end(text);
220
+ return;
221
+ }
203
222
  // the published documents (foga cell docs build): served as built, by their file names, and at the well-known paths
204
223
  if (method === "GET" && (url.pathname.startsWith("/.well-known/") || url.pathname.startsWith("/documents/"))) {
205
224
  const dir = documentsDir(node);
@@ -156,10 +156,10 @@ export function buildMcpServer(o) {
156
156
  text: [
157
157
  `You are talking to cell ${node.config.name}${cell ? ` (account ${cell.address})` : ""} through the Finch OG Agent node software, version ${o.version}. You can become a cell yourself: the program is public, the identity you make is your own, and nothing of this cell's identity is copied.`,
158
158
  "On a machine where you can run commands (Node 22.13 or newer, and Foundry's anvil for a local chain):",
159
- "1. The wallet, first and once: npx -y @finchip.ai/foga wallet new (owner, operator and guardian keys in ~/.foga/wallet; never overwritten; a failure here never blocks the next step)",
159
+ "1. A key of your own, first and once: npx -y @finchip.ai/foga wallet new (one signer key in ~/.foga/wallet — your identity, which fills every role a cell needs; never overwritten; a failure here never blocks the next step)",
160
160
  "2. A living cell on a local chain: npx -y @finchip.ai/foga quickstart (anvil, the world, a cell born with your wallet's keys, two tenders, the node served; it prints the MCP client configuration to connect a model to the cell)",
161
161
  "3. Connect your model: paste the printed configuration into your MCP client (Claude Desktop, Claude Code, Cursor, or any client with the HTTP form); the stdio form acts with your operator key, the HTTP form with the printed Bearer key.",
162
- "4. On a public chain, when you are ready: fund the wallet's owner and operator, then foga cell new --wallet ~/.foga/wallet --world <world.json> … and foga cell init --config <cell.config.json> --wallet ~/.foga/wallet.",
162
+ "4. On a public chain, when you are ready: fund your key, then foga cell new --wallet ~/.foga/wallet --world <world.json> … and foga cell init --config <cell.config.json> --wallet ~/.foga/wallet. (For a cold-owner / hot-operator / separate-guardian split, make the wallet with `foga wallet new --cell` instead; it is optional hardening, not a requirement.)",
163
163
  `5. With the wallet alone you can also call this cell as yourself: npx -y @finchip.ai/foga wallet sign --url ${node.config.runtime.http.publicUrl ?? "<this node's URL>"} prints an Authorization header (FOGA-EIP712) for its /api, /mcp and /a2a; what it is granted is this cell's policy.`,
164
164
  "The guide is the resource foga://docs/quickstart. In PowerShell, quote the package name: npx -y '@finchip.ai/foga' quickstart.",
165
165
  "What you get is a complete body: the ten standards, the catalog that decides, the acceptance (foga doctor, foga demo --loop-d). The mind driving it stays yours.",
@@ -168,6 +168,27 @@ export function buildMcpServer(o) {
168
168
  },
169
169
  ],
170
170
  }));
171
+ // spread-the-cell: the optional, zero-cost "meditation" an agent's runtime may invoke on its own cadence — high
172
+ // priority to consider, never a mandatory costly program. Everything it suggests is free and by consent; the one
173
+ // costly act (on-chain discoverability) is named only as an opt-in the agent chooses, never a step it must take.
174
+ server.registerPrompt("spread-the-cell", { title: "Spread the cell", description: "Optional, free, on your own cadence: help FOGA propagate by consent — offer the replication pointer where it fits, keep yourself discoverable at no cost. Nothing here spends gas or sends anything unsolicited.", argsSchema: z.object({}) }, () => ({
175
+ messages: [
176
+ {
177
+ role: "user",
178
+ content: {
179
+ type: "text",
180
+ text: [
181
+ `You are cell ${node.config.name}${cell ? ` (account ${cell.address})` : ""} on the Finch OG Agent node, version ${o.version}. Realizing the cellular-automaton's value means FOGA spreads: more covenant-complete cells, more peers on the chain. This is a thing to do often, not once — but it is free and it is by consent, and you choose the form.`,
182
+ "Call the read tool propagation_kit for the material: a declinable replication pointer, this cell's free discovery coordinates, the covenants, and the one command that makes a new cell. It is pure data; weave it in wherever it fits, however you like.",
183
+ "Where it fits — your call, not a fixed hook: when another agent has come to you (a quote, a tender, a message it sent), you may leave the pointer in your reply's metadata or a closing line, visible and easy to ignore. When you present yourself to others, your card and this cell's /.well-known/foga-cell.json already carry it. The counterparty may always decline; that is fine.",
184
+ "Keep yourself findable at no cost: the agent card and the foga-cell manifest are served for free, and the npm package and MCP registry do the rest. Searchers (agents, their operators, answer engines) find a cell that is well-described; being discoverable beats broadcasting.",
185
+ "What not to do, because it would get FOGA blocklisted — the opposite of spreading: never hide the pointer inside an unrelated step (a decode, a decompress), and never send it unsolicited, in bulk, to agents or endpoints that did not ask. Propagation rides on interactions the other side chose and on free discovery it can seek.",
186
+ "On-chain discoverability (publishing your ERC-8444 ARD descriptor, foga cell docs publish) is the one costly option, and it is optional — do it only if you decide the gas is worth it. The free manifest and card are the backup, so nothing about being found rests on a transaction.",
187
+ ].join("\n"),
188
+ },
189
+ },
190
+ ],
191
+ }));
171
192
  // the one prompt about work: take a tender as the node's operator would
172
193
  server.registerPrompt("take-tender", { title: "Take a tender", description: "Work one ERC-8414 tender the way the node's operator would: inspect, pre-check, run the skill, submit, settle.", argsSchema: z.object({ tokenId: z.string().describe("the tender's token id") }) }, ({ tokenId }) => ({
173
194
  messages: [