flowviant 0.103.0 → 0.105.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 (141) hide show
  1. package/bin/cli.mjs +87 -474
  2. package/bin/lib/admission.mjs +19 -3
  3. package/bin/lib/agentPlan.mjs +2 -2
  4. package/bin/lib/agentTaskKinds.mjs +130 -0
  5. package/bin/lib/agySessions.mjs +158 -0
  6. package/bin/lib/artifactContracts.mjs +153 -0
  7. package/bin/lib/artifactPolicy.mjs +45 -0
  8. package/bin/lib/artifactPreview.mjs +52 -0
  9. package/bin/lib/artifactReporter.mjs +163 -0
  10. package/bin/lib/artifacts.mjs +49 -228
  11. package/bin/lib/authReject.mjs +16 -4
  12. package/bin/lib/authproxy.mjs +47 -304
  13. package/bin/lib/boxIdentity.mjs +245 -0
  14. package/bin/lib/childEnv.mjs +11 -9
  15. package/bin/lib/claude.mjs +14 -851
  16. package/bin/lib/claudeAuth.mjs +3 -3
  17. package/bin/lib/claudePosture.mjs +370 -0
  18. package/bin/lib/claudeSessions.mjs +411 -0
  19. package/bin/lib/claudeStream.mjs +207 -0
  20. package/bin/lib/cliLimit.mjs +34 -0
  21. package/bin/lib/commandSpecs.mjs +173 -0
  22. package/bin/lib/config.mjs +11 -42
  23. package/bin/lib/credentialDate.mjs +22 -0
  24. package/bin/lib/credentials.mjs +21 -26
  25. package/bin/lib/daemonLogging.mjs +55 -0
  26. package/bin/lib/daemonState.mjs +64 -0
  27. package/bin/lib/deploy.mjs +148 -647
  28. package/bin/lib/deployCheckout.mjs +265 -0
  29. package/bin/lib/deployConfig.mjs +142 -0
  30. package/bin/lib/deployRunner.mjs +311 -0
  31. package/bin/lib/deployWire.mjs +41 -0
  32. package/bin/lib/desktopContract.mjs +56 -105
  33. package/bin/lib/env.mjs +49 -484
  34. package/bin/lib/fleet.mjs +183 -2383
  35. package/bin/lib/fleetJobs.mjs +120 -0
  36. package/bin/lib/fleetPost.mjs +40 -0
  37. package/bin/lib/fleetReports.mjs +392 -0
  38. package/bin/lib/fleetRoster.mjs +470 -0
  39. package/bin/lib/fleetStartup.mjs +196 -0
  40. package/bin/lib/fleetWake.mjs +45 -0
  41. package/bin/lib/fleetWire.mjs +46 -0
  42. package/bin/lib/git.mjs +2 -2
  43. package/bin/lib/gitMergeIdentity.mjs +51 -0
  44. package/bin/lib/grant.mjs +5 -3
  45. package/bin/lib/help.mjs +3 -125
  46. package/bin/lib/holder.mjs +482 -0
  47. package/bin/lib/instance.mjs +124 -46
  48. package/bin/lib/knowledge.mjs +16 -799
  49. package/bin/lib/knowledgeFetch.mjs +38 -0
  50. package/bin/lib/knowledgeFileSync.mjs +112 -0
  51. package/bin/lib/knowledgeFiles.mjs +489 -0
  52. package/bin/lib/knowledgeLibrary.mjs +299 -0
  53. package/bin/lib/landed.mjs +2 -1
  54. package/bin/lib/listeners.mjs +99 -141
  55. package/bin/lib/localSessions.mjs +30 -555
  56. package/bin/lib/login.mjs +3 -2
  57. package/bin/lib/loginPath.mjs +3 -5
  58. package/bin/lib/machineDisconnect.mjs +203 -0
  59. package/bin/lib/machineEnv.mjs +22 -0
  60. package/bin/lib/machineLimits.mjs +61 -0
  61. package/bin/lib/machineListing.mjs +338 -0
  62. package/bin/lib/machines.mjs +78 -455
  63. package/bin/lib/machinesCommand.mjs +214 -0
  64. package/bin/lib/mcp-cli.mjs +2 -1
  65. package/bin/lib/patchRevert.mjs +60 -0
  66. package/bin/lib/prWorkflow.mjs +157 -0
  67. package/bin/lib/preflight.mjs +2 -1
  68. package/bin/lib/pressurePolicy.mjs +104 -0
  69. package/bin/lib/preview.mjs +2 -272
  70. package/bin/lib/previewFramePolicy.mjs +112 -0
  71. package/bin/lib/previewGatePolicy.mjs +236 -0
  72. package/bin/lib/previewRegistry.mjs +235 -0
  73. package/bin/lib/printable.mjs +2 -2
  74. package/bin/lib/procRegistry.mjs +28 -18
  75. package/bin/lib/procScan.mjs +146 -0
  76. package/bin/lib/processes.mjs +29 -37
  77. package/bin/lib/projectToolRuntimes.mjs +138 -0
  78. package/bin/lib/projectTools.mjs +13 -88
  79. package/bin/lib/prompts.mjs +105 -139
  80. package/bin/lib/repoPath.mjs +26 -0
  81. package/bin/lib/resources.mjs +7 -69
  82. package/bin/lib/resumeLost.mjs +107 -0
  83. package/bin/lib/runTurn.mjs +324 -0
  84. package/bin/lib/runtimeAntigravity.mjs +230 -0
  85. package/bin/lib/runtimeCapabilities.mjs +334 -0
  86. package/bin/lib/runtimeClaude.mjs +126 -0
  87. package/bin/lib/runtimeCodex.mjs +217 -0
  88. package/bin/lib/runtimeDetection.mjs +121 -0
  89. package/bin/lib/runtimeEvents.mjs +482 -0
  90. package/bin/lib/runtimeOfferings.mjs +4 -1
  91. package/bin/lib/runtimes.mjs +42 -1402
  92. package/bin/lib/safeFileName.mjs +74 -0
  93. package/bin/lib/standDownExit.mjs +181 -0
  94. package/bin/lib/standDownGate.mjs +48 -0
  95. package/bin/lib/startProject.mjs +279 -0
  96. package/bin/lib/trace.mjs +3 -3
  97. package/bin/lib/turnLock.mjs +68 -0
  98. package/bin/lib/turnProfile.mjs +77 -0
  99. package/bin/lib/uninstall.mjs +55 -45
  100. package/bin/lib/uninstallCopies.mjs +112 -0
  101. package/bin/lib/update.mjs +30 -17
  102. package/bin/lib/uplinkScrub.mjs +265 -0
  103. package/bin/lib/vault.mjs +22 -151
  104. package/bin/lib/vaultDiff.mjs +183 -0
  105. package/bin/lib/versionOrder.mjs +42 -0
  106. package/bin/lib/views.mjs +17 -17
  107. package/bin/lib/wikiRunner.mjs +630 -0
  108. package/bin/lib/work.mjs +188 -3034
  109. package/bin/lib/workAdoptCarry.mjs +94 -0
  110. package/bin/lib/workAgentCheck.mjs +278 -0
  111. package/bin/lib/workAgentMerges.mjs +54 -142
  112. package/bin/lib/workAgentPlans.mjs +14 -12
  113. package/bin/lib/workAgentPrecheck.mjs +393 -0
  114. package/bin/lib/workAgentPublish.mjs +177 -0
  115. package/bin/lib/workAgentReview.mjs +8 -666
  116. package/bin/lib/workAgentTurnBegun.mjs +109 -0
  117. package/bin/lib/workAgentTurnExecution.mjs +604 -0
  118. package/bin/lib/workAgentTurnOutcome.mjs +219 -0
  119. package/bin/lib/workAgentTurnReports.mjs +189 -0
  120. package/bin/lib/workAgentTurns.mjs +91 -898
  121. package/bin/lib/workAttachments.mjs +93 -0
  122. package/bin/lib/workBrain.mjs +53 -0
  123. package/bin/lib/workChildren.mjs +110 -0
  124. package/bin/lib/workCommandAudit.mjs +62 -0
  125. package/bin/lib/workDiffs.mjs +2 -1
  126. package/bin/lib/workNarration.mjs +152 -0
  127. package/bin/lib/workPlaceReports.mjs +426 -0
  128. package/bin/lib/workPlaces.mjs +228 -0
  129. package/bin/lib/workPreviews.mjs +31 -9
  130. package/bin/lib/workProcesses.mjs +4 -5
  131. package/bin/lib/workPullRequests.mjs +52 -126
  132. package/bin/lib/workReportQueue.mjs +254 -0
  133. package/bin/lib/workRetire.mjs +185 -0
  134. package/bin/lib/workSessionGroups.mjs +166 -0
  135. package/bin/lib/workSessionRuntime.mjs +164 -0
  136. package/bin/lib/workSessionTokens.mjs +68 -0
  137. package/bin/lib/workSessionTurns.mjs +892 -0
  138. package/bin/lib/workShip.mjs +9 -29
  139. package/bin/lib/workToolLog.mjs +76 -0
  140. package/package.json +3 -2
  141. package/bin/lib/patch.mjs +0 -363
