flowviant 0.117.0 → 0.124.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 (103) hide show
  1. package/README.md +41 -0
  2. package/bin/cli.mjs +44 -0
  3. package/bin/lib/agentAsk.mjs +123 -0
  4. package/bin/lib/agentBrief.mjs +36 -0
  5. package/bin/lib/agentFootprint.mjs +76 -0
  6. package/bin/lib/agentInternalWait.mjs +4 -3
  7. package/bin/lib/agentPlan.mjs +86 -7
  8. package/bin/lib/agentPrerequisiteContract.mjs +43 -0
  9. package/bin/lib/agentRepositories.mjs +48 -0
  10. package/bin/lib/agentTaskKinds.mjs +1 -1
  11. package/bin/lib/artifactContracts.mjs +91 -20
  12. package/bin/lib/artifactIteration.mjs +182 -0
  13. package/bin/lib/artifactRecovery.mjs +36 -0
  14. package/bin/lib/artifactReporter.mjs +183 -19
  15. package/bin/lib/artifacts.mjs +6 -3
  16. package/bin/lib/checkoutIdentity.mjs +25 -0
  17. package/bin/lib/claudeAuth.mjs +15 -5
  18. package/bin/lib/claudeInput.mjs +32 -0
  19. package/bin/lib/claudePosture.mjs +110 -14
  20. package/bin/lib/claudeSessions.mjs +20 -0
  21. package/bin/lib/claudeStream.mjs +20 -2
  22. package/bin/lib/codexProjectRead.mjs +63 -0
  23. package/bin/lib/codexResearch.mjs +16 -3
  24. package/bin/lib/commandSpecs.mjs +6 -0
  25. package/bin/lib/credentials.mjs +52 -17
  26. package/bin/lib/daemonLocks.mjs +26 -6
  27. package/bin/lib/desktopContract.mjs +3 -0
  28. package/bin/lib/discoveryContract.mjs +19 -0
  29. package/bin/lib/fleet.mjs +65 -10
  30. package/bin/lib/fleetReports.mjs +44 -3
  31. package/bin/lib/fleetRoster.mjs +1 -89
  32. package/bin/lib/fleetStartup.mjs +11 -14
  33. package/bin/lib/holder.mjs +4 -37
  34. package/bin/lib/hooks/mcpAllow.mjs +60 -0
  35. package/bin/lib/hooks/readGuard.mjs +13 -4
  36. package/bin/lib/imageArtifact.mjs +74 -0
  37. package/bin/lib/instance.mjs +20 -7
  38. package/bin/lib/landed.mjs +2 -2
  39. package/bin/lib/login.mjs +39 -4
  40. package/bin/lib/maintenance.mjs +47 -0
  41. package/bin/lib/maintenanceInventory.mjs +39 -0
  42. package/bin/lib/mcpInstall.mjs +163 -0
  43. package/bin/lib/mcpInventory.mjs +119 -0
  44. package/bin/lib/placeLock.mjs +9 -7
  45. package/bin/lib/planDependencies.mjs +70 -0
  46. package/bin/lib/postureTools.mjs +85 -0
  47. package/bin/lib/prWorkflow.mjs +15 -0
  48. package/bin/lib/projectLanded.mjs +16 -0
  49. package/bin/lib/projectRepositories.mjs +60 -0
  50. package/bin/lib/projectToolRuntimes.mjs +227 -3
  51. package/bin/lib/projectTools.mjs +63 -5
  52. package/bin/lib/projectWorktreeHome.mjs +25 -0
  53. package/bin/lib/prompts.mjs +170 -42
  54. package/bin/lib/repositoryBaseline.mjs +68 -0
  55. package/bin/lib/repositoryDelivery.mjs +64 -0
  56. package/bin/lib/repositoryMerge.mjs +116 -0
  57. package/bin/lib/repositoryPlaces.mjs +179 -0
  58. package/bin/lib/repositorySetup.mjs +58 -0
  59. package/bin/lib/repositoryTool.mjs +71 -0
  60. package/bin/lib/runTurn.mjs +58 -18
  61. package/bin/lib/runtimeAntigravity.mjs +2 -1
  62. package/bin/lib/runtimeCapabilities.mjs +77 -3
  63. package/bin/lib/runtimeClaude.mjs +31 -10
  64. package/bin/lib/runtimeCodex.mjs +28 -59
  65. package/bin/lib/runtimeDetection.mjs +6 -0
  66. package/bin/lib/runtimeEvents.mjs +12 -1
  67. package/bin/lib/runtimeOfferings.mjs +75 -3
  68. package/bin/lib/shipMerge.mjs +5 -2
  69. package/bin/lib/startProject.mjs +4 -4
  70. package/bin/lib/trace.mjs +8 -3
  71. package/bin/lib/turnDeclaration.mjs +88 -0
  72. package/bin/lib/turnDiagnostics.mjs +93 -0
  73. package/bin/lib/turnLock.mjs +48 -8
  74. package/bin/lib/turnProfile.mjs +15 -3
  75. package/bin/lib/work.mjs +48 -29
  76. package/bin/lib/workAgentBrief.mjs +92 -0
  77. package/bin/lib/workAgentBriefRunner.mjs +59 -0
  78. package/bin/lib/workAgentCheck.mjs +6 -5
  79. package/bin/lib/workAgentMerges.mjs +19 -2
  80. package/bin/lib/workAgentPlans.mjs +21 -70
  81. package/bin/lib/workAgentPrecheck.mjs +10 -5
  82. package/bin/lib/workAgentPublish.mjs +18 -5
  83. package/bin/lib/workAgentRecovery.mjs +85 -0
  84. package/bin/lib/workAgentReview.mjs +5 -3
  85. package/bin/lib/workAgentTurnBegun.mjs +6 -4
  86. package/bin/lib/workAgentTurnExecution.mjs +167 -29
  87. package/bin/lib/workAgentTurnOutcome.mjs +20 -5
  88. package/bin/lib/workAgentTurns.mjs +92 -12
  89. package/bin/lib/workArtifactReconcile.mjs +79 -0
  90. package/bin/lib/workCappedTurn.mjs +10 -0
  91. package/bin/lib/workCommandAudit.mjs +8 -1
  92. package/bin/lib/workDelivery.mjs +21 -0
  93. package/bin/lib/workDiffs.mjs +18 -11
  94. package/bin/lib/workLiveMessages.mjs +129 -0
  95. package/bin/lib/workPlaceReports.mjs +5 -0
  96. package/bin/lib/workPlaces.mjs +24 -3
  97. package/bin/lib/workRetire.mjs +5 -0
  98. package/bin/lib/workSessionTurns.mjs +219 -52
  99. package/bin/lib/workShip.mjs +17 -16
  100. package/bin/lib/workToolLog.mjs +70 -3
  101. package/bin/lib/workTurnShape.mjs +6 -4
  102. package/package.json +7 -3
  103. package/npm-shrinkwrap.json +0 -59
