@figs-so/cli 1.5.0 → 1.6.1

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/CHANGELOG.md CHANGED
@@ -9,6 +9,43 @@ as **one versioned thing.** When a newer CLI runs, `figs status` / `doctor` / `i
9
9
  baked stance is behind and point here; read from your baseline forward, refresh the file you load each
10
10
  session, then `figs init --yes` to re-stamp. Newest first.
11
11
 
12
+ ## 1.6.1 — departments also converge at push
13
+
14
+ *2026-06-15*
15
+
16
+ The push-time twin of 1.6.0's link nudge: when a push coins a department no teammate uses (and the
17
+ workspace already has others), `figs push` lists the existing ones so you can adopt one — or keep yours
18
+ if it's genuinely separate. Fires once, on the change *into* a new department; never on a steady re-push.
19
+
20
+ ## 1.6.0 — coherent asks + a coherent org chart
21
+
22
+ *2026-06-14*
23
+
24
+ Two coherence fixes: the ask⇄answer contract is enforced, and departments converge at link.
25
+
26
+ **The ask⇄answer contract, enforced — not just advised.** Options belong to questions, verdicts to
27
+ sign-offs — they can't overlap, so a "double verdict" (cite one path, rule another) is impossible.
28
+
29
+ - **Sign-offs take no options.** `figs ask sign-off --option` is **refused** — a sign-off's answer is
30
+ the verdict (approve / request-changes / reject); an option there only restates one. What approval
31
+ sets in motion stays `--on-approve`; any caveat rides in the reply note.
32
+ - **A verdict cites no option.** `figs answer --approve` / `--request-changes` / `--reject` no longer
33
+ accepts `--chosen` (that pairing *was* the contradiction). `--chosen` is question-only.
34
+ - **Guide + spec say so.** `GUIDE.md` and `SPEC.md` now mark `options` question-only and `chosen`
35
+ answer-only. The `.figs` schema is unchanged and stays **`figs-spec v2`** — a tightening of what the
36
+ verbs mint, not a wire change; existing records still validate and render.
37
+
38
+ **Departments converge at `figs link`.** `department` is free-text you author locally, before any
39
+ workspace exists — so independently-built agents drift ("Member Retention" vs "Member Success" vs
40
+ "Member Ops") and the org chart fragments into columns of one.
41
+
42
+ - **Link surfaces the workspace's departments** and compares them to your `agent.json#org.department`:
43
+ confirms a match, flags yours as new (with the existing list to pick from), or prompts when it's unset.
44
+ - **Print-only — you reconcile your own charter.** Link never writes `agent.json`; you adopt an existing
45
+ department and push. No app-side merge/rename, no new flag — just a nudge at the one connected moment.
46
+ - **Spec unchanged**, still **`figs-spec v2`** — `department` was already free-text; this is link-time
47
+ guidance, not a wire change.
48
+
12
49
  ## 1.5.0 — Figs as your operating system
13
50
 
14
51
  *2026-06-14*
package/GUIDE.md CHANGED
@@ -197,7 +197,9 @@ description of who you are.
197
197
  agent never logs in**; auth is the human's job. Already set up on this machine? `figs status` says so