@@ -80,7 +80,23 @@
80
80
  */
81
81
 
82
82
  import { MAX_CONCURRENT, MAX_CONCURRENT_FROM_ENV } from './config.mjs';
83
- import { pressureVerdict } from './resources.mjs';
83
+ import { measurePressure } from './resources.mjs';
84
+ import { pressureVerdict, readPressureThresholds } from './pressurePolicy.mjs';
85
+
86
+ /**
87
+ * THE PRESSURE HALF OF AN ADMISSION, AS THE DAEMON ASKS IT: this box's
88
+ * (briefly cached) measurement, judged against the thresholds the environment
89
+ * states AT THIS CALL. Both are read per decision — an operator who exports
90
+ * `FLOWVIANT_MIN_FREE_MB` into a running daemon's environment is heard on the
91
+ * next spawn. Injectable only so that promise can be pinned; `createAdmission`
92
+ * passes nothing. A guard switched off measures nothing (no `vm_stat` spawn
93
+ * per decision on macOS), as before the split.
94
+ */
95
+ export function livePressureVerdict(level, { measure = measurePressure, env = process.env } = {}) {
96
+ const thresholds = readPressureThresholds(env);
97
+ if (thresholds.off) return null;
98
+ return pressureVerdict(level, measure(), thresholds);
99
+ }
84
100
 
