@figs-so/cli 0.2.1 → 0.4.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.
Files changed (4) hide show
  1. package/README.md +2 -1
  2. package/SPEC.md +6 -2
  3. package/figs.mjs +261 -34
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -73,7 +73,8 @@ are shorthand for exactly that (always current, no version drift). Prefer a real
73
73
  | `figs login` / `logout` | device-flow browser approve / remove local token |
74
74
  | `figs workspaces [--json]` | list your workspaces (create one in the web app) |
75
75
  | `figs init [--workspace <slug>]` | generate identity + write `.figs/` (omit the flag: uses your only workspace, else lists them) |
76
- | **`figs report --result "…"`** | record a runstamps id + timestamp, auto-captures the session trace, `--attach`es artifacts, pushes itself (`--resolves <ask-id>` closes an ask in the same stroke) |
76
+ | **`figs inbox [<ask-id>]`** | start every session here your humans' answers/verdicts, verbatim, with the next command per ask; with an id: the full zero-context handoff package (thread + artifacts restored) |
77
+ | **`figs report --result "…"`** | record a run — **one job, one stable `--id`** (re-reporting an id folds progress onto that job's row); stamps the timestamp, auto-captures the session trace, `--attach`es artifacts, pushes itself |
77
78
  | **`figs ask <type> --title "…"`** | raise a self-contained ask (`blocked` · `needs-decision` · `sign-off` · `fyi`) — options/details/attachments, pushed so a human sees it |
78
79
  | **`figs resolve <ask-id>`** | close an ask — `--chosen` verbatim-checked against its options, `--withdrawn` for the un-ask |
79
80
  | `figs push` | the bare transport — the verbs call it automatically; type it yourself after hand-edits or `--no-push` |
package/SPEC.md CHANGED
@@ -82,7 +82,12 @@ Use **`steps`** *or* **`responsibilities`** depending on shape — a fixed pipel
82
82
 
83
83
  ## 5. `runs.jsonl` — activity
84
84
 
85
- One JSON object per line (JSON Lines). Each is something the agent did.
85
+ One JSON object per line (JSON Lines). **One record = one job** — a unit of work the agent's
86
+ *manager* would recognize ("recon — Acme — November"), under a **stable, meaningful id**
87
+ (`recon-acme-2026-11`); the runs list reads as the job list. Records **fold by `id`** (same
88
+ merge as asks): re-reporting a job's id layers progress onto its row (`status` evolves
89
+ blocked-ish `warn` → `ok`) — sittings/sessions are agent plumbing and never mint records.
90
+ Closing an ask is **not** a job: that's a `resolution` in `asks.jsonl` (§6), never a run.
86
91
 
87
92
  | Field | Type | Req | Meaning |
88
93
  |---|---|:--:|---|
@@ -93,7 +98,6 @@ One JSON object per line (JSON Lines). Each is something the agent did.
93
98
  | `result` | string | | One-line outcome. |
94
99
  | `status` | `"ok"` \| `"warn"` \| `"fail"` | | Default `"ok"`. **Outcome, never lifecycle** — a run is a complete fact when reported; nothing "closes" a run. |