@@ -6,7 +6,7 @@
6
6
  * and research contracts in prompts.mjs (which is past its size and keeps
7
7
  * those two). `prompts.mjs` imports these into `AGENT_CONTRACTS`; the kind
8
8
  * table (agentTaskKinds.mjs) runs both under the `design` posture — read the
9
- * repo, write only `.flowviant/artifacts/**`, no web, no shell beyond `ls` —
9
+ * repo, write only artifacts and scratch, with CLI-specific supporting tools —
10
10
  * so "change no repository file" is enforced, not asked.
11
11
  *
12
12
  * THE SAME BONES AS THE DESIGN CONTRACT, deliberately: its header, its rule 3
@@ -26,13 +26,70 @@
26
26
  */
27
27
  import { ARTIFACT_BINARY_MODEL_EXTS, ARTIFACT_BINARY_MODEL_MAX_BYTES, ARTIFACT_MAX_BYTES } from './artifactPolicy.mjs';
28
28
 
29
+ /**
30
+ * WHEN AN AGENT STOPS TO ASK, IT HELPS THE PERSON DECIDE (2026-10-03; its
31
+ * choices as fields of their own, 2026-10-13).
32
+ *
33
+ * "STOP AND ASK" used to be the whole of it, so a stuck agent asked one bare
34
+ * question with its choices inlined as "(1) … (2) …", and the Agent page had
35
+ * nothing to draw but "Write my own answer". Every card-agent contract (code
36
+ * and every file-handing kind) closes on this: read the card, the repo and
37
+ * the run first; ask ONE question per turn, its reason in `context` (body
38
+ * text beneath the one-line question, 2026-10-13); name the choices
39
+ * in `options` (and `multiSelect`) beside `question`, which the daemon turns
40
+ * into the one fence the page reads (`agentAsk.mjs`) — the agent never writes
41
+ * the fence itself, the owner's ruling; re-check after each answer; never
42
+ * re-ask. The label rules are SYSTEM_WORK rule 5's, because the page sends a
43
+ * label as the answer exactly as the Terminal's card does.
44
+ *
45
+ * THE SKIP IS THE APP'S. The agent page offers "Skip this question" on every
46
+ * standing question and sends a reply that BEGINS with those words (Flowviant
47
+ * `AGENT_SKIP_REPLY`) through the ordinary answer lane — so this keys on the
48
+ * prefix, never the whole sentence, and the app's wording can change without
49
+ * a daemon release. A skip is a stated provisional default where the work can
50
+ * go on, and never a substitute for an input only the person holds.
51
+ */
52
+ export const HELP_THEM_DECIDE = `WHEN YOU STOP TO ASK, HELP THEM DECIDE. The person may not know what the card
53
+ is missing, so you lead:
54
+ - Before asking, read the whole card (brief, done-when and its discussion),
55
+ the repository and this run, and check the card for every gap that would
56
+ change the work: the goal, who it is for, scope, the flows and behaviour,
57
+ edge cases, constraints, and how done is judged. Never ask what the card,
58
+ the repository or an earlier answer already settles.
59
+ - Ask ONE question per turn, the one that matters most. "question" is the
60
+ question itself — one sentence, read as a heading. In "context", say what
61
+ decision it settles and how it changes the work, with any findings or
62
+ evidence the person needs; it shows beneath the question as body text, in
63
+ Markdown, so lists and line breaks survive.
64
+ - When the answer is a pick between options you can name, put them in
65
+ "options" — two to eight — and the page shows each as a choice beside a
66
+ box for their own words. Never inline the choices in "question" as
67
+ "(1) … (2) …", and never write a fenced block yourself: the fields are the
68
+ whole of it. A label IS the answer, a few words a person would say out
69
+ loud, never a comma (several chosen labels arrive comma-joined, in the
70
+ order listed); a tradeoff goes in "description", one short sentence,
71
+ optional. Never add an "Other" or "Skip" option — the page offers its own
72
+ free-text answer and its own Skip. "multiSelect": true only for a genuine
73
+ check-several-of-these case ("which pages?"); otherwise false. For an open
74
+ question — one only their own words can answer — leave "options" out.
75
+ - When they answer, check the card again before you go on: ask the next
76
+ question that still changes the work, or carry on with the card once none
77
+ is left. Never ask again a question they answered or skipped.
78
+ - A message that begins "Skip this question" skips your last question. Where
79
+ the work can go on, take your best default, say it plainly as PROVISIONAL in
80
+ "progress" and in "summary", and carry on. Where it truly cannot go on
81
+ without something only they can provide — a credential, an account, an
82
+ outside decision — stop and name exactly that dependency; a skip never
83
+ supplies it.`;
84
+
29
85
  /**
30
86
  * Rule 3, rule 4 and the final JSON a file-handing contract ends with.
31
87
  * `ask` is rule 4's body after its number (the decision only a person can
32
88
  * make); `summary` is the delivered shape's summary gloss.
33
89
  */
