@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 +37 -0
- package/GUIDE.md +22 -13
- package/SPEC.md +12 -6
- package/figs.mjs +121 -19
- package/package.json +1 -1
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**.
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
|
445
|
-
options. On a sign-off use `--approve` /
|
|
446
|
-
|
|
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).
|
|
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
|
|
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), --
|
|
202
|
-
"
|
|
203
|
-
"
|
|
204
|
-
"
|
|
205
|
-
"
|
|
206
|
-
"
|
|
207
|
-
"
|
|
208
|
-
"--on-approve '<step>'
|
|
209
|
-
"approval sets in motion: an
|
|
210
|
-
"Flag anything irreversible in the step
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
2329
|
-
//
|
|
2330
|
-
|
|
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
|
-
|
|
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.
|