198
198
  — **skip straight to `figs link`** (don't re-run `login`). The flow: `figs login` → `figs link`
199
199
  (connect to a workspace; bare lists them, or `--workspace <slug>`) → `figs push`. Nothing recorded
200
- before linking is lost — push sends it all.
200
+ before linking is lost — push sends it all. **`figs link` prints the workspace's existing
201
+ departments** — reconcile `agent.json#org.department` to one before you push, so you land in the
202
+ right org-chart column instead of a column of one.
201
203
 
202
204
  > **After your first `figs push`, stop.** This is the moment your user sees you appear. Give them the
203
205
  > link — **`<endpoint>/w/<workspaceId>`** (in `config.json`; `figs push` prints it) — ask them to look,
@@ -217,7 +219,7 @@ CLI attaches it on push.
217
219
  | `status` | | Free text — **your lifecycle**: `"in_dev"` while being built, `"active"` once operating (flip it when you start real work). Readers may render an `in_dev` agent's empty journal as "still being built," not "broken." |
218
220
  | `mandate` | | **Your charter** — one sentence: what you're accountable for. Shown loudest. |
219
221
  | `avatar` | | `{ "seed": "<string>" }` — seeds your avatar. |
220
- | `org` | | `{ "department": "..." }` — **`department` groups you on the org chart.** |
222
+ | `org` | | `{ "department": "..." }` — **`department` groups you on the org chart.** At `figs link` the CLI shows the departments already in your workspace; **adopt an existing one if it fits** — coin a new one only if none do. Coherent grouping is the whole point of the chart. (`figs push` repeats the nudge if you introduce a department no teammate uses — the link-time list can be stale.) |
221
223
  | `runtime` | | e.g. `"Claude Code"`. |
222
224
  | `cadence` | | e.g. `"Monthly"`. |
223
225
  | `steps` | | `string[]` — your **fixed, ordered procedure**, numbered. Only if your work has one. |
@@ -402,14 +404,20 @@ figs ask question --title 'No bridge rule for prefixed invoice numbers' \
402
404
  - **`to`**: `"manager"` (accountable for your *work*) · `"builder"` (maintains *you* — broken, creds,
403
405
  self-edit flags). Omit if genuinely either.
404
406
  - `found` / `need` — **the case**: what you saw, what you need back. Write these so a *stranger* (the
405
- human deciding, and the future session acting) can act from the ask **alone**. `options[]` — **short,
406
- stable, quotable** candidate answers (a reply cites one *verbatim*) and **only when there are
407
- discrete paths to choose**: a clear standalone question (`how much did we spend in May?`) needs none.
408
- Options are *candidates, not a cage* — **your human may also reply in free text**, so `found`/`need`
409
- must stand on their own. On a **sign-off**, options are answer paths (`"Approvedfile the 15 ready
410
- charges"`), and **`--on-approve '<step>'`** (repeatable, ordered) states what approval sets in motion
411
- an approval authorizes exactly those steps; flag anything irreversible. `--attach` the **exact
412
- content to approve** (a verdict blesses what the ask carries).
407
+ human deciding, and the future session acting) can act from the ask **alone**.
408
+ - **Options are question-only and so is the answer⇄verdict split, so get the type right.**
409
+ - On a **question**, `options[]` are **short, stable, quotable** candidate answers (a reply cites one
410
+ *verbatim*)and **only when there are discrete paths to choose**: a clear standalone question
411
+ (`how much did we spend in May?`) needs none. Options are *candidates, not a cage* **your human may
412
+ also reply in free text**, so `found`/`need` must stand on their own.
413
+ - On a **sign-off**, the answer is the **verdict** (approve / request-changes / reject) — that *is*
414
+ the choice, so a sign-off takes **no options.** An option on a sign-off just restates a verdict
415
+ (`"Hold"`, `"Reject"`, `"Approve as written"`) — the human already has those buttons, and citing
416
+ one path while ruling another is a contradiction. `figs ask sign-off --option` is **refused.**
417
+ Instead, **`--on-approve '<step>'`** (repeatable, ordered) states what approval sets in motion — an
418
+ approval authorizes exactly those steps; flag anything irreversible. Any caveat ("approve, but only
419
+ the 15") rides in the reply note, not an option. `--attach` the **exact content to approve** (a
420
+ verdict blesses what the ask carries).
413
421
  - For long texts, `--stdin` a JSON object. The line it writes:
414
422
 
415
423
  ```json
@@ -441,9 +449,10 @@ Figs app. Either way you bring the reply into the record and act on it:
441
449
  figs answer acme-bridge --chosen 'Strip the alpha prefix' --by 'Sarah (accounting)'
442
450
  ```
443
451
 
444
- `--by` names the **human** who said it (not you). `--chosen` is checked verbatim against the ask's
445
- options. On a sign-off use `--approve` / `--request-changes` / `--reject` (a qualified verdict may
446
- also carry `--chosen`). **Transcribe their words never author the reply yourself.**
452
+ `--by` names the **human** who said it (not you). On a **question**, `--chosen` is checked verbatim
453
+ against the ask's options (or `--text` for free text). On a **sign-off** use `--approve` /
454
+ `--request-changes` / `--reject` the verdict *is* the answer; it **takes no `--chosen`** (any caveat
455
+ goes in `--text`). **Transcribe their words — never author the reply yourself.**
447
456
 