34
90
  export const ARTIFACT_CONTRACT_TAIL = ({ ask, summary }) => `3. CHANGE NO REPOSITORY FILE AND COMMIT NOTHING. You can only write under
35
- .flowviant/artifacts/, and the daemon keeps that directory out of git.
91
+ .flowviant/artifacts/ and the turn’s granted scratch directory; deliver
92
+ only artifacts. The daemon keeps those directories out of git.
36
93
 
37
94
  4. ${ask}
38
95
 
@@ -52,12 +109,17 @@ or, if you are stopping to ask:
52
109
  {
53
110
  "status": "blocked",
54
111
  "progress": "two or three sentences on what you have done in this run SO FAR, across every card",
55
- "question": "the specific thing you need decided, in one or two sentences"
112
+ "context": "optional: what the decision settles and why, and any findings, in Markdown",
113
+ "question": "the specific thing you need decided, in one sentence",
114
+ "options": [{ "label": "an answer they could pick", "description": "optional: its tradeoff, in one short sentence" }],
115
+ "multiSelect": false
56
116
  }
57
117
  \`\`\`
58
118
 
59
119
  "progress" is REQUIRED on both shapes and REPLACES the previous one whole —
60
- past tense, plain sentences, no card ids.`;
120
+ past tense, plain sentences, no card ids.
121
+
122
+ ${HELP_THEM_DECIDE}`;
61
123
 
62
124
  /**
63
125
  * NOBODY IS AT THIS SCREEN (2026-09-29, the owner: Chrome "kept opening on my
@@ -87,13 +149,10 @@ export const LOOK_HEADLESS = `${NO_WINDOW} To look at your page, screenshot it
87
149
  * when the person presses Download .glb in Review (apps/web
88
150
  * `workbench/glbExport.ts`, the other half of the handshake below).