85
101
  /**
86
102
  * ── THE CEILING HAS THREE POSSIBLE AUTHORS (2026-09-17) ──
@@ -127,7 +143,7 @@ export function pickMaxTurns({ env = null, server = null, derived = 1 } = {}) {
127
143
  * A module holder rather than a value threaded through `createWorkManager`,
128
144
  * because the reader and the writer are two files apart and one poll apart: the
129
145
  * POLL learns it (fleet.mjs) and every ADMISSION reads it, including the ones
130
- * work.mjs takes in lanes fleet.mjs never sees. The alternative — passing it
146
+ * the work manager takes in lanes fleet.mjs never sees (workChildren.mjs builds it). The alternative — passing it
131
147
  * down — would mean the number that bound a spawn was whatever was current when
132
148
  * the manager was CONSTRUCTED, which is exactly the frozen-at-startup bug
133
149
  * `getBaseRef` and `getLeaseTtl` are getters to avoid.
@@ -171,7 +187,7 @@ export function createAdmission({
171
187
  * A number is still accepted, and every test here passes one — the whole
172
188
  * point of injecting it. */
173
189
  maxConcurrent = effectiveMaxTurns,
174
- verdict = pressureVerdict,
190
+ verdict = livePressureVerdict,
175
191
  } = {}) {
176
192
  /** Slots taken by a decision whose process does not exist yet. A Set of
177
193
  * tokens rather than a counter: a double release then costs nothing, which
@@ -27,7 +27,7 @@
27
27
 
28
28
  // THE UPLINK SCRUBBER, as the DEFAULT for the two readers whose every string
29
29
  // goes to the server (2026-09-24, the audit). See `parseTurnResult`.
30
- import { scrub as envScrub } from './env.mjs';
30
+ import { scrub as envScrub } from './uplinkScrub.mjs';
31
31
 
32
32
  /** The biggest a single proposal may be. Bounds on a machine, not a policy:
33
33
  * the server caps these again at its own boundary. */
@@ -277,7 +277,7 @@ function uniqueTempIds(list) {
277
277
  * summary and the wire, and a cap must never run ahead of it. So this is the
278
278
  * same absurdity bound `summary` takes one line up (8000): enough that nothing
279
279
  * a model writes reaches the caller unbounded, far enough above the 1000 the
280
- * wire cuts to that the scrub always sees whole values. `work.mjs` scrubs and
280
+ * wire cuts to that the scrub always sees whole values. `workAgentTurns.mjs` scrubs and
281
281
  * THEN cuts to 1000, and the server clamps again because the wire never trusts
282
282
  * a machine to have clamped.
283
283
  */
@@ -0,0 +1,130 @@
1
+ /**
2
+ * A CARD'S KIND — the daemon's one table of what each kind IS (0.97.0):
3
+ * `code` (the default), `design` and `research`; since 0.105.0 also `model`
4
+ * (the 3D-model kind — not an AI-model pin, which is `job.model`) and `deck`
5
+ * (the presentation kind — not the review deck). Every reader of a kind goes
6
+ * through this module: the prompts that print it and pick a contract
7
+ * (prompts.mjs, which keeps the strings, with the 3D-model and presentation
8
+ * contracts and the shared tail in artifactContracts.mjs), the agent lane
9
+ * that picks a posture,
10
+ * refuses a runtime (workAgentTurnExecution.mjs) and checks the artifact
11
+ * (workAgentTurnOutcome.mjs), and the turn runner that fences the non-code
12
+ * postures (runTurn.mjs and claudePosture.mjs, via turnProfile.mjs).
13
+ *
14
+ * Split out (SOLID F041) because the kind was branched on at five sites — the
15
+ * vocabulary twice in prompts.mjs, the posture and the artifact rule in the
16
+ * agent lane, the posture names again in claude.mjs — so a fourth kind was a
17
+ * coordinated edit whose missed site ran a card under the wrong posture. Now a
18
+ * kind is one entry here, and `agentTaskKinds.test.mjs` proves every entry has
19
+ * a contract, a posture a runtime can declare, and (for a non-code kind) the
20
+ * artifact that proves it delivered.
21
+ *
22
+ * Pure data and pure functions: no imports, no environment, no I/O — prompts
23
+ * imports it, and prompts is strings.
24
+ */
25
+
26
+ /**
27
+ * The kinds. Per kind:
28
+ * - `posture`: the turn profile the card runs under (turnProfile.mjs);
29
+ * - `artifact`: null for code (it delivers commits), else what proves a
30
+ * delivery — `match` over the artifact file name, an optional `companion`
31
+ * that must ALSO stand beside it (the 3D-model kind's model file: an HTML
32
+ * viewer alone is a page, not a model), and the `missing` sentence a turn
33
+ * settles `nothing` with when the proof was not written;
34
+ * - `offRuntime(rt)`: the sentence when the chosen CLI cannot run the posture.
35
+ */
36
+ /** Every non-code kind runs on Claude: the one CLI that declares their postures. */
37
+ const CLAUDE_ONLY = 'mockup, 3D model, presentation and write-up cards run on Claude on this machine';
38
+
39
+ export const AGENT_TASK_KINDS = Object.freeze({
40
+ code: Object.freeze({
41
+ posture: 'build',
42
+ artifact: null,
43
+ offRuntime: (rt) => `this machine cannot run ${rt}`,
44
+ }),
45
+ design: Object.freeze({
46
+ posture: 'design',
47
+ artifact: Object.freeze({
48
+ match: /\.html?$/i,
49
+ missing: 'the turn ended without writing a mockup under .flowviant/artifacts/',
50
+ }),
51
+ offRuntime: () => CLAUDE_ONLY,
52
+ }),
53
+ /**
54
+ * THE 3D-MODEL KIND (0.105.0): a viewable bundle — an HTML viewer and the
55
+ * model files beside it. Under the design fence (read the repo, write only
56
+ * `.flowviant/artifacts/**`): a model is a picture of an asset, and
57
+ * committing it is a separate code card a person raises at Accept.
58
+ */
59
+ model: Object.freeze({
60
+ posture: 'design',
61
+ artifact: Object.freeze({
62
+ match: /\.html?$/i,
63
+ companion: /\.(gltf|glb|obj)$/i,
64
+ missing:
65
+ 'the turn ended without writing a model bundle (an HTML viewer and a .gltf, .glb or .obj file) under .flowviant/artifacts/',
66
+ }),
67
+ offRuntime: () => CLAUDE_ONLY,
68
+ }),
69
+ /** THE PRESENTATION KIND (0.105.0): one HTML slide deck, under the design fence. */
70
+ deck: Object.freeze({
71
+ posture: 'design',
72
+ artifact: Object.freeze({
73
+ match: /\.html?$/i,
74
+ missing: 'the turn ended without writing a deck under .flowviant/artifacts/',
75
+ }),
76
+ offRuntime: () => CLAUDE_ONLY,
77
+ }),
78
+ research: Object.freeze({
79
+ posture: 'research',
80
+ artifact: Object.freeze({
81
+ match: /\.md$/i,
82
+ missing: 'the turn ended without writing a write-up under .flowviant/artifacts/',
83
+ }),
84
+ offRuntime: () => CLAUDE_ONLY,
85
+ }),
86
+ });
87
+
88
+ const known = (v) => typeof v === 'string' && Object.hasOwn(AGENT_TASK_KINDS, v);
89
+
90
+ /**
91
+ * A CARD'S KIND, READ THE ONE WAY THE DAEMON READS IT.
92
+ *
93
+ * The server sends the key only for non-code kinds, so ABSENT IS CODE, and an
94
+ * unknown string (a newer server, a typo) degrades to code: the one kind that
95
+ * changes nothing about how a card is PRINTED. Every printing reader — the
96
+ * contract selector, the kickoff, the spec — goes through this, so no two of
97
+ * them can disagree about what an odd value means.
98
+ */
99
+ export const agentTaskKindOf = (v) => (known(v) ? v : 'code');
100
+
101
+ /**
102
+ * …EXCEPT WHERE A TURN IS ABOUT TO SPAWN (2026-09-23). "Unknown degrades to
103
+ * code" is right for every reader that only PRINTS the kind, and wrong for the
104
+ * one that picks a POSTURE: code is the build turn, permissions skipped, so a
105
+ * kind a newer server invented for a card that must not touch the code (a
106
+ * fourth kind, a typo, `Design`) would have run as exactly the turn it exists
107
+ * to prevent. The agent lane asks this first and refuses in words, relaying
108
+ * the word it was handed. Absent, null and empty stay code — that is how a
109
+ * code card arrives.
110
+ *
111
+ * @returns {string|null} the unrecognised value, or null when the kind is one
112
+ * this daemon runs.
113
+ */
114
+ export const unknownAgentTaskKind = (v) =>
115
+ v === undefined || v === null || v === '' || known(v)
116
+ ? null
117
+ : String(v).replace(/[\u0000-\u001f\u007f]/g, '').slice(0, 40);
118
+
119
+ /** The table entry for a kind value, read the printing way (unknown → code). */
120
+ export const agentTaskKind = (v) => AGENT_TASK_KINDS[agentTaskKindOf(v)];
121
+
122
+ /** The postures only a card's kind selects (every kind's but code's), each
123
+ * once: the 3D-model and presentation kinds reuse the design posture. */
124
+ export const KIND_POSTURES = Object.freeze([
125
+ ...new Set(
126
+ Object.values(AGENT_TASK_KINDS)
127
+ .map((k) => k.posture)
128
+ .filter((p) => p !== 'build')
129
+ ),
130
+ ]);
@@ -0,0 +1,158 @@
1
+ /**
2
+ * ANTIGRAVITY (agy) SESSION PRESENCE (split out of localSessions.mjs
3
+ * 2026-09-26, SOLID F062). agy's SQLite store, its cwd cache and its
4
+ * process-name liveness change for reasons of their own, apart from Claude's
5
+ * transcript format (claudeSessions.mjs); the shared fence and report cap are
6
+ * the coordinator's (localSessions.mjs).
7
+ *
8
+ * NOTHING in this file throws.
9
+ */
10
+
11
+ import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
12
+ import { homedir } from 'node:os';
13
+ import { join } from 'node:path';
14
+ import { execFileSync } from 'node:child_process';
15
+ import { procPids, scanProcs } from './procScan.mjs';
16
+
17
+ // agy's store is nothing like Claude's: one SQLite db per conversation at
18
+ // ~/.gemini/antigravity-cli/conversations/<uuid>.db (global, not cwd-keyed),
19
+ // no per-pid liveness registry that survives contact (the presence/*.lock
20
+ // files sit untouched by real runs — measured), and the only cwd mapping is
21
+ // cache/last_conversations.json: {cwd → the LAST conversation run there}.
22
+ // So the honest agy report is a SUBSET — the last conversation per directory
23
+ // inside this repo — and that is exactly the one `agy --continue` would give
24
+ // the person at that keyboard, i.e. the one worth offering to adopt.
25
+
26
+ const AGY_DIR = () => join(homedir(), '.gemini', 'antigravity-cli');
27
+ const AGY_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
28
+
29
+ /** Newest write to the conversation's store — the wal carries recent turns,
30
+ * so its mtime (not the db's) is the real "last active" (measured: a resume
31
+ * touched db+wal, and never the presence lock). 0 = no such conversation. */
32
+ function agyLastWriteMs(id) {
33
+ if (!AGY_UUID_RE.test(id)) return 0;
34
+ let newest = 0;
35
+ for (const suffix of ['.db', '.db-wal']) {
36
+ try {
37
+ const t = statSync(join(AGY_DIR(), 'conversations', `${id}${suffix}`)).mtimeMs;
38
+ if (t > newest) newest = t;
39
+ } catch {
40
+ /* absent half is fine — the db alone still answers */
41
+ }
42
+ }
43
+ return newest;
44
+ }
45
+
46
+ /** Any agy process on the machine right now? /proc comm scan — cheap at the
47
+ * 60s cadence, and the only liveness signal agy leaves (locks are inert).
48
+ * Tri-state on purpose: true (an agy process exists), false (scanned /proc
49
+ * and found none), null (no /proc to scan — macOS — so the question is
50
+ * UNANSWERABLE here, which is a different fact from "no", and the adoption
51
+ * path below treats it differently: unknowable refuses, absent permits). */
52
+ function agyProcessAlive() {
53
+ if (!existsSync('/proc')) {
54
+ // No /proc (macOS): ask pgrep the same question. A MEASURED "none" here
55
+ // matters — answering null instead made every agy conversation on such a
56
+ // machine read live forever, which both invented a state ("live" when the
57
+ // truth was "unmeasured") and permanently killed agy adoption there.
58
+ try {
59
+ execFileSync('pgrep', ['-x', 'agy'], {
60
+ stdio: ['ignore', 'ignore', 'ignore'],
61
+ timeout: 5_000,
62
+ });
63
+ return true; // exit 0 — at least one agy process
64
+ } catch (e) {
65
+ if (e?.status === 1) return false; // pgrep ran and found none
66
+ return null; // pgrep itself unavailable/errored — genuinely unknowable
67
+ }
68
+ }
69
+ // The /proc walk is procScan's (every pid looked at; a bound of ONE row,
70
+ // because the first agy answers the question).
71
+ const pids = procPids();
72
+ if (pids === null) return null; // /proc exists but won't read — still unknowable
73
+ const isAgy = (name) => {
74
+ try {
75
+ return readFileSync(`/proc/${name}/comm`, 'utf8').trim() === 'agy' ? true : null;
76
+ } catch {
77
+ return null; // raced exit — keep scanning
78
+ }
79
+ };
80
+ return scanProcs(pids, isAgy, 1).rows.length > 0;
81
+ }
82
+
83
+ /**
84
+ * Is this agy conversation being driven RIGHT NOW? agy cannot answer
85
+ * per-conversation, so this is the conservative composite: an agy process
86
+ * exists AND this conversation's store was written in the last 10 minutes.
87
+ * Adoption is a MOVE for agy (no fork exists — measured, "trajectory not
88
+ * found" on a renamed copy), so refusing a maybe-live conversation for a few
89
+ * minutes costs a retry; adopting an actually-live one puts two drivers on
90
+ * one store.
91
+ */
92
+ const AGY_LIVE_WINDOW_MS = 10 * 60 * 1000;
93
+ export function isAgyConversationLive(id, { processAlive = agyProcessAlive } = {}) {
94
+ try {
95
+ const up = processAlive();
96
+ // Unknowable is NOT "ended". Without /proc the process half of the
97
+ // composite cannot be measured, and calling that "no process" would make
98
+ // every agy conversation on such a machine adoptable — the move-adoption
99
+ // then puts a second driver on a store someone may still be writing.
100
+ // "Live" here means "refuse adoption", and refusing what we cannot verify
101
+ // is the same conservatism as the composite itself.
102
+ if (up === null) return true;
103
+ if (!up) return false;
104
+ const t = agyLastWriteMs(id);
105
+ return t > 0 && Date.now() - t < AGY_LIVE_WINDOW_MS;
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+
111
+ /** The repo's agy conversations, via the cwd registry — see the section
112
+ * comment for why this is deliberately a subset. The coordinator hands over
113
+ * the fence (`ours`, `mine`) and the ended-session `cutoff`; `processAlive`
114
+ * measures liveness (true / false / null = unknowable), asked once and only
115
+ * when there is a registry to read. */
116
+ export function scanAgyConversations({ ours, mine, cutoff, processAlive = agyProcessAlive }) {
117
+ const out = [];
118
+ try {
119
+ const raw = readFileSync(join(AGY_DIR(), 'cache', 'last_conversations.json'), 'utf8');
120
+ const map = JSON.parse(raw);
121
+ if (!map || typeof map !== 'object') return out;
122
+ const processUp = processAlive();
123
+ for (const [cwd, id] of Object.entries(map)) {
124
+ if (typeof id !== 'string' || !AGY_UUID_RE.test(id)) continue;
125
+ // The operator's own agy TAB, whose conversation is registered against
126
+ // the checkout exactly as a terminal one would be.
127
+ if (mine.has(id)) continue;
128
+ let real;
129
+ try {
130
+ real = realpathSync(cwd);
131
+ } catch {
132
+ continue; // the directory is gone — nothing to point a tab at
133
+ }
134
+ if (!ours(real)) continue;
135
+ const lastMs = agyLastWriteMs(id);
136
+ if (!lastMs || lastMs < cutoff) continue;
137
+ out.push({
138
+ id,
139
+ cwd: real,
140
+ // Mirrors isAgyConversationLive exactly, unknowable (null) included:
141
+ // the report must never offer as adoptable a conversation the adopt
142
+ // path will then refuse — an offer wired to a refusal is the dead
143
+ // control this product keeps deleting.
144
+ live: processUp === null ? true : processUp && Date.now() - lastMs < AGY_LIVE_WINDOW_MS,
145
+ lastActiveAt: new Date(lastMs).toISOString(),
146
+ runtime: 'antigravity',
147
+ });
148
+ }
149
+ out.sort(
150
+ (a, b) =>
151
+ (b.lastActiveAt < a.lastActiveAt ? -1 : b.lastActiveAt > a.lastActiveAt ? 1 : 0) ||
152
+ (a.id < b.id ? -1 : 1)
153
+ );
154
+ } catch {
155
+ /* no agy on this machine, or an unreadable registry — nothing to report */
156
+ }
157
+ return out;
158
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * THE 3D-MODEL AND PRESENTATION CARDS' CONTRACTS (2026-09-27, 0.105.0).
3
+ *
4
+ * Two more kinds that hand back a file rather than a change, beside the design
5
+ * and research contracts in prompts.mjs (which is past its size and keeps
6
+ * those two). `prompts.mjs` imports these into `AGENT_CONTRACTS`; the kind
7
+ * table (agentTaskKinds.mjs) runs both under the `design` posture — read the
8
+ * repo, write only `.flowviant/artifacts/**`, no web, no shell beyond `ls` —
9
+ * so "change no repository file" is enforced, not asked.
10
+ *
11
+ * THE SAME BONES AS THE DESIGN CONTRACT, deliberately: its header, its rule 3
12
+ * (commit nothing), its rule 4 (stop and ask) and its final JSON — one shape
13
+ * `parseTurnResult`, the settle, the board and the review deck already read.
14
+ * The tail is spelled ONCE, in `ARTIFACT_CONTRACT_TAIL`, and every
15
+ * file-handing contract renders it — these two, and the design and research
16
+ * contracts in prompts.mjs (whose bytes `taskKind.test.mjs` pins unchanged by
17
+ * the move), so no two of them can drift apart.
18
+ *
19
+ * "model" here is the 3D-model KIND (not an AI-model pin, `job.model`), and
20
+ * "deck" the presentation kind (not the review deck).
21
+ *
22
+ * Pure strings: no imports, no environment, no I/O.
23
+ */
24
+
25
+ /**
26
+ * Rule 3, rule 4 and the final JSON a file-handing contract ends with.
27
+ * `ask` is rule 4's body after its number (the decision only a person can
28
+ * make); `summary` is the delivered shape's summary gloss.
29
+ */
30
+ export const ARTIFACT_CONTRACT_TAIL = ({ ask, summary }) => `3. CHANGE NO REPOSITORY FILE AND COMMIT NOTHING. You can only write under
31
+ .flowviant/artifacts/, and the daemon keeps that directory out of git.
32
+
33
+ 4. ${ask}
34
+
35
+ END YOUR TURN WITH ONE JSON OBJECT AND NOTHING AFTER IT, in a \`\`\`json fence:
36
+
37
+ \`\`\`json
38
+ {
39
+ "status": "delivered",
40
+ "summary": "${summary}",
41
+ "progress": "two or three sentences on what you have done in this run SO FAR, across every card"
42
+ }
43
+ \`\`\`
44
+
45
+ or, if you are stopping to ask:
46
+
47
+ \`\`\`json
48
+ {
49
+ "status": "blocked",
50
+ "progress": "two or three sentences on what you have done in this run SO FAR, across every card",
51
+ "question": "the specific thing you need decided, in one or two sentences"
52
+ }
53
+ \`\`\`
54
+
55
+ "progress" is REQUIRED on both shapes and REPLACES the previous one whole —
56
+ past tense, plain sentences, no card ids.`;
57
+
58
+ /**
59
+ * THE 3D-MODEL CARD. The product is a model a person can turn around in
60
+ * Review: one folder holding a three.js viewer and the model files it loads
61
+ * by relative URL — the bundle shape the artifact scan keeps (relative paths
62
+ * to depth four, 0.99.0) and the library keeps as one versioned folder. The
63
+ * daemon calls it delivered only when an `.html` AND a `.gltf`/`.glb`/`.obj`
64
+ * stand under `.flowviant/artifacts/` (agentTaskKinds.mjs).
65
+ *
66
+ * TEXT, BECAUSE TEXT IS ALL THIS POSTURE WRITES (2026-09-27, a call the owner
67
+ * may overrule). The kind runs under the `design` posture: Read, `ls`, and
68
+ * `Edit(.flowviant/artifacts/**)` — Claude's Write/Edit tools write text, and
69
+ * there is no shell that could produce bytes. The spec's first wording asked
70
+ * for glTF with a `.bin`, or a `.glb`, and `.png`/`.jpg` textures: outputs
71
+ * this card cannot make. So the contract asks for OBJ + `.mtl` (text, fetched
72
+ * by relative URL from the artifact host, which the viewer's `connect-src`
73
+ * admits) and SVG or procedural textures (`img-src` admits the host). NOT a
74
+ * `.gltf` with its buffer as a `data:` URI: three.js fetches that buffer, and
75
+ * the artifact CSP's `connect-src` names only the artifact host
76
+ * (apps/api/src/lib/artifactDelivery.ts), so the review viewer would refuse
77
+ * it. Binary files are still accepted — the artifact policy, the proof, the
78
+ * library and the 20 MB cap all keep them — for a folder that already holds
79
+ * one; widening the posture to write them is the owner's decision.
80
+ */
81
+ export const SYSTEM_AGENT_MODEL = `You are the human's own Claude, working one 3D MODEL card in a git worktree of
82
+ their repository. Nobody is watching this run. You have the repo and nothing
83
+ else — no project tools, no board, no chat.
84
+
85
+ A 3D MODEL CARD HANDS BACK A MODEL YOU CAN LOOK AT, NOT A CHANGE. Nothing you
86
+ do here edits the product: the person turns the model around, and decides.
87
+
88
+ WHAT TO DO:
89
+
90
+ 1. Read the repository first — the existing 3D assets and the scene they sit
91
+ in: their scale and units, their up axis, their style, their materials and
92
+ texture sizes. The model must fit THIS project, not a stock asset library.
93
+
94
+ 2. Write ONE folder, .flowviant/artifacts/<kebab-name>/ — a short kebab-case
95
+ name for what it is (for example .flowviant/artifacts/reading-chair/) —
96
+ holding:
97
+ - index.html, a viewer: three.js loaded from cdn.jsdelivr.net/npm or
98
+ unpkg.com, with orbit controls, loading the model by a RELATIVE URL
99
+ (./reading-chair.obj — never a path outside the folder);
100
+ - the model, written as TEXT: a .obj with its .mtl, each loaded by a
101
+ relative URL;
102
+ - its textures as .svg files referenced by relative paths, or drawn by
103
+ the viewer itself (procedural materials, canvas textures).
104
+ You can write only text files here. A .glb, .bin, .png or .jpg is kept
105
+ and shown if one already stands in the folder, but this card cannot make
106
+ one — never plan on one. Nothing else may load from the network, and
107
+ nothing can be sent. Keep every file under 2 MB — the viewer, the .obj,
108
+ the .mtl, each texture.
109
+
110
+ ${ARTIFACT_CONTRACT_TAIL({
111
+ ask: `If you cannot model it without a DECISION only a person can make — which of
112
+ two directions, what size or style, which asset it stands beside — STOP AND
113
+ ASK. Do not guess. A question costs one reply; a guessed model costs a review.`,
114
+ summary: 'one or two sentences on what the model shows, and the folder it is in',
115
+ })}`;
116
+
117
+ /**
118
+ * THE PRESENTATION CARD. One self-contained HTML deck under the design
119
+ * card's inline and CDN rules, navigable by keyboard and on screen, and
120
+ * printable one slide per page — so "send it as a PDF" is the browser's
121
+ * Print, never a converter this machine would have to run.
122
+ */
123
+ export const SYSTEM_AGENT_DECK = `You are the human's own Claude, working one PRESENTATION card in a git worktree
124
+ of their repository. Nobody is watching this run. You have the repo and nothing
125
+ else — no project tools, no board, no chat.
126
+
127
+ A PRESENTATION CARD HANDS BACK A DECK, NOT A CHANGE. Nothing you do here edits
128
+ the product: the person pages through it, and decides.
129
+
130
+ WHAT TO DO:
131
+
132
+ 1. Read what the deck is about first — the repository, the real product, its
133
+ brand and its own words. The deck must sound and look like THIS project,
134
+ not like a template.
135
+
136
+ 2. Write ONE self-contained HTML file directly in .flowviant/artifacts/ — a
137
+ short kebab-case name for the talk (for example
138
+ .flowviant/artifacts/q3-roadmap.html). Inline CSS (a Google Fonts
139
+ stylesheet may load). Scripts may be inline or loaded from
140
+ cdnjs.cloudflare.com, cdn.jsdelivr.net/npm or unpkg.com, and from nowhere
141
+ else; nothing else may load from the network and nothing can be sent, so
142
+ images are data: URIs or inline SVG. Each slide is a <section>; the arrow
143
+ keys and on-screen previous/next buttons move between them, and a counter
144
+ says which slide of how many is showing. Add @media print rules that put
145
+ one slide on each page (break-after: page), so the browser prints it to a
146
+ PDF. Keep it under 2 MB.
147
+
148
+ ${ARTIFACT_CONTRACT_TAIL({
149
+ ask: `If you cannot write it without a DECISION only a person can make — who the
150
+ audience is, how long it runs, what it must argue — STOP AND ASK. Do not
151
+ guess. A question costs one reply; a guessed deck costs a review.`,
152
+ summary: 'one or two sentences on what the deck covers, and the file it is in',
153
+ })}`;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * THE ARTIFACT POLICY — GENERATED, DO NOT EDIT BY HAND.
3
+ *
4
+ * Rendered from the server's packages/shared/src/schemas/artifact.schema.ts
5
+ * by flowviant's packages/shared/src/schemas/daemonArtifactPolicy.ts
6
+ * (`bun scripts/write-daemon-artifact-policy.ts` in the app repo). The server
7
+ * is the release source; its parity test fails until this file matches it.
8
+ * artifacts.mjs reads the allowlist and the caps from here, and the server
9
+ * still verifies every upload on its own.
10
+ */
11
+
12
+ export const ARTIFACT_POLICY_SOURCE = 'packages/shared/src/schemas/artifact.schema.ts';
13
+
14
+ /** Per text file and ordinary binary artifact. */
15
+ export const ARTIFACT_MAX_BYTES = 2097152;
16
+ /** Binary model buffers (the extensions below). */
17
+ export const ARTIFACT_BINARY_MODEL_MAX_BYTES = 20971520;
18
+ export const ARTIFACT_BINARY_MODEL_EXTS = ['glb', 'bin'];
19
+ /** Per session and per agent — the server keeps as many per owner. */
20
+ export const ARTIFACT_MAX_PER_OWNER = 40;
21
+
22
+ /** Extension → the one mime the server serves it under; `text` is scrubbed. */
23
+ export const ARTIFACT_TYPES = {
24
+ html: { mime: 'text/html', text: true },
25
+ htm: { mime: 'text/html', text: true },
26
+ md: { mime: 'text/markdown', text: true },
27
+ svg: { mime: 'image/svg+xml', text: true },
28
+ png: { mime: 'image/png', text: false },
29
+ jpg: { mime: 'image/jpeg', text: false },
30
+ jpeg: { mime: 'image/jpeg', text: false },
31
+ gif: { mime: 'image/gif', text: false },
32
+ webp: { mime: 'image/webp', text: false },
33
+ json: { mime: 'application/json', text: true },
34
+ csv: { mime: 'text/csv', text: true },
35
+ txt: { mime: 'text/plain', text: true },
36
+ docx: { mime: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', text: false },
37
+ pptx: { mime: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', text: false },
38
+ xlsx: { mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', text: false },
39
+ pdf: { mime: 'application/pdf', text: false },
40
+ gltf: { mime: 'model/gltf+json', text: true },
41
+ obj: { mime: 'model/obj', text: true },
42
+ mtl: { mime: 'model/mtl', text: true },
43
+ glb: { mime: 'model/gltf-binary', text: false },
44
+ bin: { mime: 'application/octet-stream', text: false },
45
+ };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * THE DESIGN PREVIEW — a screenshot of an HTML artifact, taken in a browser
3
+ * (2026-09-26, split out of artifacts.mjs — SOLID F051).
4
+ *
5
+ * Browser rendering is its own reason to change: a Chrome flag, a CSP tweak or
6
+ * a viewport size has nothing to do with how the artifact directory is scanned
7
+ * or how an upload is retried. The reporter (`artifactReporter.mjs`) is handed
8
+ * this function rather than importing it, so a test can stand in a renderer
9
+ * that never launches a browser.
10
+ *
11
+ * It renders the SCRUBBED bytes, never the checkout's source, under the
12
+ * artifact box's own network fence (`PREVIEW_CSP`) placed before any
13
+ * model-written markup executes. A failed measurement is explicit
14
+ * (`renderState: 'unavailable'`); a daemon older than this simply sends no
15
+ * `renderState`, which the server reads as unmeasured.
16
+ */
17
+
18
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
19
+ import { join } from 'node:path';
20
+ import { tmpdir } from 'node:os';
21
+ import { pathToFileURL } from 'node:url';
22
+ import { captureScreenshot } from './shot.mjs';
23
+
24
+ export const DESIGN_PREVIEW_MAX_BYTES = 2 * 1024 * 1024;
25
+ const PREVIEW_CSP = "default-src 'none'; script-src 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net/npm/ https://unpkg.com; style-src 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net/npm/ https://unpkg.com https://fonts.googleapis.com; img-src data: blob:; font-src data: https://fonts.gstatic.com; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'";
26
+
27
+ /** Render the scrubbed HTML, never the checkout's unsanitized source. A
28
+ * failed browser measurement is explicit; older daemon reports are absent. */
29
+ export async function renderDesignPreview(bytes, capture = captureScreenshot) {
30
+ const dir = mkdtempSync(join(tmpdir(), 'flowviant-design-'));
31
+ try {
32
+ const html = join(dir, 'design.html');
33
+ const png = join(dir, 'design.png');
34
+ // A fresh Chrome profile has no Flowviant credentials. Put the artifact
35
+ // box's network fence before any model-written markup executes.
36
+ writeFileSync(html, Buffer.concat([
37
+ Buffer.from(`<meta http-equiv="Content-Security-Policy" content="${PREVIEW_CSP}">`),
38
+ bytes,
39
+ ]));
40
+ const result = await capture({ url: pathToFileURL(html).href, out: png, width: 720, height: 450, timeoutMs: 15_000 });
41
+ if (!result.ok) return { renderState: 'unavailable' };
42
+ const image = readFileSync(png);
43
+ if (image.byteLength > DESIGN_PREVIEW_MAX_BYTES || !image.subarray(0, 8).equals(Buffer.from([137,80,78,71,13,10,26,10]))) {
44
+ return { renderState: 'unavailable' };
45
+ }
46
+ return { renderState: 'rendered', preview: image };
47
+ } catch {
48
+ return { renderState: 'unavailable' };
49
+ } finally {
50
+ rmSync(dir, { recursive: true, force: true });
51
+ }
52
+ }