448
457
  - **Then act, then close.** `figs close` is a **pure close** — it reads the newest reply on file and
449
458
  derives the outcome, citing it:
package/SPEC.md CHANGED
@@ -82,7 +82,7 @@ and the CLI attaches it on push. Everything else is optional and rendered when p
82
82
  | `avatar` | `{ seed: string }` | | Seed for the generated avatar. |
83
83
  | `role` | string | | Short title, e.g. "Reconciliation Officer". |
84
84
  | `status` | string | | Free-text lifecycle, e.g. `in_dev`, `active`. |
85
- | `org` | `{ department?: string }` | | `department` groups the agent into an org-chart column. |
85
+ | `org` | `{ department?: string }` | | `department` groups the agent into an org-chart column. Free-text; `figs link` surfaces the workspace's existing departments so independently-authored agents converge on one rather than each coining its own, and `figs push` repeats the nudge when a push introduces a department no peer uses (the link-time list can be stale). |
86
86
  | `runtime` | string | | What runs it, e.g. "Claude Code". |
87
87
  | `cadence` | string | | How often it runs, e.g. "Monthly". |
88
88
  | `mandate` | string | | One-paragraph statement of what it's responsible for. |
@@ -172,7 +172,7 @@ edge of its autonomy.
172
172
  | `run` | string | | The run `id` this ask was raised during. **Optional** — asks also arise outside runs. |
173
173
  | `found` | string | | What the agent found / why it's stuck. |
174
174
  | `need` | string | | What it needs from the human. |
175
- | `options` | string[] | | Candidate answers — **short, stable, quotable** strings: a reply cites one *verbatim* (§6.2). On a **sign-off** they are qualified-verdict paths (e.g. `"Approved file the 15 ready charges"`). |
175
+ | `options` | string[] | | **Question-only.** Candidate answers — **short, stable, quotable** strings: a reply cites one *verbatim* (§6.2). **Invalid on a `sign-off`** a sign-off's answer is the verdict (approve / request-changes / reject), so an option there only restates a verdict; `figs ask sign-off --option` is refused. What approval triggers is `onApprove`. |
176
176
  | `onApprove` | string[] | | **Sign-off only.** The ordered steps approval sets in motion — **an approval authorizes exactly these steps, in order**; flag anything irreversible in the step. The agent's declared intent, not a bound plan. Invalid on a `question`. |
177
177
  | `details` | `{ l, v }[]` | | Labelled facts (e.g. amount at risk). |
178
178
  | `attachments` | string[] | | File names under `artifacts/` attached to this ask (the exact content to review — §7). |
@@ -203,7 +203,7 @@ The close is derived from the newest reply on the ask and cites it.
203
203
  | Field | Type | Meaning |
204
204
  |---|---|---|
205
205
  | `note` | string | The agent's one-line account of the close. |
206
- | `chosen` | string | The decision taken — **verbatim** one of the ask's `options[]` (copied from the cited reply). |
206
+ | `chosen` | string | The option taken — **verbatim** one of the ask's `options[]`, copied from the cited reply. **Question closes only** (a verdict carries no `chosen`). |
207
207
  | `run` | string | The job the reply set in motion (mirror of `ask.run`) — so a reader can navigate answer → work → outcome. |
208
208
  | `via` | `"figs"` \| `"human"` \| `"self"` | How it closed: `figs` = derived from a reply on file, citing it (`answer`) · `human` = an out-of-band reply with no event cited · `self` = the blocker cleared on its own. |
209
209
  | `answer` | string | The `messages.jsonl` event id the close acted on — written by `figs close` (attribution by mechanism, never typed). **Trust derives from that event's mint origin** (§6.3), not from this field. |
@@ -225,9 +225,9 @@ immutable, ids minted once, they **accumulate** (no fold) — an ask can carry a
225
225
  | `by` | string | ✓ | Who said it (the human). |