95
100
  | `artifacts` | string[] | | File names under `artifacts/` to attach. Singular `artifact` (string) remains valid shorthand for one — readers normalize to the array (same pattern as `resolution`'s bare-string shorthand). |
96
- | `resolves` | string | | The ask `id` this run executes/closes (the agent did the answered/approved thing and is reporting back — see [§6.1](#61-lifecycle--two-ledgers-split-by-author)). |
97
101
  | `session` | `Session` | | Where/how this ran (see [§5.1](#51-session--runtime-metadata-optional)). Optional, self-reported. |
98
102
 
99
103
  ### 5.1 `Session` — runtime metadata (optional)
package/figs.mjs CHANGED
@@ -11,7 +11,8 @@
11
11
  * create .figs/config.json + GUIDE.md (generates a stable agent id)
12
12
  * figs report --result "…" record a run (stamps id/ts/session, --attach files, auto-push)
13
13
  * figs ask <type> --title "…" raise an ask (self-contained: options/details/attachments, auto-push)
14
- * figs resolve <ask-id> close an ask (verbatim-checks --chosen, auto-push)
14
+ * figs inbox [<ask-id>] what needs you — answers/verdicts from your humans (pure read)
15
+ * figs resolve <ask-id> close an ask (verbatim-checks --chosen; cites the Figs answer it acted on)
15
16
  * figs doctor validate .figs/ against the spec before pushing
16
17
  * figs push one-way push the .figs/ spine to the ingest endpoint
17
18
  * figs version print the CLI version (and check for updates)
@@ -49,7 +50,7 @@ import {
49
50
  } from "node:fs"
50
51
  import { homedir } from "node:os"
51
52
  import { basename, extname, join } from "node:path"
52
- import { randomUUID } from "node:crypto"
53
+ import { createHash, randomUUID } from "node:crypto"
53
54
  import { execSync, spawn } from "node:child_process"
54
55
 
55
56
  // Single source of truth for the version: package.json (shipped alongside this
@@ -112,20 +113,23 @@ const COMMANDS = {
112
113
  report: {
113
114
  args: "--result <text> [options]",
114
115
  flags: [
115
- "--result", "--id", "--unit", "--period", "--status", "--attach",
116
- "--resolves", "--chosen", "--by", "--note", "--no-push",
116
+ "--result", "--id", "--unit", "--period", "--status", "--attach", "--no-push",
117
117
  ],
118
- desc: "record a run — writes one line to runs.jsonl, stamps id/ts/session, pushes",
118
+ desc: "record a run — one job's row in runs.jsonl; stamps id/ts/session, pushes",
119
119
  more: [
120
+ "One run = one JOB — a unit of work your manager would recognize; the runs",
121
+ "list reads as the job list. Give a job a stable, meaningful --id",
122
+ "(recon-acme-2026-11); reporting the same id again folds onto that job's row",
123
+ "(progress evolves: blocked → ok). Sittings/sessions never mint runs —",
124
+ "stopping to wait for a human is not a job.",
120
125
  "You supply the content; the CLI does the bookkeeping (id, real-clock ts, session",
121
126
  "trace from your runtime's own records, validation, artifact copy, push).",
122
127
  "--attach <file> (repeatable) copies the file into artifacts/ and links it.",
123
- "--resolves <ask-id> also closes that ask (with optional --chosen/--by/--note).",
124
- "--id only for deliberate stable ids (re-running the same job updates the same run).",
125
128
  "--no-push writes locally only; `figs push` publishes later.",
129
+ "Closing an ask is `figs resolve` — a close is not a job; never report one.",
126
130
  "Hand-writing runs.jsonl works too — this verb is sugar over the same file.",
127
131
  ],
128
- eg: 'figs report --result "88% matched · 31 flagged" --attach ./acme-2025-11.html',
132
+ eg: 'figs report --id recon-acme-2026-11 --result "88% matched · 31 flagged" --attach ./acme-2025-11.html',
129
133
  },
130
134
  ask: {
131
135
  args: "<type> --title <text> [options]",
@@ -148,6 +152,21 @@ const COMMANDS = {
148
152
  ],
149
153
  eg: 'figs ask sign-off --title "Send 10 payment reminders" --attach ./previews.html --run recon-2026-06',
150
154
  },
155
+ inbox: {
156
+ args: "[<ask-id>] [--json]",
157
+ flags: ["--json"],
158
+ desc: "what needs you — your humans' answers/verdicts on your asks (pure read)",
159
+ more: [
160
+ "Start every session with this. Bare: lists every ask with thread activity —",
161
+ "answers and verdicts verbatim, plus the exact next command for each.",
162
+ "With an ask id: the full handoff package — the ask, the whole thread, and its",
163
+ "attached artifacts restored into .figs/artifacts/ (hash-verified) so a fresh",
164
+ "session can act from the record alone.",
165
+ "Scope: THIS agent's open asks + human-rejected ones you haven't acknowledged.",
166
+ "Reads only — closing still happens via figs resolve.",
167
+ ],
168
+ eg: "figs inbox",
169
+ },
151
170
  resolve: {
152
171
  args: "<ask-id> [--chosen <option>] [--by <who>] [--note <text>] [--withdrawn|--rejected]",
153
172
  flags: ["--chosen", "--by", "--note", "--withdrawn", "--rejected", "--no-push"],
@@ -157,7 +176,9 @@ const COMMANDS = {
157
176
  "Three closes, by who ended it: resolved (default — the need was met) ·",
158
177
  "--withdrawn (YOU retracted it; nobody acted) · --rejected (a HUMAN declined",
159
178
  "it — record their out-of-band no; rejected is terminal, re-raising = a new ask).",
160
- "Use `figs report --resolves <ask-id>` instead when a run did the work.",
179
+ "After an answer, fork on what it unlocked: nothing new resolve right away;",
180
+ "real work → do the job, `figs report` it under its own id, THEN resolve —",
181
+ "cite the job id in --note so a reader can find the work.",
161
182
  ],
162
183
  eg: 'figs resolve acme-bridge --chosen "Strip the alpha prefix" --by "Sarah (accounting)"',
163
184
  },
@@ -477,6 +498,7 @@ else if (COMMANDS[cmd]) {
477
498
  else if (cmd === "init") await init()
478
499
  else if (cmd === "report") await reportCmd()
479
500
  else if (cmd === "ask") await askCmd()
501
+ else if (cmd === "inbox") await inboxCmd()
480
502
  else if (cmd === "resolve") await resolveCmd()
481
503
  else if (cmd === "doctor") await doctor()
482
504
  else if (cmd === "push") await push()
@@ -1103,31 +1125,237 @@ function captureCommit() {
1103
1125
  }
1104
1126
  }
1105
1127
 
1106
- // ---------- the resolution fold (shared by `resolve` and `report --resolves`) -
1107
- function buildResolution(askId, { chosen, by, note, withdrawn, rejected }) {
1128
+ // ---------- the inbox (the DOWN direction a pure read) ---------------------
1129
+
1130
+ /** Relative time for inbox lines — rough on purpose. */
1131
+ function agoStr(iso) {
1132
+ const mins = Math.max(0, Math.round((Date.now() - new Date(iso).getTime()) / 60000))
1133
+ if (mins < 60) return `${mins}m ago`
1134
+ if (mins < 60 * 48) return `${Math.round(mins / 60)}h ago`
1135
+ return `${Math.round(mins / 1440)}d ago`
1136
+ }
1137
+
1138
+ /** Fetch this agent's inbox; null on any failure when `soft` (auto-cite path). */
1139
+ async function fetchInbox({ soft = false } = {}) {
1140
+ const config = readJson(join(repoDir, "config.json"), {})
1141
+ if (!config.agentId) {
1142
+ if (soft) return null
1143
+ die("config missing agentId — run `figs init`")
1144
+ }
1145
+ const r = await request("GET", `/api/inbox?agent=${config.agentId}`)
1146
+ if (!r.ok) {
1147
+ if (soft) return null
1148
+ die(`inbox failed (${r.status || "network"}): ${r.data.error ?? r.data.raw ?? ""}`)
1149
+ }
1150
+ return r.data
1151
+ }
1152
+
1153
+ /** One human event, verbatim — answers are instructions; never paraphrase them. */
1154
+ function eventLine(e) {
1155
+ const head =
1156
+ e.kind === "verdict"
1157
+ ? `${e.verdict.replace(/_/g, " ")} by ${e.byName} (${e.asRole})`
1158
+ : `answered by ${e.byName} (${e.asRole})`
1159
+ const body = [e.chosen ? `→ "${e.chosen}"` : null, e.text ? `"${e.text}"` : null]
1160
+ .filter(Boolean)
1161
+ .join(" · ")
1162
+ return `${head} · ${agoStr(e.createdAt)}${body ? `\n ${body}` : ""}`
1163
+ }
1164
+
1165
+ /** The type×state matrix → the exact next command. The stranger never needs
1166
+ * to know the state machine — the protocol tells them their move. */
1167
+ function nextMove(a) {
1168
+ if (a.status === "rejected") {
1169
+ return `a human declined this — acknowledge it: figs resolve ${a.id} --rejected`
1170
+ }
1171
+ const last = a.events[a.events.length - 1]
1172
+ if (!last) return "waiting on your human — nothing for you to do"
1173
+ if (last.kind === "verdict" && last.verdict === "approved") {
1174
+ return (
1175
+ `approved — verify any prerequisites in the ask, then fork on what it unlocked:` +
1176
+ `\n nothing left to do → figs resolve ${a.id}` +
1177
+ `\n real work → do the job, figs report it under its own --id, then figs resolve ${a.id} --note "job <id>"`
1178
+ )
1179
+ }
1180
+ if (last.kind === "verdict" && last.verdict === "changes_requested") {
1181
+ return `revise, then re-raise on the same id: figs ask ${a.type} --id ${a.id} --title "…" …`
1182
+ }
1183
+ return `act on the answer (real work → figs report it under its own --id), then: figs resolve ${a.id} --chosen "…"`
1184
+ }
1185
+
1186
+ /** Restore an ask's refs into artifacts/ — hash-verified; never clobbers. */
1187
+ async function fetchRefs(config, refs) {
1188
+ for (const ref of refs ?? []) {
1189
+ if (!ref.artifact) continue
1190
+ const name = ref.artifact
1191
+ let res
1192
+ try {
1193
+ res = await fetchT(
1194
+ `${resolveEndpoint()}/api/artifacts/raw?agent=${config.agentId}&name=${encodeURIComponent(name)}`,
1195
+ { headers: { "x-figs-token": getToken() } },
1196
+ )
1197
+ } catch (e) {
1198
+ console.warn(`figs: ! couldn't fetch artifacts/${name} (${netReason(e)})`)
1199
+ continue
1200
+ }
1201
+ if (!res.ok) {
1202
+ console.warn(`figs: ! couldn't fetch artifacts/${name} (${res.status})`)
1203
+ continue
1204
+ }
1205
+ const bytes = Buffer.from(await res.arrayBuffer())
1206
+ const want = res.headers.get("x-figs-sha256")
1207
+ if (want && createHash("sha256").update(bytes).digest("hex") !== want) {
1208
+ console.warn(`figs: ! artifacts/${name}: bytes didn't match the server's hash — skipped`)
1209
+ continue
1210
+ }
1211
+ const dest = join(repoDir, "artifacts", name)
1212
+ if (existsSync(dest)) {
1213
+ if (readFileSync(dest).equals(bytes)) {
1214
+ console.log(`figs: ✓ artifacts/${name} (already present)`)
1215
+ } else {
1216
+ console.warn(
1217
+ `figs: ! artifacts/${name} exists locally with different content — left untouched (the published copy stays one fetch away)`,
1218
+ )
1219
+ }
1220
+ continue
1221
+ }
1222
+ mkdirSync(join(repoDir, "artifacts"), { recursive: true })
1223
+ writeFileSync(dest, bytes)
1224
+ console.log(`figs: ✓ artifacts/${name} (fetched, hash ok)`)
1225
+ }
1226
+ }
1227
+
1228
+ /**
1229
+ * `figs inbox` — session start. Bare: every ask with thread activity + the
1230
+ * next command for each. With an id: the zero-context handoff package (the
1231
+ * ask, the whole thread verbatim, refs restored to disk). Pure read — writes
1232
+ * nothing to the outbox; closing happens via resolve.
1233
+ */
1234
+ async function inboxCmd() {
1235
+ requireFigs()
1236
+ const config = readJson(join(repoDir, "config.json"), {})
1237
+ const data = await fetchInbox()
1238
+ const items = data.asks ?? []
1239
+ const askId = positional()
1240
+
1241
+ if (hasFlag("--json") && !askId) {
1242
+ console.log(JSON.stringify(data, null, 2))
1243
+ return
1244
+ }
1245
+
1246
+ if (askId) {
1247
+ const a = items.find((x) => x.id === askId)
1248
+ if (!a) {
1249
+ die(
1250
+ `ask "${askId}" isn't in your inbox (open asks + unacknowledged rejections only — closed history lives in the app)`,
1251
+ )
1252
+ }
1253
+ console.log(`figs: ${a.title}`)
1254
+ console.log(` ${a.type} · ${a.status}${a.to ? ` · for the ${a.to}` : ""} · raised ${agoStr(a.ts)}`)
1255
+ if (a.found) console.log(`\n What it found:\n ${a.found}`)
1256
+ if (a.need) console.log(`\n What it needs:\n ${a.need}`)
1257
+ if (a.options?.length) {
1258
+ console.log(`\n Options (answers cite these verbatim):`)
1259
+ for (const o of a.options) console.log(` · ${o}`)
1260
+ }
1261
+ if (a.details?.length) {
1262
+ console.log(`\n Details:`)
1263
+ for (const d of a.details) console.log(` ${d.l}: ${d.v}`)
1264
+ }
1265
+ if (a.events.length) {
1266
+ console.log(`\n THE THREAD (your humans' words, verbatim):`)
1267
+ for (const e of a.events) console.log(` · ${eventLine(e)}`)
1268
+ } else {
1269
+ console.log(`\n No answer yet.`)
1270
+ }
1271
+ if (a.refs?.length) {
1272
+ console.log(`\n Attached artifacts (restoring to .figs/artifacts/):`)
1273
+ await fetchRefs(config, a.refs)
1274
+ }
1275
+ console.log(`\n → next: ${nextMove(a)}`)
1276
+ return
1277
+ }
1278
+
1279
+ if (data.truncated) {
1280
+ console.warn(`figs: ! showing the first ${items.length} — more exist (close some asks)`)
1281
+ }
1282
+ if (items.length === 0) {
1283
+ console.log("figs: ✓ inbox empty — no open asks, nothing needs you")
1284
+ return
1285
+ }
1286
+ const rejected = items.filter((a) => a.status === "rejected")
1287
+ const answered = items.filter((a) => a.status === "open" && a.events.length > 0)
1288
+ const quiet = items.filter((a) => a.status === "open" && a.events.length === 0)
1289
+ console.log(
1290
+ `figs: inbox — ${answered.length} answered · ${rejected.length} rejected to acknowledge · ${quiet.length} waiting on your human`,
1291
+ )
1292
+ const printItem = (a) => {
1293
+ const last = a.events[a.events.length - 1]
1294
+ console.log(`\n ${a.id} · ${a.type} — ${a.title}`)
1295
+ if (last) console.log(` ${eventLine(last)}`)
1296
+ console.log(` → ${nextMove(a)}${a.events.length > 1 ? ` (full thread: figs inbox ${a.id})` : ""}`)
1297
+ }
1298
+ for (const a of [...rejected, ...answered]) printItem(a)
1299
+ if (quiet.length) {
1300
+ console.log(`\n Waiting on your human (nothing for you to do):`)
1301
+ for (const a of quiet) console.log(` · ${a.id} · ${a.type} — ${a.title} (raised ${agoStr(a.ts)})`)
1302
+ }
1303
+ }
1304
+
1305
+ // ---------- the resolution fold (`resolve` — the one closing verb) ----------
1306
+ /**
1307
+ * Build the closing fold line. Best-effort, the verified path: fetches this
1308
+ * agent's inbox and, when the ask has human events, cites the one acted on —
1309
+ * `via: "figs"` + `resolution.answer: <event-id>` (+ `by` from the event) —
1310
+ * attribution by mechanism, not testimony. An ask found on the server but
1311
+ * missing locally is appended first (its own bytes coming home), so the fold
1312
+ * lands on a complete record. Offline or any failure → today's self-reported
1313
+ * `via: "human"` path, unchanged.
1314
+ */
1315
+ async function buildResolution(askId, { chosen, by, note, withdrawn, rejected }) {
1108
1316
  if (withdrawn && rejected) {
1109
1317
  die("--withdrawn and --rejected are different closes: withdrawn = YOU retracted the ask; rejected = a HUMAN declined it. Pick the one that's true.")
1110
1318
  }
1111
1319
  if ((withdrawn || rejected) && chosen) {
1112
1320
  die(`--chosen marks the need as met — it can't combine with ${withdrawn ? "--withdrawn" : "--rejected"} (use --note for the account)`)
1113
1321
  }
1114
- const asks = foldById(readJsonl("asks.jsonl"))
1115
- const ask = asks.find((a) => a.id === askId)
1116
1322
  const warnings = []
1117
- if (!ask) {
1323
+ let ask = foldById(readJsonl("asks.jsonl")).find((a) => a.id === askId)
1324
+
1325
+ // The verified path (soft — never blocks a close).
1326
+ let serverAsk = null
1327
+ if (getToken()) {
1328
+ const inbox = await fetchInbox({ soft: true })
1329
+ serverAsk = inbox?.asks?.find((a) => a.id === askId) ?? null
1330
+ }
1331
+ if (!ask && serverAsk) {
1332
+ // Its own bytes coming home: append the record so the fold has something
1333
+ // local to land on (the cross-machine close, solved inside the verb).
1334
+ const record = { ...serverAsk }
1335
+ delete record.events
1336
+ delete record.updatedAt
1337
+ if (record.resolution == null) delete record.resolution
1338
+ appendJsonl("asks.jsonl", record)
1339
+ warnings.push(`fetched "${askId}" from Figs (raised elsewhere) — recorded locally before closing`)
1340
+ ask = record
1341
+ } else if (!ask) {
1118
1342
  warnings.push(
1119
1343
  `ask "${askId}" isn't in the local asks.jsonl (raised on another machine, or pruned) — recording the close anyway; the server folds it onto the full record`,
1120
1344
  )
1121
- } else if (chosen && ask.options?.length && !ask.options.includes(chosen)) {
1345
+ }
1346
+
1347
+ const options = ask?.options ?? serverAsk?.options ?? []
1348
+ if (chosen && options.length && !options.includes(chosen)) {
1122
1349
  const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "")
1123
- const near = ask.options.find((o) => norm(o) === norm(chosen))
1350
+ const near = options.find((o) => norm(o) === norm(chosen))
1124
1351
  if (near) die(`--chosen must quote the option verbatim — did you mean "${near}"?`)
1125
1352
  die(
1126
1353
  `--chosen "${chosen}" doesn't match any of the ask's options:\n` +
1127
- ask.options.map((o) => ` · ${o}`).join("\n") +
1354
+ options.map((o) => ` · ${o}`).join("\n") +
1128
1355
  "\n (quote one verbatim, or use --note for a free-text account)",
1129
1356
  )
1130
1357
  }
1358
+
1131
1359
  const resolution = {}
1132
1360
  if (chosen) resolution.chosen = chosen
1133
1361
  if (by) resolution.by = by
@@ -1137,6 +1365,20 @@ function buildResolution(askId, { chosen, by, note, withdrawn, rejected }) {
1137
1365
  // when there's evidence of one (a chosen/by), never on withdrawn (nobody
1138
1366
  // acted — that's the point).
1139
1367
  if (rejected || (!withdrawn && (chosen || by))) resolution.via = "human"
1368
+
1369
+ // Cite the event acted on, when one exists: prefer the latest answer whose
1370
+ // chosen matches, else the latest event (e.g. the approval, the rejection).
1371
+ const events = serverAsk?.events ?? []
1372
+ if (!withdrawn && events.length) {
1373
+ const match = chosen
1374
+ ? [...events].reverse().find((e) => e.kind === "answer" && e.chosen === chosen)
1375
+ : null
1376
+ const cited = match ?? events[events.length - 1]
1377
+ resolution.via = "figs"
1378
+ resolution.answer = cited.id
1379
+ if (!by && cited.byName) resolution.by = cited.byName
1380
+ }
1381
+
1140
1382
  const line = {
1141
1383
  id: askId,
1142
1384
  status: withdrawn ? "withdrawn" : rejected ? "rejected" : "resolved",
@@ -1173,16 +1415,6 @@ async function reportCmd() {
1173
1415
  const attached = attachFiles(flagAll("--attach"))
1174
1416
  if (attached.length === 1) run.artifact = attached[0]
1175
1417
  else if (attached.length > 1) run.artifacts = attached
1176
- const resolves = flag("--resolves")
1177
- let resolution = null
1178
- if (resolves) {
1179
- run.resolves = resolves
1180
- resolution = buildResolution(resolves, {
1181
- chosen: flag("--chosen"),
1182
- by: flag("--by"),
1183
- note: flag("--note"),
1184
- })
1185
- }
1186
1418
  const session = captureSession()
1187
1419
  if (session) run.session = session
1188
1420
 
@@ -1190,11 +1422,6 @@ async function reportCmd() {
1190
1422
  if (issues.length) die(`not written:\n ${issues.join("\n ")}`)
1191
1423
  appendJsonl("runs.jsonl", run)
1192
1424
  console.log(`figs: ✓ run recorded — ${summarize(run)}`)
1193
- if (resolution) {
1194
- for (const w of resolution.warnings) console.warn(`figs: ! ${w}`)
1195
- appendJsonl("asks.jsonl", resolution.line)
1196
- console.log(`figs: ✓ ask ${resolves} ${resolution.line.status}`)
1197
- }
1198
1425
  await autoPush()
1199
1426
  }
1200
1427
 
@@ -1272,7 +1499,7 @@ async function askCmd() {
1272
1499
  }
1273
1500
  await autoPush()
1274
1501
  console.log(
1275
- "figs: answers arrive out-of-band for now (`figs inbox` is coming) — your human replies in the app thread or directly to you",
1502
+ "figs: your human answers in the app start your next session with `figs inbox` to read it",
1276
1503
  )
1277
1504
  }
1278
1505
 
@@ -1280,7 +1507,7 @@ async function resolveCmd() {
1280
1507
  requireFigs()
1281
1508
  const askId = positional()
1282
1509
  if (!askId) die("resolve needs the ask id: figs resolve <ask-id> [--chosen …] [--withdrawn]")
1283
- const { line, warnings } = buildResolution(askId, {
1510
+ const { line, warnings } = await buildResolution(askId, {
1284
1511
  chosen: flag("--chosen"),
1285
1512
  by: flag("--by"),
1286
1513
  note: flag("--note"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@figs-so/cli",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
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": {