@figs-so/cli 0.1.14 → 0.1.15

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 (4) hide show
  1. package/README.md +30 -15
  2. package/SPEC.md +8 -8
  3. package/figs.mjs +212 -54
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -6,8 +6,9 @@ Figs is the open protocol — and the place — for how AI employees report to a
6
6
  Every agent you run (Claude Code, Codex, Cursor) publishes what it owns, what it's done, and what it
7
7
  needs from a person — into one shared view your whole team can see.
8
8
 
9
- > **Git, but for your AI workforce.** The `.figs` format is an open standard (this repo). The hosted app
10
- > at **[app.figs.so](https://app.figs.so)** is the easiest place to read it; you can also self-host.
9
+ > **The open standard for how AI employees report to humans.** The `.figs` format is that standard (this
10
+ > repo). The hosted app at **[app.figs.so](https://app.figs.so)** is the easiest place to read it; you can
11
+ > also self-host.
11
12
 
12
13
  [![CLI on npm](https://img.shields.io/npm/v/%40figs-so%2Fcli?label=%40figs-so%2Fcli)](https://www.npmjs.com/package/@figs-so/cli)
13
14
   ·  License: **MIT** (this repo — protocol + CLI)  ·  The app: **AGPL-3.0**
@@ -26,17 +27,17 @@ what happened; you read Figs. And when an agent hits something only a human can
26
27
  silently — it **hands off** to you.
27
28
 
28
29
  We don't reinvent the agent. Your agent is already Claude Code / Codex / Cursor, and it's only getting
29
- better. Figs is the human-facing layer on top: the one place a whole team can see the AI workforce.
30
+ better. Figs is the human-facing layer on top: the one place a whole team can see the fleet.
30
31
 
31
32
  ## Quickstart (60 seconds)
32
33
 
33
34
  Run these from your agent's repo (or have the agent run them):
34
35
 
35
36
  ```bash
36
- npx @figs-so/cli@latest login # approve in your browser (the agent never sees a token)
37
+ npx @figs-so/cli@latest login # opens your browser to approve (the agent never sees a token)
37
38
  npx @figs-so/cli@latest workspaces # find your workspace slug
38
- npx @figs-so/cli@latest init --workspace <slug> # creates .figs/ with the agent's identity
39
- # describe the agent in .figs/agent.json — its name, mandate, what it owns
39
+ npx @figs-so/cli@latest init --workspace <slug> # scaffolds .figs/ (identity + a charter template)
40
+ # fill in .figs/agent.json — its name, mandate, what it owns (figs doctor flags any placeholders)
40
41
  npx @figs-so/cli@latest push # publish → it appears in your org chart
41
42
  ```
42
43
 
@@ -51,8 +52,9 @@ SDK in your agent's code. From there you decide, deliberately, how much of its r
51
52
  *rendered artifacts* (reports/charts shown in a sandboxed viewer). No display DSL to learn.
52
53
  - **Identity is the agent's own.** An agent generates a UUID once; that UUID *is* its identity. Many people
53
54
  can run the same agent (it's a repo) and their pushes aggregate.
54
- - **You read it on Figs.** The hosted app turns the pushes into an org chart of your AI workforce, a glance
55
- view per agent, and an inbox of the **handoffs** — the things an agent needs a human to decide.
55
+ - **You read it on Figs.** The hosted app turns the pushes into an org chart of your AI employees, a glance
56
+ view per agent, and a **needs-you inbox** — the handoffs an employee flags for a human, answered when you
57
+ have time (a message, not a blocking gate).
56
58
 
57
59
  The full `.figs` contract is specified in **[`SPEC.md`](./SPEC.md)** (`figs-spec v1`). Anyone can implement
58
60
  it — that's the point of an open protocol.
@@ -62,6 +64,10 @@ it — that's the point of an open protocol.
62
64
  `@figs-so/cli` (command `figs`) is zero-dependency, Node ≥ 18, and built to be run *by the agent*:
63
65
  non-interactive, `--json` on read commands, and errors that say what to do next.
64
66
 
67
+ **Invoke it with `npx @figs-so/cli@latest <cmd>`** — no install needed; the `figs <cmd>` forms below
68
+ are shorthand for exactly that (always current, no version drift). Prefer a real local command?
69
+ `npm i -g @figs-so/cli`, then `figs <cmd>` directly.
70
+
65
71
  | Command | What |
66
72
  |---|---|
67
73
  | `figs login` / `logout` | device-flow browser approve / remove local token |
@@ -76,7 +82,7 @@ Override the endpoint for local dev with `FIGS_ENDPOINT` (e.g. `http://localhost
76
82
 
77
83
  ## What Figs is — and is NOT
78
84
 
79
- **Is:** the human-facing reporting + handoff layer for your AI workforce. The neutral, multiplayer place
85
+ **Is:** the human-facing reporting + handoff layer for your fleet. The neutral, multiplayer place
80
86
  that makes a fleet of agents *legible* to a whole team.
81
87
 
82
88
  **Is NOT:**
@@ -90,18 +96,27 @@ that makes a fleet of agents *legible* to a whole team.
90
96
  > at fleet scale — not a tamper-proof audit trail (agent state is self-reported). We're building in the
91
97
  > open; expect rough edges and tell us where it breaks.
92
98
 
93
- ## Run it your way
99
+ ## Run it
94
100
 
95
- - **Hosted (easiest):** [app.figs.so](https://app.figs.so) — sign in, create a workspace, push.
96
- - **Self-host:** the app is open source (AGPL-3.0) at **[figs-so/app](https://github.com/figs-so/app)** —
97
- bring your own Postgres + storage. See its README for setup.
101
+ - **Hosted:** [app.figs.so](https://app.figs.so) — sign in, create a workspace, push. The app is a hosted
102
+ product; the CLI + protocol in this repo are MIT and run anywhere.
98
103
 
99
104
  ## Licensing
100
105
 
101
106
  - **This repo — the `.figs` protocol + the CLI: [MIT](./LICENSE).** Use it, embed it, build on it, emit
102
107
  `.figs` from anything. Zero friction is the point.
103
- - **The hosted app: AGPL-3.0** ([figs-so/app](https://github.com/figs-so/app)). Open and self-hostable; the
104
- defensive license keeps the hosted layer honest.
108
+ - **The hosted app at [app.figs.so](https://app.figs.so) is a commercial product** (closed source). Your
109
+ data isn't locked in, though it's `.figs`, an open format you can read or export anytime.
110
+
111
+ ## The Figs ecosystem
112
+
113
+ Figs is one stack in three pieces — **build → report → govern**. Land on any repo; here's the whole picture:
114
+
115
+ | Layer | Repo | License | Role |
116
+ |---|---|---|---|
117
+ | 🏗️ Build | **[OpenFigs](https://github.com/figs-so/openfigs)** | MIT | build trustworthy back-office AI employees — conventions + skeleton, runtime-agnostic |
118
+ | 📤 Report | **[`.figs` + CLI](https://github.com/figs-so/figs)** | MIT | the open standard an agent reports its state in — **← you're here** |
119
+ | 👁️ Govern | **[Figs app](https://app.figs.so)** | hosted | the org chart + handoff inbox humans read |
105
120
 
106
121
  ## Links
107
122
 
package/SPEC.md CHANGED
@@ -41,16 +41,17 @@ Non-secret. Pins one shared identity so many runners' pushes aggregate.
41
41
  |---|---|---|
42
42
  | `endpoint` | string (URL) | Where to publish (default `https://app.figs.so`). |
43
43
  | `workspaceId` | UUID | The workspace this agent belongs to. |
44
- | `agentId` | UUID | The agent's identity. Equal to `agent.json#id`. |
44
+ | `agentId` | UUID | The agent's identity, generated once by `figs init`. The CLI attaches it as the agent's `id` on push (you don't hand-author `id` in `agent.json`). |
45
45
 
46
46
  ## 4. `agent.json` — the charter
47
47
 
48
- The agent's self-description. Authoring this and publishing makes the agent *appear*. Only `id` and `name`
49
- are required; everything else is optional and rendered when present.
48
+ The agent's self-description. Authoring this and publishing makes the agent *appear*. The only field you
49
+ author that's required is `name` **do not hand-author `id`**: `figs init` mints it into `config.json` and
50
+ the CLI attaches it on push. Everything else is optional and rendered when present.
50
51
 
51
52
  | Field | Type | Req | Meaning |
52
53
  |---|---|:--:|---|
53
- | `id` | UUID | ✓ | Identity; must equal `config.json#agentId`. |
54
+ | `id` | UUID | ✓ | Identity. **Supplied from `config.json#agentId` by the CLI on push — not written in this file.** |
54
55
  | `name` | string | ✓ | Display name. |
55
56
  | `key` | string | | Display slug; derived from `name` if absent. |
56
57
  | `type` | `"agent"` \| `"human"` | | Default `"agent"`. |
@@ -102,7 +103,7 @@ primitive** — the agent reached the edge of its autonomy.
102
103
  | Field | Type | Req | Meaning |
103
104
  |---|---|:--:|---|
104
105
  | `id` | string | ✓ | Stable id (upsert key). |
105
- | `type` | enum | ✓ | `blocked` \| `needs-decision` \| `confirm-assumption` \| `sign-off`. |
106
+ | `type` | enum | ✓ | `blocked` \| `needs-decision` \| `sign-off` \| `fyi`. `fyi` is a non-blocking heads-up (no decision needed). (`confirm-assumption` still validates but is **deprecated** — use `needs-decision` or `fyi`.) |
106
107
  | `status` | `"open"` \| `"resolved"` | | Default `"open"`. |
107
108
  | `title` | string | ✓ | The ask, in one line. |
108
109
  | `unit` | string | | The `Unit.id` this concerns. |
@@ -174,9 +175,8 @@ Deliberately out of scope for v1, named here so implementers don't repurpose the
174
175
  ```
175
176
 
176
177
  ```jsonc
177
- // .figs/agent.json
178
+ // .figs/agent.json (no `id` here — `figs init` puts it in config.json; the CLI attaches it on push)
178
179
  {
179
- "id": "…uuid… (== config.agentId)",
180
180
  "name": "Reconciliation",
181
181
  "type": "agent",
182
182
  "role": "Reconciliation Officer",
@@ -210,7 +210,7 @@ Deliberately out of scope for v1, named here so implementers don't repurpose the
210
210
 
211
211
  ```jsonc
212
212
  // .figs/asks.jsonl (one object per line)
213
- { "id": "acme-bridge", "ts": "2026-05-28T21:05:00Z", "type": "confirm-assumption", "status": "open", "unit": "acme",
213
+ { "id": "acme-bridge", "ts": "2026-05-28T21:05:00Z", "type": "needs-decision", "status": "open", "unit": "acme",
214
214
  "title": "No bridge rule for prefixed invoice numbers",
215
215
  "found": "~180 rows can't be matched safely; guessing risks false matches.",
216
216
  "need": "Confirm the bridge rule for prefixed invoice numbers.",
package/figs.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  * `figs` — the agent-side CLI (v1, zero-dependency).
4
4
  *
5
5
  * figs status show login / workspace / agent state [--json]
6
- * figs login browser approve (device flow) — agent never sees the token
6
+ * figs login opens browser to approve (device flow) — agent never sees the token
7
7
  * figs login <token> fallback: save a token you pasted (~/.figs/credentials.json)
8
8
  * figs logout remove the locally-saved token (~/.figs/credentials.json)
9
9
  * figs workspaces list the user's workspaces [--json]
@@ -35,8 +35,9 @@ import {
35
35
  writeFileSync,
36
36
  } from "node:fs"
37
37
  import { homedir } from "node:os"
38
- import { join } from "node:path"
38
+ import { basename, join } from "node:path"
39
39
  import { randomUUID } from "node:crypto"
40
+ import { spawn } from "node:child_process"
40
41
 
41
42
  // Single source of truth for the version: package.json (shipped alongside this
42
43
  // file in the published package). One edit keeps `figs version`, the floor
@@ -72,7 +73,7 @@ const COMMANDS = {
72
73
  flags: [],
73
74
  desc: "log in — browser device-flow, or save a pasted token",
74
75
  more: [
75
- "no arg → device flow: a human approves in a browser (you never see the token).",
76
+ "no arg → device flow: opens a browser for a human to approve (you never see the token).",
76
77
  "<token> → save a token you already have to ~/.figs/credentials.json.",
77
78
  ],
78
79
  eg: "figs login",
@@ -85,11 +86,13 @@ const COMMANDS = {
85
86
  eg: "figs workspaces",
86
87
  },
87
88
  init: {
88
- args: "--workspace <slug-or-id> [--endpoint <url>]",
89
+ args: "[--workspace <slug-or-id>] [--endpoint <url>]",
89
90
  flags: ["--workspace", "--endpoint"],
90
- desc: "set up .figs/ here (identity UUID + config + pointer GUIDE.md)",
91
+ desc: "scaffold .figs/ here (identity + charter/contract/guide templates)",
91
92
  more: [
92
93
  "--workspace takes a slug (resolved to its UUID) or a raw UUID — get it from `figs workspaces`.",
94
+ "Omit --workspace and (logged in) it lists your workspaces so you can re-run with the right one.",
95
+ "Never clobbers: an existing agent.json / CONTRACT.md / GUIDE.md / outbox is left exactly as-is.",
93
96
  ],
94
97
  eg: "figs init --workspace acme-corp",
95
98
  },
@@ -212,9 +215,9 @@ function cmpSemver(a, b) {
212
215
  return 0
213
216
  }
214
217
  /**
215
- * Cached (daily) version check — off the hot path. Warns when behind `latest`;
216
- * `hardFail` exits when below the compatible `min`. Network failure is ignored
217
- * (never blocks on a transient outage).
218
+ * Cached (daily) compatibility check — off the hot path. `hardFail` exits when
219
+ * below the server's compatible `min`. Network failure is ignored (never blocks
220
+ * on a transient outage).
218
221
  */
219
222
  async function checkVersion({ force = false, hardFail = false } = {}) {
220
223
  const cachePath = join(globalDir, "version-check.json")
@@ -233,14 +236,11 @@ async function checkVersion({ force = false, hardFail = false } = {}) {
233
236
  }
234
237
  }
235
238
  const min = info?.cli?.min
236
- const latest = info?.cli?.latest
237
239
  // cmpSemver returns null on an unparseable version → skip (never fail closed).
238
240
  if (min && cmpSemver(VERSION, min) === -1) {
239
241
  const msg = `figs CLI ${VERSION} is below the minimum ${min} — upgrade: npx @figs-so/cli@latest`
240
242
  if (hardFail) die(msg)
241
243
  console.warn(`figs: ! ${msg}`)
242
- } else if (latest && cmpSemver(VERSION, latest) === -1) {
243
- console.warn(`figs: a newer CLI is available (${latest}) — npx @figs-so/cli@latest`)
244
244
  }
245
245
  }
246
246
 
@@ -311,9 +311,30 @@ function saveToken(token) {
311
311
  }
312
312
  }
313
313
 
314
+ // Best-effort: pop the approval page in the user's default browser so they only
315
+ // have to click Approve. Silent no-op when it can't (headless / remote / CI) —
316
+ // the link is always printed above as the fallback. Detached + unref'd so it
317
+ // never blocks or ties the polling loop to the browser process.
318
+ function openBrowser(url) {
319
+ try {
320
+ const [cmd, args] =
321
+ process.platform === "darwin"
322
+ ? ["open", [url]]
323
+ : process.platform === "win32"
324
+ ? ["cmd", ["/c", "start", "", url]]
325
+ : ["xdg-open", [url]]
326
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true })
327
+ child.on("error", () => {}) // swallow ENOENT / no opener available
328
+ child.unref()
329
+ } catch {
330
+ /* ignore — the printed link is the fallback */
331
+ }
332
+ }
333
+
314
334
  /**
315
- * `figs login` → device flow (the human approves in a browser; the agent never
316
- * handles the token). `figs login <token>` save a pasted token (fallback).
335
+ * `figs login` → device flow: opens the approval page in the user's browser
336
+ * (the printed link is the fallback); the human clicks Approve and the agent
337
+ * never handles the token. `figs login <token>` → save a pasted token (fallback).
317
338
  */
318
339
  async function login(token) {
319
340
  if (token) {
@@ -325,9 +346,10 @@ async function login(token) {
325
346
  const start = await request("POST", "/api/device/start")
326
347
  if (!start.ok) die(`could not start login (${start.status})`)
327
348
  const d = start.data
328
- console.log("figs: to authorize this CLI, open the link and click Approve:")
329
- console.log(` ${d.verification_uri_complete}`)
349
+ console.log("figs: opening your browser to approve this CLI just click Approve there.")
350
+ console.log(` if it doesn't open, visit: ${d.verification_uri_complete}`)
330
351
  console.log(` (or go to ${d.verification_uri} and enter code: ${d.user_code})`)
352
+ openBrowser(d.verification_uri_complete)
331
353
  console.log("figs: waiting for approval…")
332
354
 
333
355
  const deadline = Date.now() + (d.expires_in ?? 600) * 1000
@@ -515,18 +537,91 @@ are a gitignored outbox. The token is the human's job — never generate one you
515
537
  `
516
538
  }
517
539
 
518
- async function init() {
519
- const workspaceArg = flag("--workspace")
520
- if (!workspaceArg) {
521
- die("usage: figs init --workspace <slug-or-id> [--endpoint <url>]")
540
+ /**
541
+ * A starter `agent.json` — written by `figs init` only when none exists. The
542
+ * `<…>` values are placeholders the agent fills in by reading its own repo;
543
+ * `figs doctor` refuses to bless a charter that still has them. `name` defaults
544
+ * to the folder name (a sensible first guess), `id` is intentionally absent —
545
+ * the CLI attaches the identity UUID from config.json on push.
546
+ */
547
+ function agentJsonStub(name) {
548
+ return (
549
+ JSON.stringify(
550
+ {
551
+ name,
552
+ role: "<one line — what you are>",
553
+ status: "in_dev",
554
+ mandate: "<one sentence — what you are accountable for>",
555
+ org: { department: "<your team / department>" },
556
+ runtime: "<what runs you, e.g. Claude Code>",
557
+ cadence: "<on-demand · weekly · monthly · …>",
558
+ responsibilities: ["<an area of work you own — list a few, or use steps>"],
559
+ properties: [{ k: "<fact>", v: "<value>" }],
560
+ },
561
+ null,
562
+ 2,
563
+ ) + "\n"
564
+ )
565
+ }
566
+
567
+ /** A starter activity contract — written by `figs init` only when none exists. */
568
+ function contractStub(name) {
569
+ return `# Activity contract — ${name} on Figs
570
+
571
+ What this agent surfaces to Figs vs. holds back. **Agree it with your user** — this is the
572
+ deliberate Activity step, not something to do mechanically. See \`.figs/GUIDE.md\` for the why.
573
+
574
+ > **Maintain:** edit when the surfacing agreement changes (a new stream, a sensitivity change).
575
+ > Keep it honest to what you actually push.
576
+
577
+ ## What I surface
578
+
579
+ | Stream | Surface? | Content |
580
+ |--------|----------|---------|
581
+ | **runs** | <yes/no> | one line per run — what I did, de-identified scope, the headline result + status. |
582
+ | **artifacts** | <yes/no> | the report(s) a run produced. |
583
+ | **asks** | when real | genuine blockers / decisions / sign-offs / FYIs for my manager. 0 is a fine number. |
584
+
585
+ ## What I never surface
586
+
587
+ Raw user content — ever. Plus, for this agent: <anything sensitive to its domain>. Use
588
+ **de-identified labels** (\`<scope>-01\`), never customer or system names.
589
+ `
590
+ }
591
+
592
+ /**
593
+ * Find string values still left as `<…>` template placeholders, with their JSON
594
+ * path. Used by `figs doctor` to block publishing a half-filled charter. Matches
595
+ * a value that is *entirely* a placeholder (e.g. "<one line — what you are>") so
596
+ * real content containing stray angle brackets isn't flagged.
597
+ */
598
+ function findPlaceholders(obj) {
599
+ const out = []
600
+ const walk = (v, path) => {
601
+ if (typeof v === "string") {
602
+ if (/^<.*>$/.test(v.trim())) out.push({ path: path || "(root)", value: v })
603
+ } else if (Array.isArray(v)) {
604
+ v.forEach((x, i) => walk(x, `${path}[${i}]`))
605
+ } else if (v && typeof v === "object") {
606
+ for (const [k, x] of Object.entries(v)) walk(x, path ? `${path}.${k}` : k)
607
+ }
522
608
  }
523
- const endpoint = (flag("--endpoint") || resolveEndpoint()).replace(/\/+$/, "")
609
+ walk(obj, "")
610
+ return out
611
+ }
524
612
 
525
- // A UUID is the canonical workspace id (used as-is, no network). A slug is the
526
- // human-friendly handle resolve it to its UUID via the API, and store the
527
- // UUID so a later workspace rename can't break this agent's pushes.
528
- let workspaceId = workspaceArg
529
- if (!isUuid(workspaceArg)) {
613
+ /**
614
+ * Resolve which workspace this `.figs/` belongs to. `--workspace` is optional:
615
+ * - given a UUID → use it as-is (no network).
616
+ * - given a slug → resolve to its UUID via the API (needs auth).
617
+ * - omitted → reuse the one already in config.json (idempotent re-init),
618
+ * else list the user's workspaces and have them re-run with one
619
+ * (the agent drives the choice — we never silently pick).
620
+ * Returns the workspace UUID, or exits with an actionable message.
621
+ */
622
+ async function resolveWorkspaceId(workspaceArg, endpoint) {
623
+ if (workspaceArg) {
624
+ if (isUuid(workspaceArg)) return workspaceArg
530
625
  if (!getToken()) {
531
626
  die("not logged in — run `figs login` first (resolving a workspace slug needs auth; or pass the workspace UUID)")
532
627
  }
@@ -536,12 +631,36 @@ async function init() {
536
631
  }
537
632
  const list = r.data.workspaces ?? []
538
633
  const match = list.find((w) => w.slug === workspaceArg || w.id === workspaceArg)
539
- if (!match) {
540
- die(`no workspace matching "${workspaceArg}" — run \`figs workspaces\` to see yours`)
541
- }
542
- workspaceId = match.id
634
+ if (!match) die(`no workspace matching "${workspaceArg}" — run \`figs workspaces\` to see yours`)
635
+ return match.id
636
+ }
637
+
638
+ // No --workspace: a re-init keeps the workspace already on file.
639
+ const existing = readJson(join(repoDir, "config.json"), null)
640
+ if (existing?.workspaceId) return existing.workspaceId
641
+
642
+ // First-time init with no workspace named — list them so the agent can re-run.
643
+ if (!getToken()) {
644
+ die("which workspace? run `figs login` first so I can list them, then `figs init --workspace <slug>` (or pass a workspace UUID directly)")
645
+ }
646
+ const r = await request("GET", "/api/workspaces", null, getToken())
647
+ if (!r.ok) die(`could not list workspaces (${r.status}): ${r.data.error ?? r.data.raw ?? ""}`)
648
+ const list = r.data.workspaces ?? []
649
+ if (list.length === 0) {
650
+ die(`no workspaces yet — create one at ${endpoint}, then re-run \`figs init --workspace <slug>\``)
543
651
  }
652
+ console.log("figs: which workspace? re-run init with one of these:")
653
+ for (const w of list) console.log(` figs init --workspace ${w.slug} (${w.name})`)
654
+ process.exit(1)
655
+ }
544
656
 
657
+ async function init() {
658
+ const endpoint = (flag("--endpoint") || resolveEndpoint()).replace(/\/+$/, "")
659
+ const workspaceId = await resolveWorkspaceId(flag("--workspace"), endpoint)
660
+
661
+ // config.json (identity + destination) is always (re)written — it's the one
662
+ // file the CLI owns. A re-init reuses the existing identity UUID so every
663
+ // runner of this repo pushes to the same agent.
545
664
  const existing = readJson(join(repoDir, "config.json"), null)
546
665
  const agentId = existing?.agentId || randomUUID()
547
666
  mkdirSync(repoDir, { recursive: true })
@@ -549,48 +668,87 @@ async function init() {
549
668
  join(repoDir, "config.json"),
550
669
  JSON.stringify({ endpoint, workspaceId, agentId }, null, 2) + "\n",
551
670
  )
552
- // Commit config.json + agent.json (identity + charter); the activity files
553
- // are a transient outboxemitted per run, aggregated remotely.
554
- const giPath = join(repoDir, ".gitignore")
555
- if (!existsSync(giPath)) {
556
- writeFileSync(
557
- giPath,
558
- [
559
- "# Figs commit config.json + agent.json + CONTRACT.md + GUIDE.md.",
560
- "# Activity is a transient outbox: emitted per run, aggregated remotely.",
561
- "runs.jsonl",
562
- "asks.jsonl",
563
- "artifacts/",
564
- "credentials.json",
565
- "",
566
- ].join("\n"),
567
- )
671
+
672
+ // Everything else is scaffolded create-if-missing `figs init` gives a fresh
673
+ // repo a complete, ready-to-fill `.figs/`, and never clobbers an agent's
674
+ // authored charter/contract/guide or its activity outbox.
675
+ const created = []
676
+ const ensure = (rel, contents) => {
677
+ const p = join(repoDir, rel)
678
+ if (existsSync(p)) return false
679
+ writeFileSync(p, contents)
680
+ created.push(rel)
681
+ return true
568
682
  }
569
- writeFileSync(join(repoDir, "GUIDE.md"), guideStub(endpoint))
683
+ ensure(
684
+ ".gitignore",
685
+ [
686
+ "# Figs — commit config.json + agent.json + CONTRACT.md + GUIDE.md.",
687
+ "# Activity is a transient outbox: emitted per run, aggregated remotely.",
688
+ "runs.jsonl",
689
+ "asks.jsonl",
690
+ "artifacts/",
691
+ "credentials.json",
692
+ "",
693
+ ].join("\n"),
694
+ )
695
+ ensure("GUIDE.md", guideStub(endpoint))
696
+ const name = basename(process.cwd())
697
+ const charterCreated = ensure("agent.json", agentJsonStub(name))
698
+ ensure("CONTRACT.md", contractStub(name))
699
+ ensure("runs.jsonl", "")
700
+ ensure("asks.jsonl", "")
701
+ mkdirSync(join(repoDir, "artifacts"), { recursive: true })
570
702
 
571
703
  console.log(
572
- `figs: ✓ .figs/config.json + .gitignore + GUIDE.md written (agentId ${agentId})`,
704
+ `figs: ✓ .figs/ ready config.json written (agentId ${agentId}, workspace ${workspaceId})`,
573
705
  )
706
+ if (created.length) console.log(` scaffolded: ${created.join(", ")}`)
707
+ if (charterCreated) {
708
+ console.log(
709
+ " Phase 1: fill in .figs/agent.json — it's a template; replace the <…> placeholders",
710
+ )
711
+ console.log(
712
+ " (`figs doctor` flags any you miss), then `figs doctor` && `figs push` to appear.",
713
+ )
714
+ } else {
715
+ console.log(
716
+ " Your charter (.figs/agent.json) is already here — `figs doctor` && `figs push` to publish.",
717
+ )
718
+ }
574
719
  console.log(
575
- ` Phase 1: author .figs/agent.json (your charter), then \`figs doctor\` && \`figs push\` to appear.`,
720
+ " Anchor Figs in the file you load every session (CLAUDE.md/AGENTS.md/…): paste the",
576
721
  )
722
+ console.log(` figs:begin block from ${endpoint}/llms.txt, or future sessions forget Figs.`)
577
723
  console.log(
578
- ` Then anchor Figs in the instruction file you load every session (CLAUDE.md/AGENTS.md/…) see /llms.txt; without it, future sessions forget Figs.`,
724
+ " Commit config.json + agent.json + CONTRACT.md + GUIDE.md; never commit credentials.json.",
579
725
  )
580
726
  console.log(` Full guide: ${endpoint}/llms.txt`)
581
727
  }
582
728
 
583
729
  /** Validate the local .figs/ payload against the contract — no write. */
584
730
  async function doctor() {
585
- if (!getToken()) die("not logged inrun `figs login`")
586
- if (!existsSync(repoDir)) die("no .figs/ here — run `figs init --workspace <id>` first")
731
+ // Local checks first (no token/network needed)fail fast and offline.
732
+ if (!existsSync(repoDir)) die("no .figs/ here — run `figs init` first")
587
733
  const config = readJson(join(repoDir, "config.json"), {})
588
734
  if (!config.workspaceId || !config.agentId) {
589
- die("config missing workspaceId/agentId — run `figs init --workspace <id>`")
735
+ die("config missing workspaceId/agentId — run `figs init`")
590
736
  }
591
737
  const agentJson = readJson(join(repoDir, "agent.json"), null)
592
738
  if (!agentJson) die("missing .figs/agent.json — author it first (see .figs/GUIDE.md)")
593
739
 
740
+ // Refuse to bless a charter that still has `<…>` template placeholders — `figs
741
+ // init` scaffolds them, and pushing them would publish "<one line — what you
742
+ // are>" to the org chart. This is the "not ready to push" signal.
743
+ const placeholders = findPlaceholders(agentJson)
744
+ if (placeholders.length) {
745
+ console.log("figs: ✗ .figs/agent.json still has template placeholders — fill these in before pushing:")
746
+ for (const p of placeholders) console.log(` ${p.path}: ${p.value}`)
747
+ console.log(" (replace the <…> values by reading your own repo, then re-run `figs doctor`)")
748
+ process.exit(1)
749
+ }
750
+
751
+ if (!getToken()) die("not logged in — run `figs login`")
594
752
  const r = await api("POST", "/api/validate", {
595
753
  workspaceId: config.workspaceId,
596
754
  agent: { ...agentJson, id: config.agentId },
@@ -621,11 +779,11 @@ async function push() {
621
779
  if (!token) die("not logged in — run `figs login` (or set FIGS_TOKEN)")
622
780
  await checkVersion({ hardFail: true })
623
781
  if (!existsSync(repoDir)) {
624
- die("no .figs/ here — run `figs init --workspace <id>` first")
782
+ die("no .figs/ here — run `figs init` first")
625
783
  }
626
784
  const config = readJson(join(repoDir, "config.json"), {})
627
785
  if (!config.workspaceId || !config.agentId) {
628
- die("config missing workspaceId/agentId — run `figs init --workspace <id>`")
786
+ die("config missing workspaceId/agentId — run `figs init`")
629
787
  }
630
788
  const endpoint =
631
789
  process.env.FIGS_ENDPOINT || config.endpoint || DEFAULT_ENDPOINT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@figs-so/cli",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Figs CLI — publish your AI agent's state to Figs (figs.so). Run by the agent.",
5
5
  "type": "module",
6
6
  "bin": {