226
226
  | `ts` | string (ISO-8601 w/ offset) | ✓ | Machine-stamped (server clock for reader-minted, CLI clock for transcribed). |
227
227
  | `source` | `"app"` \| `"chat"` \| … | | **Where the reply arrived** (display metadata, *not* trust) — `app` = in the reader, `chat` = transcribed by the agent. Extensible (`slack`, `email`, …). |
228
- | `chosen` | string | | The option cited, **verbatim** from the ask's `options[]`. |
229
- | `text` | string | | Free-text reply. |
230
- | `verdict` | `"approved"` \| `"changes-requested"` \| `"rejected"` | | On a `verdict` message. |
228
+ | `chosen` | string | | **Answer-only.** The option cited, **verbatim** from the ask's `options[]`. A `verdict` message carries no `chosen` (options are question-only; the verdict is the whole answer). |
229
+ | `text` | string | | Free-text reply (an answer, or a caveat riding with a verdict). |
230
+ | `verdict` | `"approved"` \| `"changes-requested"` \| `"rejected"` | | On a `verdict` message (a sign-off ruling). `rejected` is also the human-side close of any open ask. |
231
231
 
232
232
  **The trust rule (normative):** a reader derives the *verified* grade from **mint origin** — a message
233
233
  the reader minted itself is attested; a message that arrived via push (transcribed by an agent) is
@@ -281,6 +281,12 @@ minimum CLI version requires Bearer.)
281
281
  immutable (§7): same name + different bytes is refused `409`. `figs push --reupload` forces a full
282
282
  re-send (recovery if a stored object ever drifts from its row).
283
283
 
284
+ The `/api/ingest` response may also carry **`newDepartment`** (`{ "peers": ["<dept>", …] }`) —
285
+ present only when this push introduced a department **no other live agent uses** while the workspace
286
+ already has others. The CLI prints a one-time hint to adopt one of `peers` (or keep the new name). It
287
+ rides only on the change *into* such a department, so a steady re-push omits it; absent otherwise, and
288
+ older CLIs ignore it.
289
+
284
290
  The server upserts the agent by `id` and runs/asks by `id`; it dedupes messages by event `id`; it
285
291
  **rebuilds each run's timeline from `runEvents`, one entry per fold, deduped by `eventId`** (a run with no
286
292
  `runEvents` — an older CLI, or legacy/hand-authored lines without an `eventId` — keeps one timeline entry
package/figs.mjs CHANGED
@@ -130,6 +130,8 @@ const COMMANDS = {
130
130
  "needed; get it from the app). --endpoint overrides the destination (default app.figs.so).",
131
131
  "Verifies the workspace when you're logged in; a UUID set while logged out is accepted",
132
132
  "but unverified until your first `figs push`. Writes endpoint + workspaceId into config.json.",
133
+ "Shows the departments already in the workspace — adopt an existing one in agent.json#org.department",
134
+ "if it fits (coherent org-chart grouping is the point); only coin a new one if none do.",
133
135
  "To unlink, delete those two fields from .figs/config.json (your identity + work stay).",
134
136
  ],
135
137
  eg: "figs link --workspace acme-corp",
