@figs-so/cli 0.7.0 → 0.8.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.
- package/README.md +6 -4
- package/SPEC.md +36 -10
- package/figs.mjs +133 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,8 +41,9 @@ npx @figs-so/cli@latest push # publish → it appears in you
|
|
|
41
41
|
|
|
42
42
|
That's it — your agent now shows up at **[app.figs.so](https://app.figs.so)**. No instrumentation, no
|
|
43
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
|
|
45
|
-
`figs ask` (needs a human) · `figs resolve` (close an
|
|
44
|
+
and day to day the agent records itself in one stroke per event: `figs checkpoint` (a job opens /
|
|
45
|
+
progresses) · `figs report` (its outcome) · `figs ask` (needs a human) · `figs resolve` (close an
|
|
46
|
+
ask). Each pushes itself.
|
|
46
47
|
|
|
47
48
|
## How it works
|
|
48
49
|
|
|
@@ -73,8 +74,9 @@ are shorthand for exactly that (always current, no version drift). Prefer a real
|
|
|
73
74
|
| `figs login` / `logout` | device-flow browser approve / remove local token |
|
|
74
75
|
| `figs workspaces [--json]` | list your workspaces (create one in the web app) |
|
|
75
76
|
| `figs init [--workspace <slug>]` | generate identity + write `.figs/` (omit the flag: uses your only workspace, else lists them) |
|
|
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
|
|
77
|
+
| **`figs inbox [<ask-id>]`** | start every session here — your humans' answers/verdicts, verbatim, with the next command per ask, plus your unfinished (in-flight) jobs; with an id: the full zero-context handoff package (thread + artifacts restored) |
|
|
78
|
+
| **`figs checkpoint --id <job> --note '…'`** | save a job's progress mid-flight — the **first checkpoint opens the job** (`state: in-flight`), so a crash leaves a recoverable stub the next session finds in the inbox; `--trigger` records what set the sitting in motion |
|
|
79
|
+
| **`figs report --result '…'`** | file a job's outcome — **one job, one stable `--id`** (re-reporting an id folds progress onto that job's row); settles the job (`state: settled`), stamps the timestamp, `--attach`es artifacts, pushes itself |
|
|
78
80
|
| **`figs ask <type> --title '…'`** | raise a self-contained ask (`needs-decision` · `sign-off` · `fyi`) — options/details/attachments, pushed so a human sees it |
|
|
79
81
|
| **`figs resolve <ask-id>`** | close an ask — `--chosen` verbatim-checked against its options, `--withdrawn` for the un-ask |
|
|
80
82
|
| `figs push` | the bare transport — the verbs call it automatically; type it yourself after hand-edits or `--no-push` |
|
package/SPEC.md
CHANGED
|
@@ -25,13 +25,26 @@
|
|
|
25
25
|
.figs/
|
|
26
26
|
├── config.json # identity + destination (committed, non-secret)
|
|
27
27
|
├── agent.json # the charter — who this agent is (committed)
|
|
28
|
+
├── CONTRACT.md # agent-authored: what this agent surfaces / holds back (committed)
|
|
29
|
+
├── GUIDE.md # orientation breadcrumb, written by the CLI (committed)
|
|
28
30
|
├── runs.jsonl # activity log, one JSON object per line (outbox; gitignored)
|
|
29
31
|
├── asks.jsonl # things needing a human, one per line (outbox; gitignored)
|
|
30
32
|
└── artifacts/ # files referenced by runs/asks (outbox; gitignored)
|
|
31
33
|
```
|
|
32
34
|
|
|
33
|
-
**Commit** `config.json` + `agent.json`
|
|
34
|
-
`asks.jsonl`, `artifacts/`) are a transient outbox and are typically gitignored.
|
|
35
|
+
**Commit** `config.json` + `agent.json` + `CONTRACT.md` + `GUIDE.md`. The activity files
|
|
36
|
+
(`runs.jsonl`, `asks.jsonl`, `artifacts/`) are a transient outbox and are typically gitignored.
|
|
37
|
+
|
|
38
|
+
**`CONTRACT.md` + `GUIDE.md` are companion conventions, not wire format** — they are never
|
|
39
|
+
pushed. `CONTRACT.md` is the standing agreement between the agent and its user about what gets
|
|
40
|
+
surfaced; `GUIDE.md` is an orientation stub the reference CLI writes (and never clobbers).
|
|
41
|
+
Implementations may add files like these; readers must ignore files this spec doesn't name.
|
|
42
|
+
|
|
43
|
+
**The membership rule — what belongs in `.figs/`:** everything in the folder is *Figs-facing* —
|
|
44
|
+
protocol metadata (`config.json`), the published record (the charter, the outbox), or a
|
|
45
|
+
convention *about* publishing (`CONTRACT.md`, `GUIDE.md`). An agent's private working state —
|
|
46
|
+
memory, self-checks, scratch notes — lives outside `.figs/`, elsewhere in the repo. If a file's
|
|
47
|
+
only reader is the agent itself, it does not belong here.
|
|
35
48
|
|
|
36
49
|
## 3. `config.json` — identity + destination
|
|
37
50
|
|
|
@@ -89,14 +102,22 @@ merge as asks): re-reporting a job's id layers progress onto its row (`status` e
|
|
|
89
102
|
blocked-ish `warn` → `ok`) — sittings/sessions are agent plumbing and never mint records.
|
|
90
103
|
Closing an ask is **not** a job: that's a `resolution` in `asks.jsonl` (§6), never a run.
|
|
91
104
|
|
|
105
|
+
A job is either **in flight** or **settled** (`state`, below). A **checkpoint**
|
|
106
|
+
(`figs checkpoint`) folds progress onto the job's id and marks it in-flight — the record
|
|
107
|
+
survives the session working it, so a crash mid-job leaves a visible, recoverable stub
|
|
108
|
+
instead of nothing. A **report** files the outcome and settles it; a report with no prior
|
|
109
|
+
checkpoint is simply a job **born settled** (the single-sitting case). Nothing *external*
|
|
110
|
+
ever closes a run — only the agent's own report settles its job.
|
|
111
|
+
|
|
92
112
|
| Field | Type | Req | Meaning |
|
|
93
113
|
|---|---|:--:|---|
|
|
94
114
|
| `id` | string | ✓ | Stable id (upsert key). |
|
|
95
115
|
| `ts` | string (ISO-8601 w/ offset) | ✓ | When it ran, e.g. `2026-05-28T23:41:26Z`. |
|
|
96
116
|
| `unit` | string | | The `Unit.id` this run is about. |
|
|
97
117
|
| `period` | string | | |
|
|
98
|
-
| `result` | string | |
|
|
99
|
-
| `status` | `"ok"` \| `"warn"` \| `"fail"` | | Default `"ok"`. **Outcome, never lifecycle** —
|
|
118
|
+
| `result` | string | | The job's current one-line state — where it stands while in flight; the outcome once settled. |
|
|
119
|
+
| `status` | `"ok"` \| `"warn"` \| `"fail"` | | Default `"ok"`. **Outcome, never lifecycle** — what the work looks like right now (a stuck job is `warn`); whether the job is *done* is `state`, the orthogonal dimension. |
|
|
120
|
+
| `state` | `"in-flight"` \| `"settled"` | | Default `"settled"`. **Lifecycle, verb-stamped** — `figs checkpoint` stamps `in-flight`, `figs report` stamps `settled`; agents never hand-pick it. An in-flight job whose agent died stays visibly in flight — that's the point: the next session finds it in `figs inbox` and finishes or settles it. |
|
|
100
121
|
| `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). |
|
|
101
122
|
| `session` | `Session` | | Where/how this ran (see [§5.1](#51-session--runtime-metadata-optional)). Optional, self-reported. |
|
|
102
123
|
|
|
@@ -104,9 +125,9 @@ Closing an ask is **not** a job: that's a `resolution` in `asks.jsonl` (§6), ne
|
|
|
104
125
|
|
|
105
126
|
An optional, **self-reported** block describing the runtime session that produced a run (or raised an
|
|
106
127
|
ask — see §6). Every field is optional — fill what your runtime exposes, omit the rest. This is
|
|
107
|
-
*transparency, not attestation*: the values come from the runtime's own records —
|
|
108
|
-
|
|
109
|
-
remains [reserved](#reserved-not-in-v1).
|
|
128
|
+
*transparency, not attestation*: the values come from the runtime's own records — hand-authored, or
|
|
129
|
+
written by integrations that can copy provable values at work-time (the CLI never infers them).
|
|
130
|
+
Cryptographic provenance remains [reserved](#reserved-not-in-v1).
|
|
110
131
|
|
|
111
132
|
| Field | Type | Meaning |
|
|
112
133
|
|---|---|---|
|
|
@@ -115,6 +136,7 @@ remains [reserved](#reserved-not-in-v1).
|
|
|
115
136
|
| `sessionId` | string | The runtime's own session identifier. |
|
|
116
137
|
| `startedAt` | string (ISO-8601 w/ offset) | When this job began (the record's `ts` is when it was reported). |
|
|
117
138
|
| `commit` | string | The agent repo's HEAD at run time; append `+dirty` when the working tree had uncommitted changes, e.g. `1b68668+dirty`. |
|
|
139
|
+
| `trigger` | string | What set this sitting in motion — one self-reported line, e.g. `monthly close cron`, `inbox: answer on acme-bridge`, `Wayne, in chat`. A *fresh* sitting on a job states it (stamped from `--trigger` on `figs checkpoint`/`report`); records continuing the same session omit it. The one mechanically verified trigger stays `resolution.answer` ([§6.2](#62-resolution--how-an-ask-closed)). |
|
|
118
140
|
| `tokens` | `{ input?, output?, cacheRead?, cacheWrite? }` (numbers) | **Session totals at report time** — cumulative for the whole session, *not* per-job. Approximate by design (an interactive session may include unrelated chat). Readers may derive per-run deltas between consecutive runs sharing a `sessionId`. Include cache figures when available — in agentic sessions they often dominate real cost. |
|
|
119
141
|
|
|
120
142
|
## 6. `asks.jsonl` — handoffs to a human
|
|
@@ -150,7 +172,8 @@ An ask is the **anchor of a thread whose two halves are owned by different parti
|
|
|
150
172
|
|
|
151
173
|
```jsonc
|
|
152
174
|
{ "id": "acme-bridge", "status": "resolved",
|
|
153
|
-
"resolution": { "chosen": "Strip the alpha prefix", "via": "human",
|
|
175
|
+
"resolution": { "chosen": "Strip the alpha prefix", "via": "human",
|
|
176
|
+
"by": "Sarah (accounting)", "ts": "2026-06-01T09:12:00Z" } }
|
|
154
177
|
```
|
|
155
178
|
|
|
156
179
|
Appending keeps the local file crash-safe, concurrency-safe (multiple runners), and an honest
|
|
@@ -174,6 +197,7 @@ in the agent's own workflow; answers flowing back through the reader are arrivin
|
|
|
174
197
|
| `via` | `"figs"` \| `"human"` \| `"self"` | Where the unblock came from: an answer pulled from Figs (verified — see `answer`) · answered out-of-band (self-reported) · the blocker cleared on its own. |
|
|
175
198
|
| `by` | string | Who answered, as the agent knows it (self-reported; verified attribution only exists for `via: "figs"`). |
|
|
176
199
|
| `answer` | string | The Figs answer-event id the agent acted on — written by `figs resolve` when the answer came through the inbox (attribution by mechanism, never typed). The cited event may be an answer **or a qualified verdict** (a verdict carrying `chosen`). |
|
|
200
|
+
| `ts` | string (ISO-8601 w/ offset) | When the agent closed it — **machine-stamped by `figs resolve`, never typed**. The agent's claim of the execution-adjacent moment, same self-reported grade as the record `ts`; readers stamp their own receipt at ingest and surface both only when they diverge. Lives *inside* `resolution` so the fold can't collide with the record's raise `ts`. |
|
|
177
201
|
|
|
178
202
|
All fields optional; a bare-string `resolution` is shorthand for `{ "note": … }` and readers
|
|
179
203
|
normalize it to the object form.
|
|
@@ -282,8 +306,10 @@ Deliberately out of scope for v1, named here so implementers don't repurpose the
|
|
|
282
306
|
```
|
|
283
307
|
|
|
284
308
|
```jsonc
|
|
285
|
-
// .figs/runs.jsonl (one object per line)
|
|
286
|
-
{ "id": "acme-2025-11", "ts": "2026-05-28T23:
|
|
309
|
+
// .figs/runs.jsonl (one object per line; records fold by id — the checkpoint opened the job, the report settled it)
|
|
310
|
+
{ "id": "acme-2025-11", "ts": "2026-05-28T23:05:40Z", "unit": "acme", "period": "2025-11", "state": "in-flight", "result": "Statements pulled — matching now",
|
|
311
|
+
"session": { "runtime": "claude-code", "trigger": "monthly close cron" } }
|
|
312
|
+
{ "id": "acme-2025-11", "ts": "2026-05-28T23:41:26Z", "unit": "acme", "period": "2025-11", "result": "88% matched · 31 keys flagged", "status": "ok", "state": "settled", "artifact": "acme-2025-11.html",
|
|
287
313
|
"session": { "runtime": "claude-code", "model": "claude-fable-5", "sessionId": "3fffcd97-d4f5-4b77-8243-8f450d7c9614",
|
|
288
314
|
"startedAt": "2026-05-28T23:02:00Z", "commit": "1b68668",
|
|
289
315
|
"tokens": { "input": 26608, "output": 135532, "cacheRead": 8677869, "cacheWrite": 543145 } } }
|
package/figs.mjs
CHANGED
|
@@ -111,15 +111,20 @@ const COMMANDS = {
|
|
|
111
111
|
report: {
|
|
112
112
|
args: "--result <text> [options]",
|
|
113
113
|
flags: [
|
|
114
|
-
"--result", "--id", "--unit", "--period", "--status", "--attach", "--no-push",
|
|
114
|
+
"--result", "--id", "--unit", "--period", "--status", "--trigger", "--attach", "--no-push",
|
|
115
115
|
],
|
|
116
|
-
desc: "
|
|
116
|
+
desc: "file a job's outcome — settles its row in runs.jsonl; stamps id/ts/state, pushes",
|
|
117
117
|
more: [
|
|
118
118
|
"One run = one JOB — a unit of work your manager would recognize; the runs",
|
|
119
119
|
"list reads as the job list. Give a job a stable, meaningful --id",
|
|
120
120
|
"(recon-acme-2026-11); reporting the same id again folds onto that job's row",
|
|
121
121
|
"(progress evolves: blocked → ok). Sittings/sessions never mint runs —",
|
|
122
122
|
"stopping to wait for a human is not a job.",
|
|
123
|
+
"A report SETTLES the job (state: settled). A job that outlives a sitting",
|
|
124
|
+
"should open with `figs checkpoint` first; a report with no prior checkpoint",
|
|
125
|
+
"is simply a job born settled (the single-sitting case).",
|
|
126
|
+
"--trigger '<what set this sitting in motion>' — state it in a fresh sitting",
|
|
127
|
+
"('monthly close cron', 'Wayne, in chat'); omit when continuing one.",
|
|
123
128
|
"You supply the content; the CLI does the bookkeeping (id, real-clock ts,",
|
|
124
129
|
"validation, artifact copy, push).",
|
|
125
130
|
"Single-quote prose values ('…') — inside double quotes your shell expands $,",
|
|
@@ -131,6 +136,29 @@ const COMMANDS = {
|
|
|
131
136
|
],
|
|
132
137
|
eg: "figs report --id recon-acme-2026-11 --result '88% matched · 31 flagged' --attach ./acme-2025-11.html",
|
|
133
138
|
},
|
|
139
|
+
checkpoint: {
|
|
140
|
+
args: "--id <job> --note <text> [options]",
|
|
141
|
+
flags: [
|
|
142
|
+
"--id", "--note", "--trigger", "--status", "--unit", "--period", "--attach", "--no-push",
|
|
143
|
+
],
|
|
144
|
+
desc: "save a job's progress mid-flight — folds onto its row, marks it in-flight, pushes",
|
|
145
|
+
more: [
|
|
146
|
+
"Your first checkpoint OPENS the job (state: in-flight) — make it the first",
|
|
147
|
+
"act of any job that will outlive this sitting: say what triggered it",
|
|
148
|
+
"(--trigger) and what you're setting out to do (--note). If you die mid-job,",
|
|
149
|
+
"the checkpoint is what the next session finds in `figs inbox`; without one,",
|
|
150
|
+
"the job never existed anywhere.",
|
|
151
|
+
"Checkpoint at MANAGER grain — a step a human would recognize ('statements",
|
|
152
|
+
"pulled — matching now'), never per tool call.",
|
|
153
|
+
"--note is the job's current one-line state; it shows on the job's row and",
|
|
154
|
+
"evolves with each fold. --status still carries the outcome look (a stuck",
|
|
155
|
+
"job is --status warn — in-flight and warn are independent).",
|
|
156
|
+
"`figs report --id <same-id>` settles the job when it's done — including",
|
|
157
|
+
"abandoning it (--status warn --result 'abandoned — superseded by …').",
|
|
158
|
+
"A checkpoint isn't a checkpoint until it's pushed — this verb pushes itself.",
|
|
159
|
+
],
|
|
160
|
+
eg: "figs checkpoint --id recon-acme-2026-11 --note 'Statements pulled — matching now' --trigger 'monthly close cron'",
|
|
161
|
+
},
|
|
134
162
|
ask: {
|
|
135
163
|
args: "<type> --title <text> [options]",
|
|
136
164
|
flags: [
|
|
@@ -170,8 +198,9 @@ const COMMANDS = {
|
|
|
170
198
|
"With an ask id: the full handoff package — the ask, the whole thread, and its",
|
|
171
199
|
"attached artifacts restored into .figs/artifacts/ (hash-verified) so a fresh",
|
|
172
200
|
"session can act from the record alone.",
|
|
173
|
-
"Scope: THIS agent's open asks + human-rejected ones you haven't acknowledged
|
|
174
|
-
"
|
|
201
|
+
"Scope: THIS agent's open asks + human-rejected ones you haven't acknowledged",
|
|
202
|
+
"+ unfinished jobs (in-flight runs — your past self's work; finish or settle).",
|
|
203
|
+
"Reads only — closing still happens via figs resolve / figs report.",
|
|
175
204
|
],
|
|
176
205
|
eg: "figs inbox",
|
|
177
206
|
},
|
|
@@ -296,6 +325,9 @@ function genId(prefix) {
|
|
|
296
325
|
// status, not an ask type; what you need from a human is a needs-decision.
|
|
297
326
|
const ASK_TYPES = ["needs-decision", "sign-off", "fyi"]
|
|
298
327
|
const RUN_STATUSES = ["ok", "warn", "fail"]
|
|
328
|
+
// Lifecycle, orthogonal to status (outcome): checkpoint stamps in-flight,
|
|
329
|
+
// report stamps settled. Absent = settled (a plain report is a complete fact).
|
|
330
|
+
const RUN_STATES = ["in-flight", "settled"]
|
|
299
331
|
const ASK_STATUSES = ["open", "resolved", "withdrawn", "rejected"]
|
|
300
332
|
const TO_VALUES = ["manager", "builder"]
|
|
301
333
|
const ARTIFACT_EXTS = new Set([
|
|
@@ -322,6 +354,7 @@ function validateRun(r) {
|
|
|
322
354
|
if (!r.id || typeof r.id !== "string") issues.push(`${label}: missing required "id"`)
|
|
323
355
|
if (!r.ts) issues.push(`${label}: missing required "ts" (ISO-8601 — \`figs report\` stamps it for you)`)
|
|
324
356
|
checkEnum(issues, r, "status", RUN_STATUSES, label)
|
|
357
|
+
checkEnum(issues, r, "state", RUN_STATES, label)
|
|
325
358
|
if (r.artifact !== undefined && typeof r.artifact !== "string") {
|
|
326
359
|
issues.push(`${label}.artifact: must be a single file name (use "artifacts" for a list)`)
|
|
327
360
|
}
|
|
@@ -522,6 +555,7 @@ else if (COMMANDS[cmd]) {
|
|
|
522
555
|
else if (cmd === "workspaces") await workspaces()
|
|
523
556
|
else if (cmd === "init") await init()
|
|
524
557
|
else if (cmd === "report") await reportCmd()
|
|
558
|
+
else if (cmd === "checkpoint") await checkpointCmd()
|
|
525
559
|
else if (cmd === "ask") await askCmd()
|
|
526
560
|
else if (cmd === "inbox") await inboxCmd()
|
|
527
561
|
else if (cmd === "resolve") await resolveCmd()
|
|
@@ -1199,15 +1233,17 @@ async function inboxCmd() {
|
|
|
1199
1233
|
if (data.truncated) {
|
|
1200
1234
|
console.warn(`figs: ! showing the first ${items.length} — more exist (close some asks)`)
|
|
1201
1235
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1236
|
+
const jobs = data.jobs ?? []
|
|
1237
|
+
if (items.length === 0 && jobs.length === 0) {
|
|
1238
|
+
console.log("figs: ✓ inbox empty — no open asks, no unfinished jobs, nothing needs you")
|
|
1204
1239
|
return
|
|
1205
1240
|
}
|
|
1206
1241
|
const rejected = items.filter((a) => a.status === "rejected")
|
|
1207
1242
|
const answered = items.filter((a) => a.status === "open" && a.events.length > 0)
|
|
1208
1243
|
const quiet = items.filter((a) => a.status === "open" && a.events.length === 0)
|
|
1209
1244
|
console.log(
|
|
1210
|
-
`figs: inbox — ${answered.length} answered · ${rejected.length} rejected to acknowledge · ${quiet.length} waiting on your human
|
|
1245
|
+
`figs: inbox — ${answered.length} answered · ${rejected.length} rejected to acknowledge · ${quiet.length} waiting on your human` +
|
|
1246
|
+
(jobs.length ? ` · ${jobs.length} job${jobs.length === 1 ? "" : "s"} in flight` : ""),
|
|
1211
1247
|
)
|
|
1212
1248
|
const printItem = (a) => {
|
|
1213
1249
|
const last = a.events[a.events.length - 1]
|
|
@@ -1220,6 +1256,17 @@ async function inboxCmd() {
|
|
|
1220
1256
|
console.log(`\n Waiting on your human (nothing for you to do):`)
|
|
1221
1257
|
for (const a of quiet) console.log(` · ${a.id} · ${a.type} — ${a.title} (raised ${agoStr(a.ts)})`)
|
|
1222
1258
|
}
|
|
1259
|
+
if (jobs.length) {
|
|
1260
|
+
console.log(`\n Unfinished jobs — in flight (your past self's work; finish or settle):`)
|
|
1261
|
+
for (const j of jobs) {
|
|
1262
|
+
console.log(
|
|
1263
|
+
` · ${j.id}${j.result ? ` — ${j.result}` : ""} (last update ${agoStr(j.updatedAt ?? j.ts)})`,
|
|
1264
|
+
)
|
|
1265
|
+
console.log(
|
|
1266
|
+
` → continue it (\`figs checkpoint --id ${j.id}\` as you go); \`figs report --id ${j.id}\` settles it`,
|
|
1267
|
+
)
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1223
1270
|
}
|
|
1224
1271
|
|
|
1225
1272
|
// ---------- the resolution fold (`resolve` — the one closing verb) ----------
|
|
@@ -1299,12 +1346,18 @@ async function buildResolution(askId, { chosen, by, note, withdrawn, rejected })
|
|
|
1299
1346
|
if (!by && cited.byName) resolution.by = cited.byName
|
|
1300
1347
|
}
|
|
1301
1348
|
|
|
1349
|
+
// The close's own clock — machine-stamped, never typed (same posture as the
|
|
1350
|
+
// record `ts`). It lives INSIDE resolution so the fold can't collide with
|
|
1351
|
+
// the record's raise `ts` (folds are field-level merges); the reader stamps
|
|
1352
|
+
// its own receipt (`closedAt`) at ingest — two clocks, claim vs receipt.
|
|
1353
|
+
resolution.ts = nowIso()
|
|
1354
|
+
|
|
1302
1355
|
warnEatenDollar(resolution.chosen, resolution.note)
|
|
1303
1356
|
const line = {
|
|
1304
1357
|
id: askId,
|
|
1305
1358
|
status: withdrawn ? "withdrawn" : rejected ? "rejected" : "resolved",
|
|
1359
|
+
resolution,
|
|
1306
1360
|
}
|
|
1307
|
-
if (Object.keys(resolution).length) line.resolution = resolution
|
|
1308
1361
|
return { line, warnings }
|
|
1309
1362
|
}
|
|
1310
1363
|
|
|
@@ -1326,13 +1379,17 @@ async function reportCmd() {
|
|
|
1326
1379
|
if (!result) {
|
|
1327
1380
|
die("report needs --result '<one-line outcome>' — e.g. figs report --result '88% matched · 31 flagged'")
|
|
1328
1381
|
}
|
|
1329
|
-
|
|
1382
|
+
// state is verb-stamped, never typed: report settles the job (checkpoint
|
|
1383
|
+
// marks it in-flight). The settling fold overrides any earlier checkpoint.
|
|
1384
|
+
const run = { id: flag("--id") || genId("r"), ts: nowIso(), result, state: "settled" }
|
|
1330
1385
|
const unit = flag("--unit")
|
|
1331
1386
|
if (unit) run.unit = unit
|
|
1332
1387
|
const period = flag("--period")
|
|
1333
1388
|
if (period) run.period = period
|
|
1334
1389
|
const status = flag("--status")
|
|
1335
1390
|
if (status) run.status = status
|
|
1391
|
+
const trigger = flag("--trigger")
|
|
1392
|
+
if (trigger) run.session = { trigger }
|
|
1336
1393
|
const attached = attachFiles(flagAll("--attach"))
|
|
1337
1394
|
if (attached.length === 1) run.artifact = attached[0]
|
|
1338
1395
|
else if (attached.length > 1) run.artifacts = attached
|
|
@@ -1341,9 +1398,76 @@ async function reportCmd() {
|
|
|
1341
1398
|
if (issues.length) die(`not written:\n ${issues.join("\n ")}`)
|
|
1342
1399
|
appendJsonl("runs.jsonl", run)
|
|
1343
1400
|
console.log(`figs: ✓ run recorded — ${JSON.stringify(run)}`)
|
|
1401
|
+
// Teaching, never a gate: a settled job with open asks citing it is the
|
|
1402
|
+
// normal tail-of-job pattern (the ask owns the waiting) — but if the job's
|
|
1403
|
+
// OUTCOME depends on an answer, in-flight is the honest state. Local fold
|
|
1404
|
+
// only (other machines' asks are invisible here): best-effort by design.
|
|
1405
|
+
const openCiting = foldById(readJsonl("asks.jsonl")).filter(
|
|
1406
|
+
(a) => a.run === run.id && (a.status ?? "open") === "open",
|
|
1407
|
+
)
|
|
1408
|
+
if (openCiting.length > 0) {
|
|
1409
|
+
const n = openCiting.length
|
|
1410
|
+
console.log(
|
|
1411
|
+
`figs: note: ${n} open ask${n === 1 ? "" : "s"} cite${n === 1 ? "s" : ""} this job (${openCiting
|
|
1412
|
+
.map((a) => a.id)
|
|
1413
|
+
.join(", ")}) — a tail sign-off is fine; if the OUTCOME depends on an answer, keep the job in flight instead (\`figs checkpoint --id ${run.id} --status warn\`)`,
|
|
1414
|
+
)
|
|
1415
|
+
}
|
|
1344
1416
|
await autoPush()
|
|
1345
1417
|
}
|
|
1346
1418
|
|
|
1419
|
+
/**
|
|
1420
|
+
* `figs checkpoint` — save a job's progress so it survives this sitting. The
|
|
1421
|
+
* first checkpoint on an id OPENS the job (state: in-flight); a crash mid-job
|
|
1422
|
+
* then leaves a visible, recoverable stub the next session finds in
|
|
1423
|
+
* `figs inbox`, instead of nothing. Same fold-by-id write as report — only
|
|
1424
|
+
* the stamped state differs; `figs report` settles the id.
|
|
1425
|
+
*/
|
|
1426
|
+
async function checkpointCmd() {
|
|
1427
|
+
requireFigs()
|
|
1428
|
+
const id = flag("--id")
|
|
1429
|
+
if (!id) {
|
|
1430
|
+
die("checkpoint needs --id '<job-id>' — the stable job id the next session will look for (e.g. recon-acme-2026-11)")
|
|
1431
|
+
}
|
|
1432
|
+
const note = flag("--note")
|
|
1433
|
+
if (!note) {
|
|
1434
|
+
die(`checkpoint needs --note '<where the job stands>' — e.g. figs checkpoint --id ${id} --note 'Statements pulled — matching now'`)
|
|
1435
|
+
}
|
|
1436
|
+
// Before the append, so "new" means new-to-this-outbox (the teaching line).
|
|
1437
|
+
const isNew = !foldById(readJsonl("runs.jsonl")).some((r) => r.id === id)
|
|
1438
|
+
const run = { id, ts: nowIso(), result: note, state: "in-flight" }
|
|
1439
|
+
const unit = flag("--unit")
|
|
1440
|
+
if (unit) run.unit = unit
|
|
1441
|
+
const period = flag("--period")
|
|
1442
|
+
if (period) run.period = period
|
|
1443
|
+
const status = flag("--status")
|
|
1444
|
+
if (status) run.status = status
|
|
1445
|
+
const trigger = flag("--trigger")
|
|
1446
|
+
if (trigger) run.session = { trigger }
|
|
1447
|
+
const attached = attachFiles(flagAll("--attach"))
|
|
1448
|
+
if (attached.length === 1) run.artifact = attached[0]
|
|
1449
|
+
else if (attached.length > 1) run.artifacts = attached
|
|
1450
|
+
warnEatenDollar(run.result)
|
|
1451
|
+
const issues = validateRun(run)
|
|
1452
|
+
if (issues.length) die(`not written:\n ${issues.join("\n ")}`)
|
|
1453
|
+
appendJsonl("runs.jsonl", run)
|
|
1454
|
+
console.log(`figs: ✓ checkpoint recorded — ${JSON.stringify(run)}`)
|
|
1455
|
+
if (isNew) {
|
|
1456
|
+
console.log(
|
|
1457
|
+
`figs: new job opened: ${id} (in flight) — checkpoint as you go; \`figs report --id ${id}\` settles it`,
|
|
1458
|
+
)
|
|
1459
|
+
}
|
|
1460
|
+
await autoPush()
|
|
1461
|
+
// A checkpoint exists to survive a crash — local-only protects nothing.
|
|
1462
|
+
// autoPush already said "saved locally"; for THIS verb that reads like
|
|
1463
|
+
// success, so say what it actually means.
|
|
1464
|
+
if (process.exitCode === 1) {
|
|
1465
|
+
console.warn(
|
|
1466
|
+
`figs: ! this checkpoint is NOT protecting the job yet — nothing reached the server. Run \`figs push\` before continuing the work.`,
|
|
1467
|
+
)
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1347
1471
|
async function askCmd() {
|
|
1348
1472
|
requireFigs()
|
|
1349
1473
|
let base = {}
|