@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.
- package/README.md +8 -3
- package/SPEC.md +6 -4
- package/figs.mjs +32 -20
- 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
|
-
|
|
|
75
|
-
|
|
|
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
|
|
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` → *(
|
|
158
|
-
`resolved` | `withdrawn` *(agent, in `asks.jsonl`)
|
|
159
|
-
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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([
|
|
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 &&
|
|
1104
|
-
die("--withdrawn and --
|
|
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".
|
|
1129
|
-
//
|
|
1130
|
-
|
|
1131
|
-
|
|
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
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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)
|