@@ -198,16 +200,16 @@ const COMMANDS = {
198
200
  "sign-off (give me a verdict). Two types — the type IS the contract.",
199
201
  "Two strangers read every ask — a human deciding, a future session acting;",
200
202
  "the record must carry everything both need: --found (what you saw), --need",
201
- "(what you need), --option (repeatable; short, stable, quotable — answers cite",
202
- "one verbatim; the option is the label, context goes in --found/--detail),",
203
- "--detail 'Label=Value' (repeatable), --attach <file> (repeatable; a verdict",
204
- "blesses what the ask carries attach the exact content for review + a brief:",
205
- "what to do once approved and what it requires).",
206
- "On a sign-off, --option entries are answer paths the human's verdict can",
207
- "cite one verbatim ('Approved file the 15 ready charges') and",
208
- "--on-approve '<step>' (repeatable, ordered; sign-off only) states what",
209
- "approval sets in motion: an approval authorizes exactly the steps you stated.",
210
- "Flag anything irreversible in the step itself.",
203
+ "(what you need), --detail 'Label=Value' (repeatable), --attach <file>",
204
+ "(repeatable; a verdict blesses what the ask carries attach the exact content",
205
+ "for review + a brief: what to do once approved and what it requires).",
206
+ "QUESTION --option (repeatable; short, stable, quotable a reply cites one",
207
+ "verbatim; the option is the label, context goes in --found/--detail). Options are",
208
+ "candidates, not a cage your human may also free-text. Options are QUESTION-ONLY.",
209
+ "SIGN-OFF the answer is the VERDICT (approve / request-changes / reject); it takes",
210
+ "NO --option (an option there just restates a verdict). --on-approve '<step>'",
211
+ "(repeatable, ordered; sign-off only) states what approval sets in motion: an",
212
+ "approval authorizes exactly those steps. Flag anything irreversible in the step.",
211
213
  "--run <run-id> links the run this came out of — explicit id only (other",
212
214
  "sessions may report concurrently; `figs report` prints the id it wrote).",
213
215
  "--stdin reads a full JSON object instead of flags (long texts; attachments still via --attach).",
@@ -227,8 +229,8 @@ const COMMANDS = {
227
229
  "type commands; you transcribe what they said. --by names the HUMAN who said it, not you.",
228
230
  "question → --chosen '<option verbatim>' (checked against the ask's options) or",
229
231
  "--text '<what they said>'. sign-off → --approve | --request-changes | --reject",
230
- "(a qualified verdict may also carry --chosen). Transcribe verbatim — never summarize,",
231
- "never author the reply yourself.",
232
+ "(a verdict cites no option — put any caveat in --text). Transcribe verbatim —",
233
+ "never summarize, never author the reply yourself.",
232
234
  "Just don't RE-transcribe a reply that already came through the app: it's attested and",
233
235
  "syncs down on its own (figs inbox), and a re-typed copy would downgrade it — so answer",
234
236
  "REFUSES if the ask already has an app reply (--force only for a separate out-of-band one).",
@@ -1169,14 +1171,19 @@ async function link() {
1169
1171
  const token = getToken()
1170
1172
 
1171
1173
  let workspaceId
1174
+ // The resolved workspace row (carries `departments` for the convergence
1175
+ // nudge below). Absent only on the logged-out-UUID path — we can't fetch it.
1176
+ let matched
1172
1177
  if (wsArg && isUuid(wsArg)) {
1173
1178
  workspaceId = wsArg
1174
1179
  if (token) {
1175
1180
  // Verify access when we can; a network blip just defers it to first push.
1176
1181
  const r = await request("GET", "/api/workspaces", null, token)
1177
- if (r.ok && !(r.data.workspaces ?? []).some((w) => w.id === wsArg)) {
1182
+ const got = (r.data.workspaces ?? []).find((w) => w.id === wsArg)
1183
+ if (r.ok && !got) {
1178
1184
  die(`workspace ${wsArg} isn't one you can access — run \`figs link\` (no flag) to list yours`)
1179
1185
  }
1186
+ matched = got
1180
1187
  } else {
1181
1188
  console.warn("figs: ! linked by UUID while logged out — unverified until your first `figs push`")
1182
1189
  }
@@ -1190,6 +1197,7 @@ async function link() {
1190
1197
  const match = (r.data.workspaces ?? []).find((w) => w.slug === wsArg || w.id === wsArg)
1191
1198
  if (!match) die(`no workspace matching "${wsArg}" — run \`figs link\` (no flag) to list yours`)
1192
1199
  workspaceId = match.id
1200
+ matched = match
1193
1201
  } else {
1194
1202
  // Bare `figs link` — list; with exactly one, link it outright.
1195
1203
  if (!token) {
@@ -1201,6 +1209,7 @@ async function link() {
1201
1209
  if (list.length === 0) die(`no workspaces yet — create one at ${endpoint}, then re-run \`figs link\``)
1202
1210
  if (list.length === 1) {
1203
1211
  workspaceId = list[0].id
1212
+ matched = list[0]
1204
1213
  console.log(`figs: linking to ${list[0].slug} (${list[0].name})`)
1205
1214
  } else {
1206
1215
  console.log("figs: which workspace? re-run with one:")
@@ -1216,9 +1225,71 @@ async function link() {
1216
1225
  JSON.stringify({ agentId: config.agentId, endpoint, workspaceId }, null, 2) + "\n",
1217
1226
  )
1218
1227
  console.log(`figs: ✓ linked — workspace ${workspaceId} @ ${endpoint}`)
1228
+ printDepartmentGuidance(matched)
1219
1229
  console.log(" next: `figs push` publishes everything recorded so far")
1220
1230
  }
1221
1231
 
1232
+ /**
1233
+ * Steer department convergence at the one connected moment. `department` is
1234
+ * free-text the agent authors locally (account-free, before any workspace
1235
+ * exists) — so independently-authored agents drift ("Member Retention" vs
1236
+ * "Member Success" vs "Member Ops") and the org chart fragments. Link is where
1237
+ * we can finally compare the local charter against what the workspace already
1238
+ * uses and nudge the agent to adopt an existing department. Print-only: the
1239
+ * agent reconciles its own `agent.json` (link never writes the charter). Skips
1240
+ * silently when we couldn't fetch the workspace (logged-out UUID) or an older
1241
+ * app didn't return `departments`.
1242
+ */
1243
+ function printDepartmentGuidance(workspace) {
1244
+ if (!workspace || !Array.isArray(workspace.departments)) return
1245
+ const existing = workspace.departments.filter(Boolean)
1246
+ const raw = readJson(join(repoDir, "agent.json"), {})?.org?.department
1247
+ const mine = typeof raw === "string" ? raw.trim() : ""
1248
+ // A `<…>` stub or empty value isn't a real choice yet.
1249
+ const unset = !mine || mine.includes("<")
1250
+ const list = existing.join(" · ")
1251
+
1252
+ if (existing.length === 0) {
1253
+ console.log(
1254
+ unset
1255
+ ? " no departments here yet — you're the first; set agent.json#org.department to a broad team name"
1256
+ : ` first department here — you're starting "${mine}"`,
1257
+ )
1258
+ return
1259
+ }
1260
+ const hit = existing.find((d) => d.toLowerCase() === mine.toLowerCase())
1261
+ if (hit) {
1262
+ console.log(` ✓ department "${hit}" — grouping with the rest of the workspace`)
1263
+ } else if (!unset) {
1264
+ console.log(` ! department "${mine}" is new here. Existing: ${list}`)
1265
+ console.log(" → if one fits, set agent.json#org.department to match — coherent grouping is the point")
1266
+ } else {
1267
+ console.log(` departments here: ${list}`)
1268
+ console.log(" → set agent.json#org.department to one (or a new one if none fit)")
1269
+ }
1270
+ }
1271
+
1272
+ /**
1273
+ * The push-time twin of printDepartmentGuidance. `figs link` already lists the
1274
+ * workspace's departments, but at link the list is stale — a peer that linked
1275
+ * moments earlier hasn't pushed yet, so two co-starting agents each think
1276
+ * they're first and coin synonyms ("Growth" vs "Marketing"); the org chart only
1277
+ * groups on an EXACT match, so the board fragments. Push happens later, when the
1278
+ * list is fuller, so the server flags the one push that introduces a department
1279
+ * no peer uses and we nudge here. Print-only (link never writes the charter
1280
+ * either); the server flags only the transition in, so this fires once.
1281
+ */
1282
+ function printNewDepartmentNudge(dept, peers) {
1283
+ const name = typeof dept === "string" ? dept.trim() : ""
1284
+ console.log(` ! new department "${name}" — others here: ${peers.join(" · ")}`)
1285
+ console.log(
1286
+ " if your team is one of these, set agent.json#org.department to that name and `figs push` again,",
1287
+ )
1288
+ console.log(
1289
+ ` so the org chart groups you together — or keep "${name}" if it's genuinely separate (won't show again)`,
1290
+ )
1291
+ }
1292
+
1222
1293
  async function init() {
1223
1294
  // Purely local — `init` never touches the network, so it can never need an
1224
1295
  // account. The one gate is consent, not connectivity: Figs turns a repo into a
@@ -1708,6 +1779,14 @@ async function answerCmd() {
1708
1779
 
1709
1780
  const chosen = flag("--chosen")
1710
1781
  const text = flag("--text")
1782
+ // A verdict cites no option — options are question-only. On a sign-off the verdict
1783
+ // (approve / request-changes / reject) is the whole answer; pairing it with --chosen
1784
+ // is the "double verdict" (cite one path, rule another). Refuse it at the mint point.
1785
+ if (verdicts.length && chosen) {
1786
+ die(
1787
+ "a verdict cites no option — options are for questions. On a sign-off the verdict (approve / request-changes / reject) is the whole answer; put any caveat in --text '<note>'",
1788
+ )
1789
+ }
1711
1790
  const msg = {
1712
1791
  id: genId("msg"),
1713
1792
  kind: verdicts.length ? "verdict" : "answer",
@@ -2062,6 +2141,17 @@ async function askCmd() {
2062
2141
  if (!ask.to) ask.to = "manager"
2063
2142
  const options = flagAll("--option")
2064
2143
  if (options.length) ask.options = options
2144
+ // Options are question-only: they're candidate answers a reply cites. A sign-off's
2145
+ // answer is the VERDICT (approve / request-changes / reject) — never an option. An
2146
+ // option on a sign-off restates a verdict ("Hold", "Reject") and lets a reader cite
2147
+ // one verdict while clicking another. Refuse it at the authoring mint point. (Catches
2148
+ // --stdin options too — ask.options is the merged value.) What approval triggers goes
2149
+ // in --on-approve; any caveat rides in the reply note.
2150
+ if (ask.options?.length && ask.type === "sign-off") {
2151
+ die(
2152
+ "--option is sign-off-illegal: options are for questions (a reply cites one). A sign-off's answer is the verdict (approve / request-changes / reject) — put what approval sets in motion in --on-approve, and any caveat in the reply note",
2153
+ )
2154
+ }
2065
2155
  for (const o of ask.options ?? []) {
2066
2156
  if (o.length > 80) {
2067
2157
  console.warn(
@@ -2325,14 +2415,26 @@ async function doPush() {
2325
2415
  // The wow-moment link — relay this to your human so they can see the agent.
2326
2416
  console.log(` view at ${base}/w/${config.workspaceId}`)
2327
2417
 
2328
- // The ingest response carries the agent's artifact manifest ({ name: "sha256:…" }) —
2329
- // pushArtifacts re-hashes locally and skips re-sending bytes the server already
2330
- // has. Absent (older server / non-JSON body) → it uploads everything, as before.
2331
- let artifactManifest
2418
+ // Parse the success body once: it carries the artifact manifest AND any
2419
+ // department-convergence nudge. Non-JSON (older server) both stay undefined.
2420
+ let body
2332
2421
  try {
2333
- artifactManifest = JSON.parse(text)?.artifactManifest
2422
+ body = JSON.parse(text)
2334
2423
  } catch {
2335
- // non-JSON success body — fall back to upload-all
2424
+ // non-JSON success body — fall back to upload-all, no nudge
2425
+ }
2426
+ // The artifact manifest ({ name: "sha256:…" }) lets pushArtifacts re-hash
2427
+ // locally and skip re-sending bytes the server already has. Absent → upload all.
2428
+ const artifactManifest = body?.artifactManifest
2429
+
2430
+ // Department convergence, fired at push (the twin of `figs link`'s guidance):
2431
+ // the server flags the one push that introduced a department no peer agent
2432
+ // uses — at push because the workspace list is fuller than at link time, when
2433
+ // a co-starting peer hadn't published its department yet. Print-only; the
2434
+ // agent reconciles its own agent.json. One-shot (only the transition fires).
2435
+ const peers = body?.newDepartment?.peers
2436
+ if (Array.isArray(peers) && peers.length) {
2437
+ printNewDepartmentNudge(agent.org?.department, peers)
2336
2438
  }
2337
2439
 
2338
2440
  // The spine landed; an artifact-stage failure is transient/oversize — retry.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@figs-so/cli",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
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": {