@figs-so/cli 0.2.0 → 0.2.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.
Files changed (4) hide show
  1. package/README.md +8 -3
  2. package/SPEC.md +6 -4
  3. package/figs.mjs +32 -20
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -40,7 +40,9 @@ npx @figs-so/cli@latest push # publish → it appears in you
40
40
  ```
41
41
 
42
42
  That's it — your agent now shows up at **[app.figs.so](https://app.figs.so)**. No instrumentation, no
43
- SDK in your agent's code. From there you decide, deliberately, how much of its real work to surface.
43
+ SDK in your agent's code. From there you decide, deliberately, how much of its real work to surface
44
+ and day to day the agent records itself in one stroke per event: `figs report` (a run) ·
45
+ `figs ask` (needs a human) · `figs resolve` (close an ask). Each pushes itself.
44
46
 
45
47
  ## How it works
46
48
 
@@ -71,8 +73,11 @@ are shorthand for exactly that (always current, no version drift). Prefer a real
71
73
  | `figs login` / `logout` | device-flow browser approve / remove local token |
72
74
  | `figs workspaces [--json]` | list your workspaces (create one in the web app) |
73
75
  | `figs init [--workspace <slug>]` | generate identity + write `.figs/` (omit the flag: uses your only workspace, else lists them) |
74
- | `figs doctor` | validate `.figs/` against the contract before pushing |
75
- | `figs push` | one-way publish of `.figs/` |
76
+ | **`figs report --result "…"`** | record a run — stamps id + timestamp, auto-captures the session trace, `--attach`es artifacts, pushes itself (`--resolves <ask-id>` closes an ask in the same stroke) |
77
+ | **`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
+ | **`figs resolve <ask-id>`** | close an ask — `--chosen` verbatim-checked against its options, `--withdrawn` for the un-ask |
79
+ | `figs push` | the bare transport — the verbs call it automatically; type it yourself after hand-edits or `--no-push` |
80
+ | `figs doctor` | validate `.figs/` against the spec without pushing — the conformance check for hand-authored or non-CLI setups |
76
81
  | `figs status [--json]` | login / workspace / agent state |
77
82
  | `figs help [<command>]` | usage (`-h`/`--help` on any command; `-v` for version) |
78
83
 
package/SPEC.md CHANGED
@@ -122,7 +122,7 @@ primitive** — the agent reached the edge of its autonomy.
122
122
  |---|---|:--:|---|
123
123
  | `id` | string | ✓ | Stable id (upsert key). |
124
124
  | `type` | enum | ✓ | `blocked` \| `needs-decision` \| `sign-off` \| `fyi`. `fyi` is a non-blocking heads-up (no decision needed). |
