@figs-so/cli 1.2.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.
Files changed (3) hide show
  1. package/SPEC.md +14 -3
  2. package/figs.mjs +26 -3
  3. 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 | | |
@@ -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": [ /* runs.jsonl */ ], // optional
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 never
269
- deletes. An agent **self-registers** on first push. **A push never walks a record backwards:** the
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
@@ -386,6 +386,19 @@ function genId(prefix) {
386
386
  return `${prefix}-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 5)}`
387
387
  }
388
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
+
389
402
  // ---------- local validation (the spec's common mistakes, caught on write) ----
390
403
  // The server's schema stays the source of truth; these catch what hand-authors
391
404
  // and flag typos get wrong, with errors that teach the fix.
@@ -1816,7 +1829,7 @@ async function reportCmd() {
1816
1829
  const id = idGiven || genId("r")
1817
1830
  // Before the append, so "new" means new-to-this-outbox (the typo catch).
1818
1831
  const isNew = !foldById(readJsonl("runs.jsonl")).some((r) => r.id === id)
1819
- const run = { id, ts: nowIso(), result, state: "settled" }
1832
+ const run = { id, eventId: genEventId(), ts: nowIso(), result, state: "settled" }
1820
1833
  const unit = flag("--unit")
1821
1834
  if (unit) run.unit = unit
1822
1835
  const period = flag("--period")
@@ -1881,7 +1894,7 @@ async function checkpointCmd() {
1881
1894
  }
1882
1895
  // Before the append, so "new" means new-to-this-outbox (the teaching line).
1883
1896
  const isNew = !foldById(readJsonl("runs.jsonl")).some((r) => r.id === id)
1884
- const run = { id, ts: nowIso(), result: note, state: "in-flight" }
1897
+ const run = { id, eventId: genEventId(), ts: nowIso(), result: note, state: "in-flight" }
1885
1898
  const unit = flag("--unit")
1886
1899
  if (unit) run.unit = unit
1887
1900
  const period = flag("--period")
@@ -2164,10 +2177,19 @@ async function doPush() {
2164
2177
  const agentJson = readJson(join(repoDir, "agent.json"), null)
2165
2178
  if (!agentJson) return fail("missing .figs/agent.json — author it, then `figs doctor`")
2166
2179
  const agent = { ...agentJson, id: config.agentId }
2167
- const runs = foldById(readJsonl("runs.jsonl"))
2180
+ const runLines = readJsonl("runs.jsonl")
2181
+ const runs = foldById(runLines)
2168
2182
  const asks = foldById(readJsonl("asks.jsonl"))
2169
2183
  // Messages are immutable events — sent whole (no fold); the server dedupes by id.
2170
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)
2171
2193
  // One-time confirm that a name change on an already-registered id is a real
2172
2194
  // rename, not a copied folder (the server's rename guard refuses it otherwise).
2173
2195
  const confirmRename = hasFlag("--rename")
@@ -2192,6 +2214,7 @@ async function doPush() {
2192
2214
  workspaceId: config.workspaceId,
2193
2215
  agent,
2194
2216
  runs,
2217
+ runEvents,
2195
2218
  asks,
2196
2219
  messages,
2197
2220
  ...(confirmRename ? { confirmRename: true } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@figs-so/cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.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": {