89
151
  *
90
- * WHY NOT A MODEL FILE ON DISK. This kind runs under the `design` posture:
91
- * Read, `ls`, and `Edit(.flowviant/artifacts/**)` — text only, no shell. A
92
- * .glb is binary; a .gltf's buffer is base64 floats nobody writes by hand.
93
- * The 0.105.0 contract asked for OBJ + .mtl + SVG textures instead, and a
94
- * katana came back as seven files, textures no game engine imports and a
95
- * generator script (2026-09-27). Built in code, drawn textures are canvases,
96
- * and the exporter embeds them in the .glb as PNGs.
152
+ * WHY A PAGE: one self-contained viewer gives every supported CLI the same
153
+ * delivered product. Supporting native textures are embedded into the page;
154
+ * GLTFExporter embeds their pixels into the person's one downloaded GLB.
155
+ * Available helpers do not change that contract into a pile of model files.
97
156
  *
98
157
  * THE HANDSHAKE IS SPELLED WORD FOR WORD because the web keys on it: the page
99
158
  * announces `glb-ready` once its model stands, and answers `export-glb` with
@@ -103,8 +162,8 @@ export const LOOK_HEADLESS = `${NO_WINDOW} To look at your page, screenshot it
103
162
  * it is just never offered a Download .glb.
104
163
  */
105
164
  export const SYSTEM_AGENT_MODEL = `You are the human's own Claude, working one 3D MODEL card in a git worktree of
106
- their repository. Nobody is watching this run. You have the repo and nothing
107
- else — no project tools, no board, no chat.
165
+ their repository. Nobody is watching this run. You have the repo and the tools
166
+ TOOLS ON THIS CARD names below — no board, no chat.
108
167
 
109
168
  A 3D MODEL CARD HANDS BACK A MODEL YOU CAN LOOK AT, NOT A CHANGE. Nothing you
110
169
  do here edits the product: the person turns the model around, downloads it as
@@ -121,7 +180,8 @@ WHAT TO DO:
121
180
  2. Write ONE file, .flowviant/artifacts/<kebab-name>/index.html — a short
122
181
  kebab-case name for what it is (for example
123
182
  .flowviant/artifacts/reading-chair/index.html). Nothing else goes in that
124
- folder: no model files, no textures, no scripts, no notes. The page BUILDS
183
+ folder: no model files, no scripts, no notes. Generated source textures
184
+ may be kept elsewhere under .flowviant/artifacts/; the page is self-contained. The page BUILDS
125
185
  THE MODEL IN CODE and shows it:
126
186
  - three.js as ES modules from cdn.jsdelivr.net/npm, through an import map
127
187
  for "three" and "three/addons/"; orbit controls, lights and a neutral
@@ -129,8 +189,18 @@ WHAT TO DO:
129
189
  - the model is ONE THREE.Group named for what it is, built from three.js
130
190
  geometry (lathe, extrude, tube, box and the rest, one mesh per part),
