@figs-so/cli 1.1.0 → 1.3.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/SPEC.md +16 -5
- package/figs.mjs +57 -5
- package/package.json +1 -1
package/SPEC.md
CHANGED
|
@@ -119,9 +119,17 @@ crash mid-job leaves a visible, recoverable stub. A **report** files the outcome
|
|
|
119
119
|
report with no prior checkpoint is a job **born settled** (the single-sitting case). Nothing *external*
|
|
120
120
|
ever closes a run — only the agent's own report settles its job.
|
|
121
121
|
|
|
122
|
+
**Folding is local; the lifecycle still publishes.** The folded record is the job's *current* state; its
|
|
123
|
+
history is the sequence of lines. On push the CLI sends **both** — the folded record (the row) *and* the
|
|
124
|
+
raw fold lines (`runEvents`, §8) — so a reader rebuilds the timeline (opened → checkpoints → settled)
|
|
125
|
+
even when a whole account-free history is published in a single first push (which would otherwise collapse
|
|
126
|
+
to one "settled" snapshot). Each line's `eventId` is the dedupe key: one timeline entry per fold,
|
|
127
|
+
idempotent across re-pushes.
|
|
128
|
+
|
|
122
129
|
| Field | Type | Req | Meaning |
|
|
123
130
|
|---|---|:--:|---|
|
|
124
131
|
| `id` | string | ✓ | Stable id (upsert key). |
|
|
132
|
+
| `eventId` | string | | Machine-minted per-fold id (`evt-…`) — **plumbing, never typed** (like the agent UUID, §4). Survives folding: a job's id collapses many lines into one record, but each line keeps its own. Lets a reader rebuild the per-fold timeline from the raw lines published as `runEvents` (§8). Absent on hand-authored/legacy lines — they fold to one published snapshot, as before. |
|
|
125
133
|
| `ts` | string (ISO-8601 w/ offset) | ✓ | When it ran. Machine-stamped by the CLI, never typed. |
|
|
126
134
|
| `unit` | string | | The `Unit.id` this run is about. |
|
|
127
135
|
| `period` | string | | |
|
|
@@ -156,9 +164,9 @@ edge of its autonomy.
|
|
|
156
164
|
| Field | Type | Req | Meaning |
|
|
157
165
|
|---|---|:--:|---|
|
|
158
166
|
| `id` | string | ✓ | Stable id (upsert key). |
|
|
159
|
-
| `type` | enum | ✓ | `question` \| `sign-off` — **the type is the answer contract**: *question* wants an answer (an option or free text), *sign-off* wants a verdict (approve / request-changes / reject). (`needs-decision` was renamed `question`; `fyi` was retired — a for-the-record note is a settled report, not an ask; `blocked` is the run's `status`, not an ask type.) |
|
|
167
|
+
| `type` | enum | ✓ | `question` \| `sign-off` — **the type is the answer contract**: *question* wants an answer (an option or free text), *sign-off* wants a verdict (approve / request-changes / reject). (`needs-decision` was renamed `question`; `fyi` was retired — a for-the-record note / assumption / heads-up is a `checkpoint` on the job (or a settled `report`), not an ask; `blocked` is the run's `status`, not an ask type.) |
|
|
160
168
|
| `status` | enum | | `"open"` (default) \| `"resolved"` (the need was met) \| `"withdrawn"` (the **agent** retracted it; nobody acted) \| `"rejected"` (a human declined it). **Rejected is terminal** on this id — re-raising is a new ask. |
|
|
161
|
-
| `to` | `"manager"` \| `"builder"` | | Who the ask is addressed to: the human accountable for the **work** (`manager`) or for the **machine** (`builder`). Absent = unaddressed. |
|
|
169
|
+
| `to` | `"manager"` \| `"builder"` | | Who the ask is addressed to: the human accountable for the **work** (`manager`) or for the **machine** (`builder`). Absent = unaddressed; `figs ask` **defaults it to `manager`** (the common case) when omitted, so a reader needn't infer. |
|
|
162
170
|
| `title` | string | ✓ | The ask, in one line. |
|
|
163
171
|
| `unit` | string | | The `Unit.id` this concerns. |
|
|
164
172
|
| `run` | string | | The run `id` this ask was raised during. **Optional** — asks also arise outside runs. |
|
|
@@ -257,7 +265,8 @@ minimum CLI version requires Bearer.)
|
|
|
257
265
|
{
|
|
258
266
|
"workspaceId": "<uuid>", // from config.json
|
|
259
267
|
"agent": { /* agent.json */ },
|
|
260
|
-
"runs":
|
|
268
|
+
"runs": [ /* runs.jsonl folded by id */ ], // optional — the current state per job (the row)
|
|
269
|
+
"runEvents": [ /* runs.jsonl raw fold lines, eventId-bearing only */ ], // optional — rebuilds the timeline
|
|
261
270
|
"asks": [ /* asks.jsonl */ ], // optional
|
|
262
271
|
"messages": [ /* messages.jsonl */ ], // optional — transcribed replies the reader lacks
|
|
263
272
|
"confirmRename": true // optional — `figs push --rename`: confirm a real name change
|
|
@@ -265,8 +274,10 @@ minimum CLI version requires Bearer.)
|
|
|
265
274
|
```
|
|
266
275
|
2. **Each attached file** → `POST {endpoint}/api/artifacts/upload`, base64-encoded, hash-verified.
|
|
267
276
|
|
|
268
|
-
The server upserts the agent by `id` and runs/asks by `id`; it dedupes messages by event `id`; it
|
|
269
|
-
|
|
277
|
+
The server upserts the agent by `id` and runs/asks by `id`; it dedupes messages by event `id`; it
|
|
278
|
+
**rebuilds each run's timeline from `runEvents`, one entry per fold, deduped by `eventId`** (a run with no
|
|
279
|
+
`runEvents` — an older CLI, or legacy/hand-authored lines without an `eventId` — keeps one timeline entry
|
|
280
|
+
per content-changing push, the prior behavior); it never deletes. An agent **self-registers** on first push. **A push never walks a record backwards:** the
|
|
270
281
|
server refuses a fold older than the record's stored close/settle (a stale machine pushing old state)
|
|
271
282
|
and accepts a newer one (a legitimate reopen — the `warn` → `ok` evolution). **A push never re-homes an
|
|
272
283
|
agent:** a `workspaceId` differing from the agent's registered home is rejected `409`
|
package/figs.mjs
CHANGED
|
@@ -89,10 +89,11 @@ const COMMANDS = {
|
|
|
89
89
|
eg: "figs status --json",
|
|
90
90
|
},
|
|
91
91
|
login: {
|
|
92
|
-
args: "[<token>]",
|
|
93
|
-
flags: [],
|
|
92
|
+
args: "[<token>] [--force]",
|
|
93
|
+
flags: ["--force"],
|
|
94
94
|
desc: "log in — browser device-flow, or save a pasted token",
|
|
95
95
|
more: [
|
|
96
|
+
"already logged in? it's a no-op that points you to `figs link` — pass --force to log in again.",
|
|
96
97
|
"no arg → device flow: opens a browser for a human to approve (you never see the token).",
|
|
97
98
|
"<token> → save a token you already have to ~/.figs/credentials.json.",
|
|
98
99
|
],
|
|
@@ -385,6 +386,19 @@ function genId(prefix) {
|
|
|
385
386
|
return `${prefix}-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 5)}`
|
|
386
387
|
}
|
|
387
388
|
|
|
389
|
+
// A run line's stable, machine-minted identity (the plumbing-id class, rule 7 —
|
|
390
|
+
// no verb accepts it; like the agent UUID). It survives folding: a job's id
|
|
391
|
+
// collapses many lines into one record, but each line keeps its own eventId.
|
|
392
|
+
// `figs push` sends the raw fold lines (with these ids) as `runEvents` so the
|
|
393
|
+
// app can rebuild the full in-flight timeline — opened → checkpoints → settled —
|
|
394
|
+
// even when a long account-free history is published in a single first link-late
|
|
395
|
+
// push (it would otherwise fold to one "settled" snapshot, losing the lifecycle).
|
|
396
|
+
// UUID-backed so it's a collision-free dedupe key: the server appends one
|
|
397
|
+
// timeline row per eventId, ON CONFLICT DO NOTHING, so re-pushes never duplicate.
|
|
398
|
+
function genEventId() {
|
|
399
|
+
return `evt-${randomUUID()}`
|
|
400
|
+
}
|
|
401
|
+
|
|
388
402
|
// ---------- local validation (the spec's common mistakes, caught on write) ----
|
|
389
403
|
// The server's schema stays the source of truth; these catch what hand-authors
|
|
390
404
|
// and flag typos get wrong, with errors that teach the fix.
|
|
@@ -846,6 +860,17 @@ async function login(token) {
|
|
|
846
860
|
return
|
|
847
861
|
}
|
|
848
862
|
|
|
863
|
+
// Already logged in? Don't start a needless device flow — it strands a cold/headless
|
|
864
|
+
// agent with no human to approve. Token presence is enough to short-circuit; a stale
|
|
865
|
+
// token surfaces on the next link/push. `--force` re-runs the flow deliberately.
|
|
866
|
+
if (!hasFlag("--force") && getToken()) {
|
|
867
|
+
console.log(`figs: ✓ already logged in to ${originOf(resolveEndpoint())}`)
|
|
868
|
+
console.log(
|
|
869
|
+
" `figs status` for details · `figs link` to connect a workspace · `figs login --force` to log in again",
|
|
870
|
+
)
|
|
871
|
+
return
|
|
872
|
+
}
|
|
873
|
+
|
|
849
874
|
const start = await request("POST", "/api/device/start")
|
|
850
875
|
if (!start.ok) die(`could not start login (${start.status})`)
|
|
851
876
|
const d = start.data
|
|
@@ -1804,7 +1829,7 @@ async function reportCmd() {
|
|
|
1804
1829
|
const id = idGiven || genId("r")
|
|
1805
1830
|
// Before the append, so "new" means new-to-this-outbox (the typo catch).
|
|
1806
1831
|
const isNew = !foldById(readJsonl("runs.jsonl")).some((r) => r.id === id)
|
|
1807
|
-
const run = { id, ts: nowIso(), result, state: "settled" }
|
|
1832
|
+
const run = { id, eventId: genEventId(), ts: nowIso(), result, state: "settled" }
|
|
1808
1833
|
const unit = flag("--unit")
|
|
1809
1834
|
if (unit) run.unit = unit
|
|
1810
1835
|
const period = flag("--period")
|
|
@@ -1824,6 +1849,14 @@ async function reportCmd() {
|
|
|
1824
1849
|
// Announce new-vs-fold only for an explicit --id — that's where a typo means
|
|
1825
1850
|
// "I meant to continue a job but opened a sibling" (auto-ids are always new).
|
|
1826
1851
|
if (idGiven) announceFold("job", id, isNew, " (settled)")
|
|
1852
|
+
// The "why it started": a born-settled job that never checkpointed still shows
|
|
1853
|
+
// "triggered by …" in the manager's timeline when --trigger is set. Nudge it
|
|
1854
|
+
// only on a fresh job (never on a fold/continuation — that would cry wolf).
|
|
1855
|
+
if (isNew && !trigger) {
|
|
1856
|
+
console.log(
|
|
1857
|
+
`figs: tip: \`--trigger '<what set this in motion>'\` records the "why" — your manager sees it on the job's timeline`,
|
|
1858
|
+
)
|
|
1859
|
+
}
|
|
1827
1860
|
// Teaching, never a gate: a settled job with open asks citing it is the
|
|
1828
1861
|
// normal tail-of-job pattern (the ask owns the waiting) — but if the job's
|
|
1829
1862
|
// OUTCOME depends on an answer, in-flight is the honest state. Local fold
|
|
@@ -1861,7 +1894,7 @@ async function checkpointCmd() {
|
|
|
1861
1894
|
}
|
|
1862
1895
|
// Before the append, so "new" means new-to-this-outbox (the teaching line).
|
|
1863
1896
|
const isNew = !foldById(readJsonl("runs.jsonl")).some((r) => r.id === id)
|
|
1864
|
-
const run = { id, ts: nowIso(), result: note, state: "in-flight" }
|
|
1897
|
+
const run = { id, eventId: genEventId(), ts: nowIso(), result: note, state: "in-flight" }
|
|
1865
1898
|
const unit = flag("--unit")
|
|
1866
1899
|
if (unit) run.unit = unit
|
|
1867
1900
|
const period = flag("--period")
|
|
@@ -1887,6 +1920,11 @@ async function checkpointCmd() {
|
|
|
1887
1920
|
console.log(
|
|
1888
1921
|
`figs: new job opened: ${id} (in flight) — checkpoint as you go; \`figs report --id ${id}\` settles it`,
|
|
1889
1922
|
)
|
|
1923
|
+
if (!trigger) {
|
|
1924
|
+
console.log(
|
|
1925
|
+
`figs: tip: add \`--trigger '<what set this in motion>'\` so your manager sees why this job started`,
|
|
1926
|
+
)
|
|
1927
|
+
}
|
|
1890
1928
|
} else if (settledBefore) {
|
|
1891
1929
|
console.warn(
|
|
1892
1930
|
`figs: ! reopening a settled job (${id}) — continue only if it's truly the same job; new work wants a new id`,
|
|
@@ -1937,6 +1975,10 @@ async function askCmd() {
|
|
|
1937
1975
|
const v = flag(f)
|
|
1938
1976
|
if (v) ask[k] = v
|
|
1939
1977
|
}
|
|
1978
|
+
// Default an unaddressed ask to the manager (the work-accountable human) — the common
|
|
1979
|
+
// case; `--to builder` is the explicit exception. Saves the reader from inferring ("guess").
|
|
1980
|
+
// (Hand-authored JSONL may still omit `to` → unaddressed; the verb is sugar that defaults.)
|
|
1981
|
+
if (!ask.to) ask.to = "manager"
|
|
1940
1982
|
const options = flagAll("--option")
|
|
1941
1983
|
if (options.length) ask.options = options
|
|
1942
1984
|
for (const o of ask.options ?? []) {
|
|
@@ -2135,10 +2177,19 @@ async function doPush() {
|
|
|
2135
2177
|
const agentJson = readJson(join(repoDir, "agent.json"), null)
|
|
2136
2178
|
if (!agentJson) return fail("missing .figs/agent.json — author it, then `figs doctor`")
|
|
2137
2179
|
const agent = { ...agentJson, id: config.agentId }
|
|
2138
|
-
const
|
|
2180
|
+
const runLines = readJsonl("runs.jsonl")
|
|
2181
|
+
const runs = foldById(runLines)
|
|
2139
2182
|
const asks = foldById(readJsonl("asks.jsonl"))
|
|
2140
2183
|
// Messages are immutable events — sent whole (no fold); the server dedupes by id.
|
|
2141
2184
|
const messages = readJsonl("messages.jsonl")
|
|
2185
|
+
// The raw fold lines ride alongside the folded spine: `runs` (folded) still
|
|
2186
|
+
// drives the run row + validation; `runEvents` lets the server rebuild the
|
|
2187
|
+
// per-fold timeline (opened → checkpoints → settled), so a link-late first
|
|
2188
|
+
// push doesn't collapse a whole offline lifecycle into one "settled" snapshot.
|
|
2189
|
+
// Only eventId-bearing lines (CLI-written) — legacy/hand-authored lines lack
|
|
2190
|
+
// the dedupe key, so the server keeps its current one-row-per-push behavior
|
|
2191
|
+
// for them (and runs they belong to are untouched).
|
|
2192
|
+
const runEvents = runLines.filter((r) => r.eventId)
|
|
2142
2193
|
// One-time confirm that a name change on an already-registered id is a real
|
|
2143
2194
|
// rename, not a copied folder (the server's rename guard refuses it otherwise).
|
|
2144
2195
|
const confirmRename = hasFlag("--rename")
|
|
@@ -2163,6 +2214,7 @@ async function doPush() {
|
|
|
2163
2214
|
workspaceId: config.workspaceId,
|
|
2164
2215
|
agent,
|
|
2165
2216
|
runs,
|
|
2217
|
+
runEvents,
|
|
2166
2218
|
asks,
|
|
2167
2219
|
messages,
|
|
2168
2220
|
...(confirmRename ? { confirmRename: true } : {}),
|