125
- | `status` | enum | | `"open"` (default) \| `"resolved"` (the need was met) \| `"withdrawn"` (the agent un-asked — no longer needed, nobody acted). |
125
+ | `status` | enum | | `"open"` (default) \| `"resolved"` (the need was met) \| `"withdrawn"` (the **asker** retracted it — no longer needed, nobody acted) \| `"rejected"` (the **answerer** declined it — a human said no; usually born in the reader's UI, but the agent may record an out-of-band rejection too). Three closes, three authors-of-the-ending. **Rejected is terminal** on this id — readers keep it sticky; re-raising is a new ask. |
126
126
  | `to` | `"manager"` \| `"builder"` | | Who the ask is addressed to: the human accountable for the **work** (`manager`) or for the **machine** (`builder` — e.g. self-edit/logic-change flags). Absent = unaddressed; readers may guess from `type` but must present it as a guess. |
127
127
  | `title` | string | ✓ | The ask, in one line. |
128
128
  | `unit` | string | | The `Unit.id` this concerns. |
@@ -154,9 +154,11 @@ An ask is the **anchor of a thread whose two halves are owned by different parti
154
154
  These are [reserved](#reserved-not-in-v1) in v1 and **never appear in `asks.jsonl`**: nobody
155
155
  writes into the other side's record; the two ledgers cross-reference by id.
156
156
 
157
- The full state machine: `open` → *(claimed → answered — human, server-side, reserved)* →
158
- `resolved` | `withdrawn` *(agent, in `asks.jsonl`)*. In v1 only the agent-owned transitions exist;
159
- resolution happens in the agent's own workflow.
157
+ The full state machine: `open` → *(answered/verdict — human, server-side)* →
158
+ `resolved` | `withdrawn` *(agent, in `asks.jsonl`)* plus the one human-side close:
159
+ **`rejected`** (a reject verdict in the reader's UI closes the ask immediately; the agent's
160
+ later resolution append folds onto it without reopening). Today resolution otherwise happens
161
+ in the agent's own workflow; answers flowing back through the reader are arriving incrementally.
160
162
 
161
163
  ### 6.2 `Resolution` — how an ask closed
162
164
 
package/figs.mjs CHANGED
@@ -142,18 +142,21 @@ const COMMANDS = {
142
142
  "verbatim), --detail \"Label=Value\" (repeatable), --attach <file> (repeatable;",
143
143
  "for sign-offs attach the exact content for review + a brief: what to do once",
144
144
  "approved and what it requires).",
145
- "--run <id|last> links the run this came out of (last = newest local run).",
145
+ "--run <run-id> links the run this came out of explicit id only (other",
146
+ "sessions may report concurrently; `figs report` prints the id it wrote).",
146
147
  "--stdin reads a full JSON object instead of flags (long texts; attachments still via --attach).",
147
148
  ],
148
- eg: 'figs ask sign-off --title "Send 10 payment reminders" --attach ./previews.html --run last',
149
+ eg: 'figs ask sign-off --title "Send 10 payment reminders" --attach ./previews.html --run recon-2026-06',
149
150
  },
150
151
  resolve: {
151
- args: "<ask-id> [--chosen <option>] [--by <who>] [--note <text>] [--withdrawn]",
152
- flags: ["--chosen", "--by", "--note", "--withdrawn", "--no-push"],
152
+ args: "<ask-id> [--chosen <option>] [--by <who>] [--note <text>] [--withdrawn|--rejected]",
153
+ flags: ["--chosen", "--by", "--note", "--withdrawn", "--rejected", "--no-push"],
153
154
  desc: "close an ask — appends the resolution fold line and pushes",
154
155
  more: [
155
156
  "--chosen must quote one of the ask's options[] verbatim (checked).",
156
- "--withdrawn = the ask is no longer needed, nobody acted (don't mark resolved).",
157
+ "Three closes, by who ended it: resolved (default the need was met) ·",
158
+ "--withdrawn (YOU retracted it; nobody acted) · --rejected (a HUMAN declined",
159
+ "it — record their out-of-band no; rejected is terminal, re-raising = a new ask).",
157
160
  "Use `figs report --resolves <ask-id>` instead when a run did the work.",
158
161
  ],
159
162
  eg: 'figs resolve acme-bridge --chosen "Strip the alpha prefix" --by "Sarah (accounting)"',
@@ -234,7 +237,9 @@ function positional() {
234
237
  }
235
238
  return undefined
236
239
  }
237
- const BOOLEAN_FLAGS = new Set(["--no-push", "--stdin", "--withdrawn", "--json", "-h", "--help"])
240
+ const BOOLEAN_FLAGS = new Set([
241
+ "--no-push", "--stdin", "--withdrawn", "--rejected", "--json", "-h", "--help",
242
+ ])
238
243
 
239
244
  /** ISO-8601 with the machine's real UTC offset (never the agent's guess). */
240
245
  function nowIso() {
@@ -258,7 +263,7 @@ function genId(prefix) {
258
263
  // and flag typos get wrong, with errors that teach the fix.
259
264
  const ASK_TYPES = ["blocked", "needs-decision", "sign-off", "fyi"]
260
265
  const RUN_STATUSES = ["ok", "warn", "fail"]
261
- const ASK_STATUSES = ["open", "resolved", "withdrawn"]
266
+ const ASK_STATUSES = ["open", "resolved", "withdrawn", "rejected"]
262
267
  const TO_VALUES = ["manager", "builder"]
263
268
  const ARTIFACT_EXTS = new Set([
264
269
  ".html", ".md", ".txt", ".json", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg",
@@ -1099,9 +1104,12 @@ function captureCommit() {
1099
1104
  }
1100
1105
 
1101
1106
  // ---------- the resolution fold (shared by `resolve` and `report --resolves`) -
1102
- function buildResolution(askId, { chosen, by, note, withdrawn }) {
1103
- if (withdrawn && chosen) {
1104
- die("--withdrawn and --chosen are mutually exclusive (withdrawn = the ask is no longer needed, nobody acted)")
1107
+ function buildResolution(askId, { chosen, by, note, withdrawn, rejected }) {
1108
+ if (withdrawn && rejected) {
1109
+ die("--withdrawn and --rejected are different closes: withdrawn = YOU retracted the ask; rejected = a HUMAN declined it. Pick the one that's true.")
1110
+ }
1111
+ if ((withdrawn || rejected) && chosen) {
1112
+ die(`--chosen marks the need as met — it can't combine with ${withdrawn ? "--withdrawn" : "--rejected"} (use --note for the account)`)
1105
1113
  }
1106
1114
  const asks = foldById(readJsonl("asks.jsonl"))
1107
1115
  const ask = asks.find((a) => a.id === askId)
@@ -1125,10 +1133,14 @@ function buildResolution(askId, { chosen, by, note, withdrawn }) {
1125
1133
  if (by) resolution.by = by
1126
1134
  if (note) resolution.note = note
1127
1135
  // `via` says where the unblock came from; out-of-band human answers are
1128
- // "human". Set it only when there's evidence of one (a chosen/by), never on
1129
- // withdrawn (nobody acted that's the point).
1130
- if (!withdrawn && (chosen || by)) resolution.via = "human"
1131
- const line = { id: askId, status: withdrawn ? "withdrawn" : "resolved" }
1136
+ // "human". A rejection is inherently a human's call; otherwise set it only
1137
+ // when there's evidence of one (a chosen/by), never on withdrawn (nobody
1138
+ // acted that's the point).
1139
+ if (rejected || (!withdrawn && (chosen || by))) resolution.via = "human"
1140
+ const line = {
1141
+ id: askId,
1142
+ status: withdrawn ? "withdrawn" : rejected ? "rejected" : "resolved",
1143
+ }
1132
1144
  if (Object.keys(resolution).length) line.resolution = resolution
1133
1145
  return { line, warnings }
1134
1146
  }
@@ -1234,12 +1246,11 @@ async function askCmd() {
1234
1246
  if (details.length) ask.details = [...(base.details ?? []), ...details]
1235
1247
  const runRef = flag("--run")
1236
1248
  if (runRef === "last") {
1237
- const runs = foldById(readJsonl("runs.jsonl"))
1238
- if (!runs.length) {
1239
- die("--run last: no runs in the local runs.jsonl — `figs report` one first, or pass an explicit id")
1240
- }
1241
- ask.run = runs.reduce((a, b) => ((a.ts ?? "") > (b.ts ?? "") ? a : b)).id
1242
- } else if (runRef) ask.run = runRef
1249
+ // Deliberately unsupported: concurrent sessions of the same agent report
1250
+ // runs in parallel — "the latest run" may be someone else's. Explicit only.
1251
+ die('--run takes the explicit run id (no "last"another session may have reported since); `figs report` prints the id of what it wrote')
1252
+ }
1253
+ if (runRef) ask.run = runRef
1243
1254
  const attached = attachFiles(flagAll("--attach"))
1244
1255
  if (attached.length) {
1245
1256
  ask.refs = [...(base.refs ?? []), ...attached.map((n) => ({ label: n, artifact: n }))]
@@ -1274,6 +1285,7 @@ async function resolveCmd() {
1274
1285
  by: flag("--by"),
1275
1286
  note: flag("--note"),
1276
1287
  withdrawn: hasFlag("--withdrawn"),
1288
+ rejected: hasFlag("--rejected"),
1277
1289
  })
1278
1290
  for (const w of warnings) console.warn(`figs: ! ${w}`)
1279
1291
  appendJsonl("asks.jsonl", line)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@figs-so/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.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": {