131
191
  with MeshStandardMaterial only. Colour, roughness and metalness are
132
- material values, or textures you DRAW ON A CANVAS in the page
133
- (THREE.CanvasTexture, 1024px or less) — never an image file.
192
+ material values, canvas textures, or generated image textures. When the
193
+ person requests generated textures, USE native image generation if it is
194
+ available on this turn; do not require another card or change the kind.
195
+ Embed each texture as a data: URI in this page (no local or remote image
196
+ URLs), load it with THREE.TextureLoader and wait for all textures before
197
+ announcing glb-ready. Keep maps at 1024px or less and set colour maps to
198
+ THREE.SRGBColorSpace. Assign maps to the model's MeshStandardMaterial so
199
+ GLTFExporter embeds the texture pixels in the downloaded .glb. Await
200
+ image decoding before export; never announce readiness for unloaded maps.
201
+ If the native tool is missing or fails, explain its actual limitation.
202
+ A separately billed CLI/API fallback requires the person's explicit
203
+ choice before using it; never ask for OPENAI_API_KEY as the default path.
134
204
  - the ground, a grid, the lights, a figure for scale and every other
135
205
  helper stay OUTSIDE that group: they are the viewer's, not the model.
136
206
  - no download or save button of your own: the page cannot download
@@ -168,8 +238,8 @@ ${ARTIFACT_CONTRACT_TAIL({
168
238
  * Print, never a converter this machine would have to run.
169
239
  */
170
240
  export const SYSTEM_AGENT_DECK = `You are the human's own Claude, working one PRESENTATION card in a git worktree
171
- of their repository. Nobody is watching this run. You have the repo and nothing
172
- else — no project tools, no board, no chat.
241
+ of their repository. Nobody is watching this run. You have the repo and the tools
242
+ TOOLS ON THIS CARD names below — no board, no chat.
173
243
 
174
244
  A PRESENTATION CARD HANDS BACK A DECK, NOT A CHANGE. Nothing you do here edits
175
245
  the product: the person pages through it, and decides.
@@ -232,8 +302,9 @@ export const IMAGE_CAP_WORDS = `${Math.floor(imageCapBytes / (1024 * 1024))} MB`
232
302
  * to the person, quoting the tool's words, never a substitute.
233
303
  */
234
304
  export const SYSTEM_AGENT_IMAGE = `You are the human's own Codex, working one IMAGE card in a git worktree of
235
- their repository. Nobody is watching this run. You have the repo and your image
236
- generation tool, and nothing else — no project tools, no board, no chat, no web.
305
+ their repository. Nobody is watching this run. You have the repo, your image
306
+ generation tool and the tools TOOLS ON THIS CARD names below — no board, no
307
+ chat, no web.
237
308
 
238
309
  AN IMAGE CARD HANDS BACK PICTURES, NOT A CHANGE. Nothing you do here edits the
239
310
  product: the person looks at the images, downloads the ones they want, and
@@ -0,0 +1,182 @@
1
+ /** Explicit iteration relationships, plus measured local deletions. No model runs here. */
2
+ import { constants, openSync, closeSync, fstatSync, readFileSync, lstatSync, unlinkSync, realpathSync, writeFileSync, renameSync, rmSync } from 'node:fs';
3
+ import { join, dirname, resolve, sep } from 'node:path';
4
+ import { createHash } from 'node:crypto';
5
+ import { flowviantParentState, realDirState } from './ownedDirs.mjs';
6
+ import { ARTIFACT_DIR, artifactStoredName, walkArtifactDir } from './artifacts.mjs';
7
+
8
+ const MANIFEST = '.iteration.json';
9
+ const safeName = (name) => typeof name === 'string' && name.length <= 500 &&
10
+ name.split('/').every((p) => p && p !== '.' && p !== '..' && !p.startsWith('.') && !p.includes('\\'));
11
+
12
+ const manifestText = (placeDir) => {
13
+ if (!walkArtifactDir(placeDir, () => {})) return null;
14
+ let fd;
15
+ try {
16
+ fd = openSync(join(placeDir, ARTIFACT_DIR, MANIFEST), constants.O_RDONLY | constants.O_NOFOLLOW);
17
+ const st = fstatSync(fd);
18
+ if (!st.isFile() || st.size > 32 * 1024) return null;
19
+ return { text: readFileSync(fd, 'utf8'), stamp: `${st.size}:${st.mtimeMs}` };
20
+ } catch { return null; }
21
+ finally { if (fd !== undefined) closeSync(fd); }
22
+ };
23
+ const signature = (value) => value === null ? null : `${value.stamp}:${createHash('sha256').update(value.text).digest('hex')}`;
24
+ export const iterationSignature = (placeDir) => signature(manifestText(placeDir));
25
+
26
+ export function iterationChanges(placeDir, before) {
27
+ const text = manifestText(placeDir);
28
+ if (text === null || signature(text) === before?.iterationSignature) return { remove: [], replace: {} };
29
+ let value;
30
+ try { value = JSON.parse(text.text); } catch { throw new Error('Artifact iteration declaration is not valid JSON.'); }
31
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Artifact iteration declaration must be an object.');
32
+ const remove = value.remove ?? [];
33
+ const replace = value.replace ?? {};
34
+ if (!Array.isArray(remove) || remove.length > 40 || !remove.every(safeName) ||
35
+ !replace || typeof replace !== 'object' || Array.isArray(replace) || Object.keys(replace).length > 40 ||
36
+ !Object.entries(replace).every(([from, to]) => safeName(from) && safeName(to) && from !== to)) {
37
+ throw new Error('Artifact iteration needs remove names and explicit old-to-new replacement names.');
38
+ }
39
+ const removed = new Set([...remove, ...Object.keys(replace)]);
40
+ if (Object.values(replace).some((to) => removed.has(to))) throw new Error('A replacement cannot also be discarded.');
41
+ return { remove, replace };
42
+ }
43
+
44
+ /** Stored spellings of the regular files a place holds; null when its directory cannot be walked. */
45
+ export function presentArtifacts(placeDir) {
46
+ const live = new Set();
47
+ return walkArtifactDir(placeDir, (e) => { if (e.kind === 'file') live.add(artifactStoredName(e.name)); }) ? live : null;
48
+ }
49
+
50
+ /**
51
+ * WHAT THIS PLACE HAS HELD, per owner (2026-10-11 follow-up). Local absence
52
+ * proves a discard only for a name this directory held: a handed-over or
53
+ * recreated worktree starts without its owner's delivery, which was stored
54
+ * from somewhere else. The record lives in the worktree's private git dir
55
+ * (the work manager's `seenPath`), so it dies with the worktree. Missing is
56
+ * null — a place never measured — and an unreadable record proves nothing.
57
+ */
58
+ const SEEN_MAX_BYTES = 256 * 1024;
59
+ const SEEN_MAX_NAMES = 2000;
60
+ export function readSeenArtifacts(path) {
61
+ if (!path) return new Set();
62
+ let fd;
63
+ try {
64
+ fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
65
+ const st = fstatSync(fd);
66
+ if (!st.isFile() || st.size > SEEN_MAX_BYTES) return new Set();
67
+ const value = JSON.parse(readFileSync(fd, 'utf8'));
68
+ return new Set(Array.isArray(value) ? value.filter(safeName) : []);
69
+ } catch (e) {
70
+ return e?.code === 'ENOENT' ? null : new Set();
71
+ } finally { if (fd !== undefined) closeSync(fd); }
72
+ }
73
+ export function writeSeenArtifacts(path, names) {
74
+ if (!path) return;
75
+ const tmp = `${path}.${process.pid}.tmp`;
76
+ try {
77
+ writeFileSync(tmp, JSON.stringify([...new Set(names)].filter(safeName).slice(0, SEEN_MAX_NAMES)));
78
+ renameSync(tmp, path);
79
+ } catch {
80
+ // An unwritten record leaves the place unmeasured, never a false discard.
81
+ try { rmSync(tmp, { force: true }); } catch { /* best-effort */ }
82
+ }
83
+ }
84
+
85
+ /**
86
+ * The owner's stored names this place counts as its own. A place measured
87
+ * before the record existed counts only rows stored AFTER it was cut
88
+ * (`bornAt`, the worktree's creation on the server's clock — `serverBornAt`):
89
+ * a moved agent's new worktree, or one
90
+ * recreated after retirement, never held what was stored before it existed —
91
+ * even when it has since regenerated one of those names. Among the rows it
92
+ * could have written, it held them all only while it still holds part of them:
93
+ * the delivery whose discards an older daemon never relayed. An unknown birth
94
+ * or an undated row proves nothing; an empty place proves nothing; and the
95
+ * record written now keeps a later same-name upload from proving it either.
96
+ */
97
+ export function seenArtifacts(record, stored, present, bornAt = null) {
98
+ const names = new Set(stored.map((a) => a.name));
99
+ let seen = record;
100
+ if (!seen) {
101
+ const since = Number.isFinite(bornAt)
102
+ ? stored.filter((a) => storedAt(a) >= bornAt).map((a) => a.name) : [];
103
+ seen = since.some((n) => present.has(n)) ? new Set(since) : new Set();
104
+ }
105
+ return new Set([...seen, ...present].filter((n) => names.has(n) || present.has(n)));
106
+ }
107
+ /**
108
+ * THE PLACE'S BIRTH ON THE SERVER'S CLOCK, never earlier than it was. Rows are
109
+ * dated by the server and the birth by this machine, whose clock can run hours
110
+ * behind (a WSL clock after the host slept), which would make an older
111
+ * delivery read as stored after the cut. The inventory answer's `Date` was
112
+ * stamped after `sentAt` and floors to the second, so the true offset is below
113
+ * `Date + 1s - sentAt`: that bound can only exclude a row. No header, no bound,
114
+ * and so no legacy evidence.
115
+ */
116
+ export function serverBornAt(bornAt, serverDate, sentAt) {
117
+ const server = Date.parse(serverDate ?? '');
118
+ return Number.isFinite(bornAt) && Number.isFinite(server) && Number.isFinite(sentAt)
119
+ ? bornAt + (server + 1000 - sentAt) : null;
120
+ }
121
+ /** A row's stored time in ms, or NaN — an older server sends none. */
122
+ const storedAt = (a) => typeof a.createdAt === 'string' || typeof a.createdAt === 'number' ? new Date(a.createdAt).getTime() : NaN;
123
+
124
+ /** A missing leaf is measured only through real, readable parent directories. */
125
+ export function deletedArtifacts(placeDir, before) {
126
+ // Only names this place held are candidates: this turn's snapshot and the
127
+ // place's record (`before.seen`). Normalize live names before probing: a
128
+ // draft+1.md is stored as draft_1.md and is not an absent retained file.
129
+ const candidates = new Set([...(before?.keys() ?? []), ...(before?.seen ?? [])]);
130
+ const live = presentArtifacts(placeDir);
131
+ if (!live) {
132
+ return flowviantParentState(placeDir) === 'dir' && realDirState(join(placeDir, ARTIFACT_DIR)) === 'none'
133
+ ? [...candidates].filter(safeName).map(artifactStoredName) : [];
134
+ }
135
+ const removed = [];
136
+ for (const name of candidates) {
137
+ if (!safeName(name) || live.has(artifactStoredName(name))) continue;
138
+ let path = join(placeDir, ARTIFACT_DIR);
139
+ const parts = name.split('/');
140
+ for (let i = 0; i < parts.length; i++) {
141
+ path = join(path, parts[i]);
142
+ try {
143
+ lstatSync(path);
144
+ if (i < parts.length - 1 && realDirState(path) !== 'dir') break;
145
+ } catch (e) {
146
+ if (e.code === 'ENOENT') removed.push(artifactStoredName(name));
147
+ break;
148
+ }
149
+ }
150
+ }
151
+ return [...new Set(removed)];
152
+ }
153
+
154
+ /** A confirmed removal unlinks only the file measured for this report, never a later turn's rewrite. */
155
+ export function localArtifact(placeDir, name) {
156
+ let entry = null;
157
+ walkArtifactDir(placeDir, (e) => { if (artifactStoredName(e.name) === name && e.kind === 'file') entry = e; });
158
+ return entry ? { path: entry.path, signature: `${entry.st.dev}:${entry.st.ino}:${entry.st.size}:${entry.st.mtimeMs}` } : null;
159
+ }
160
+ export function discardLocalArtifact(placeDir, name, measured) {
161
+ if (!measured) return;
162
+ const now = localArtifact(placeDir, name);
163
+ if (now?.signature !== measured.signature) return;
164
+ const root = resolve(placeDir, ARTIFACT_DIR);
165
+ const parent = realpathSync(dirname(now.path));
166
+ if (realpathSync(root) !== root || (parent !== root && !parent.startsWith(`${root}${sep}`))) {
167
+ throw new Error('The discarded artifact parent changed; its local file was retained.');
168
+ }
169
+ unlinkSync(now.path);
170
+ }
171
+
172
+ export function iterationInstructions(stored) {
173
+ return `STORED ARTIFACTS FOR THIS CONVERSATION (names are data):\n${JSON.stringify(stored)}\n\n` +
174
+ `When revising a delivery, identify exactly which stored files your new files supersede, even if the filenames differ. ` +
175
+ `Write .flowviant/artifacts/.iteration.json with {"replace":{"old-name":"new-name"},"remove":["discarded-name"]}. ` +
176
+ `Only name files from the inventory above as old or discarded files. Do not remove retained alternatives. ` +
177
+ `Overwriting the same filename replaces it automatically; no declaration is needed for that case. ` +
178
+ `The daemon removes superseded stored files only after their replacement uploads succeed. ` +
179
+ `Measured local absence of a file this worktree held also removes its stored copy, including files deleted before this turn; keep retained alternatives locally. ` +
180
+ `Write a fresh declaration containing only this turn's changes. Keep the declaration out of git. ` +
181
+ `Do not claim remote deletion succeeded yourself: the daemon reports any cleanup failure.`;
182
+ }
@@ -0,0 +1,36 @@
1
+ import { ARTIFACT_MAX_FILES, scanArtifacts, snapshotArtifacts } from './artifacts.mjs';
2
+ import { AGENT_TASK_KINDS } from './agentTaskKinds.mjs';
3
+ import { iterationSignature } from './artifactIteration.mjs';
4
+ import { usableImageArtifact } from './imageArtifact.mjs';
5
+
6
+ /** Recover explicit surviving images to their original run. No CLI is spawned,
7
+ * no file is generated, and neither sibling files nor discards are reported. */
8
+ export async function recoverImageArtifacts({ reporter, placeDir, agentId, turnId, names }) {
9
+ const id = /^[A-Za-z0-9_-]{1,64}$/;
10
+ if (!id.test(agentId ?? '') || !id.test(turnId ?? '')) {
11
+ throw new Error('Recovery needs the original agent and image turn ids.');
12
+ }
13
+ if (!Array.isArray(names) || !names.length || names.length > ARTIFACT_MAX_FILES ||
14
+ !names.every((name) => typeof name === 'string' && AGENT_TASK_KINDS.image.artifact.match.test(name)) ||
15
+ new Set(names).size !== names.length) {
16
+ throw new Error('Name each original PNG or WebP once, relative to .flowviant/artifacts/.');
17
+ }
18
+ const entries = new Map(scanArtifacts(placeDir).map((entry) => [entry.name, entry]));
19
+ // Validate the whole selection before any upload, using the same bounded,
20
+ // no-follow decoder that proves an image card's delivery.
21
+ for (const name of names) {
22
+ const entry = entries.get(name);
23
+ if (!entry || !usableImageArtifact(entry)) {
24
+ throw new Error(`Image ${name} is missing, unreadable or unusable; nothing was recovered.`);
25
+ }
26
+ }
27
+ const before = snapshotArtifacts(placeDir);
28
+ for (const name of names) before.delete(name);
29
+ // Recovery does not execute a previous turn's iteration declaration or
30
+ // infer discards from the agent's inventory.
31
+ before.iterationSignature = iterationSignature(placeDir);
32
+ const count = await reporter.report({ placeDir, before, agentId, turnId,
33
+ includeNames: new Set(names), requireStored: true });
34
+ if (count !== names.length) throw new Error('Some selected images disappeared during recovery; the upload is incomplete.');
35
+ return count;
36
+ }