flowviant 0.105.0 → 0.107.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.
@@ -37,6 +37,7 @@
37
37
  */
38
38
 
39
39
  import { appendFileSync, readFileSync } from 'node:fs';
40
+ import { CARD_THREAD_RENDER_MAX } from './cardThread.mjs';
40
41
 
41
42
  /**
42
43
  * How many card specs one agent may accumulate.
@@ -49,8 +50,14 @@ import { appendFileSync, readFileSync } from 'node:fs';
49
50
  export const MAX_STASHED_CARDS = 40;
50
51
  /** The most one spec may contribute. A brief is written by whoever filed the
51
52
  * card and the server caps it, but this file is composed into a prompt and a
52
- * bound it owns is a bound that cannot be argued away upstream. */
53
- export const MAX_SPEC_CHARS = 8_000;
53
+ * bound it owns is a bound that cannot be argued away upstream.
54
+ *
55
+ * 8,000 is the record's brief ceiling; since 0.106.0 the spec also carries
56
+ * the card's discussion, so the bound grows by the most a turn's discussion
57
+ * can render to (`CARD_THREAD_RENDER_MAX`). The discussion is printed LAST,
58
+ * and a stash that cut it would hand the pre-review a thread missing its
59
+ * newest words — the ones the agent read most recently. */
60
+ export const MAX_SPEC_CHARS = 8_000 + CARD_THREAD_RENDER_MAX;
54
61
 
55
62
  /**
56
63
  * Write down the spec this turn is about to hand the agent.
@@ -27,10 +27,8 @@
27
27
  * The kinds. Per kind:
28
28
  * - `posture`: the turn profile the card runs under (turnProfile.mjs);
29
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;
30
+ * delivery — `match` over the artifact file name, and the `missing`
31
+ * sentence a turn settles `nothing` with when the proof was not written;
34
32
  * - `offRuntime(rt)`: the sentence when the chosen CLI cannot run the posture.
35
33
  */
36
34
  /** Every non-code kind runs on Claude: the one CLI that declares their postures. */
@@ -51,18 +49,19 @@ export const AGENT_TASK_KINDS = Object.freeze({
51
49
  offRuntime: () => CLAUDE_ONLY,
52
50
  }),
53
51
  /**
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.
52
+ * THE 3D-MODEL KIND (0.105.0; one page since 0.107.0): a three.js page that
53
+ * builds the model in code, which the person exports to one .glb in Review.
54
+ * Under the design fence (read the repo, write only `.flowviant/artifacts/**`):
55
+ * a model is a picture of an asset, and committing it is a separate code
56
+ * card a person raises at Accept. Proven by its page alone — the model is
57
+ * IN the page; 0.105.0's second proof (a .gltf/.glb/.obj beside it) went
58
+ * with the files it asked for.
58
59
  */
59
60
  model: Object.freeze({
60
61
  posture: 'design',
61
62
  artifact: Object.freeze({
62
63
  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/',
64
+ missing: 'the turn ended without writing a 3D model page under .flowviant/artifacts/',
66
65
  }),
67
66
  offRuntime: () => CLAUDE_ONLY,
68
67
  }),
@@ -0,0 +1,135 @@
1
+ /**
2
+ * A CARD'S TYPE — the daemon's one table of what each type ASKS (0.106.0).
3
+ *
4
+ * A type is the kind of WORK a card asks for — a Bug fix, a Refactor, a Code
5
+ * review — a flavour over the card's KIND (agentTaskKinds.mjs), which stays the
6
+ * end product and the posture the turn runs under. Every type runs as exactly
7
+ * one kind (`kind` below, the app's `TASK_TYPE_KIND`), and adds the one thing
8
+ * the kind cannot say: a bug fix reproduces first, a refactor must not change
9
+ * behaviour, a code review changes nothing.
10
+ *
11
+ * The server sends the id — `agentTurnJobs[].task.taskType` and
12
+ * `agentPlanJobs[].tasks[].taskType` — and only to a daemon at 0.106.0 (below
13
+ * it the server parks a typed card's turn rather than hand it to a daemon that
14
+ * would drop the key). THE DAEMON OWNS THE WORDS: each type's `contract` is
15
+ * the one sentence an agent is held to, printed inside THE CARD right after
16
+ * the kind line (`AGENT_TASK_SPEC`), so the spec stashed for the pre-review
17
+ * carries it too. Each sentence is true to what its kind's posture can do: a
18
+ * code type may build and test; a write-up type reads and writes one Markdown
19
+ * file; a mockup type writes under .flowviant/artifacts/ and nothing else.
20
+ *
21
+ * `label` is the app's `TASK_TYPE_LABEL`, the word a person picked. The ids
22
+ * (in the app's picker order), their kinds and their labels are held to the
23
+ * app's `taskType.ts` by the release gate (`scripts/check-app-parity.mjs`,
24
+ * rule 6): a type the server sends that this table lacks is printed as
25
+ * nothing, and one it has that the server never sends is dead.
26
+ *
27
+ * Pure data and pure functions; its one import is the kind table's reader.
28
+ */
29
+
30
+ import { agentTaskKindOf } from './agentTaskKinds.mjs';
31
+
32
+ export const AGENT_TASK_TYPES = Object.freeze({
33
+ feature: Object.freeze({
34
+ label: 'Feature',
35
+ kind: 'code',
36
+ contract: 'new behaviour: build it, and cover it with tests where the repository has them.',
37
+ }),
38
+ fix: Object.freeze({
39
+ label: 'Bug fix',
40
+ kind: 'code',
41
+ contract:
42
+ 'reproduce it first, with a failing test where the repository has tests, then make that test pass.',
43
+ }),
44
+ tests: Object.freeze({
45
+ label: 'Tests',
46
+ kind: 'code',
47
+ contract:
48
+ 'add tests for code that already exists and change no production code; if a test exposes a bug, ' +
49
+ 'say so in your summary instead of fixing it.',
50
+ }),
51
+ refactor: Object.freeze({
52
+ label: 'Refactor',
53
+ kind: 'code',
54
+ contract: 'behaviour must not change; if an existing test would have to change, stop and ask.',
55
+ }),
56
+ infra: Object.freeze({
57
+ label: 'Infra & deploy',
58
+ kind: 'code',
59
+ contract: 'change the configuration in the repository; never run a deploy or touch a live environment.',
60
+ }),
61
+ automation: Object.freeze({
62
+ label: 'Automation',
63
+ kind: 'code',
64
+ contract:
65
+ "add the scheduled job to the repository (a CI schedule, or a script and its schedule) so it runs " +
66
+ "where the repository's CI runs; do not run it.",
67
+ }),
68
+ review: Object.freeze({
69
+ label: 'Code review',
70
+ kind: 'research',
71
+ contract:
72
+ 'read the code the card names and write up what is wrong, by file and line, most serious first; ' +
73
+ 'change nothing.',
74
+ }),
75
+ ui: Object.freeze({
76
+ label: 'UI design',
77
+ kind: 'design',
78
+ contract: "draw the screens and flows the card names as the mockup page, in this product's own components and words.",
79
+ }),
80
+ prototype: Object.freeze({
81
+ label: 'Prototype',
82
+ kind: 'design',
83
+ contract: 'make every control in the flow work, with realistic sample data, in the one page.',
84
+ }),
85
+ slides: Object.freeze({
86
+ label: 'Slides',
87
+ kind: 'deck',
88
+ contract: 'one deck for the audience and the length the card asks for, one point to a slide.',
89
+ }),
90
+ model3d: Object.freeze({
91
+ label: '3D model',
92
+ kind: 'model',
93
+ contract:
94
+ 'model the object the card describes in the one three.js page, at the scale and in the units of the ' +
95
+ 'target the card names, else of any 3D assets the repository already has.',
96
+ }),
97
+ vector: Object.freeze({
98
+ label: 'Icons & vector',
99
+ kind: 'design',
100
+ contract:
101
+ 'write each icon as its own .svg file directly in .flowviant/artifacts/, and make the mockup page ' +
102
+ 'the sheet that shows them all, inline, at the sizes they are used.',
103
+ }),
104
+ research: Object.freeze({
105
+ label: 'Research',
106
+ kind: 'research',
107
+ contract: 'answer the question the card asks, leading with the answer and citing what you read.',
108
+ }),
109
+ writing: Object.freeze({
110
+ label: 'Writing',
111
+ kind: 'research',
112
+ contract: 'write the document the card asks for, for the reader it names, in words that reader uses.',
113
+ }),
114
+ });
115
+
116
+ /**
117
+ * A CARD'S TYPE, READ THE ONE WAY THE DAEMON READS IT — a type this table
118
+ * knows, else null. ABSENT IS NO TYPE (every card before 0.106.0, and every
119
+ * job from an older server), and an unknown id (a newer server, a typo) is
120
+ * dropped rather than guessed at: a type only adds a sentence, so dropping one
121
+ * leaves the card running on its kind, which is what it always did.
122
+ */
123
+ export const agentTaskTypeOf = (v) => (typeof v === 'string' && Object.hasOwn(AGENT_TASK_TYPES, v) ? v : null);
124
+
125
+ /**
126
+ * The type of a card AS IT WILL BE PRINTED — null unless the card's type is
127
+ * known here AND runs as the card's own kind. The server sends a type only on
128
+ * its kind, but the card is a document any editor writes; a Bug fix sentence
129
+ * ("with a failing test") under a mockup's posture is a contract the turn
130
+ * cannot keep, so a type on another kind is printed as nothing.
131
+ */
132
+ export const agentTaskTypeFor = (task) => {
133
+ const type = agentTaskTypeOf(task?.taskType);
134
+ return type && AGENT_TASK_TYPES[type].kind === agentTaskKindOf(task?.taskKind) ? type : null;
135
+ };
@@ -56,62 +56,84 @@ or, if you are stopping to ask:
56
56
  past tense, plain sentences, no card ids.`;
57
57
 
58
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).
59
+ * THE 3D-MODEL CARD (rewritten 0.107.0, the owner, 2026-09-27: "yeah just
60
+ * glb"). The product is ONE .glb a person downloads and imports; the card
61
+ * hands back ONE three.js page that builds the model in code and shows it —
62
+ * how Claude makes a model a person can look at anywhere else — and Flowviant
63
+ * exports that page's model group to a .glb with three.js's own GLTFExporter
64
+ * when the person presses Download .glb in Review (apps/web
65
+ * `workbench/glbExport.ts`, the other half of the handshake below).
65
66
  *
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.
67
+ * WHY NOT A MODEL FILE ON DISK. This kind runs under the `design` posture:
68
+ * Read, `ls`, and `Edit(.flowviant/artifacts/**)` — text only, no shell. A
69
+ * .glb is binary; a .gltf's buffer is base64 floats nobody writes by hand.
70
+ * The 0.105.0 contract asked for OBJ + .mtl + SVG textures instead, and a
71
+ * katana came back as seven files, textures no game engine imports and a
72
+ * generator script (2026-09-27). Built in code, drawn textures are canvases,
73
+ * and the exporter embeds them in the .glb as PNGs.
74
+ *
75
+ * THE HANDSHAKE IS SPELLED WORD FOR WORD because the web keys on it: the page
76
+ * announces `glb-ready` once its model stands, and answers `export-glb` with
77
+ * the binary. The page runs in a scripts-only sandbox with an opaque origin,
78
+ * so it posts to `parent` with `'*'`; the app accepts only its own frame's
79
+ * messages. A page with no handshake (every 0.105.0 bundle) is still viewed;
80
+ * it is just never offered a Download .glb.
80
81
  */
81
82
  export const SYSTEM_AGENT_MODEL = `You are the human's own Claude, working one 3D MODEL card in a git worktree of
82
83
  their repository. Nobody is watching this run. You have the repo and nothing
83
84
  else — no project tools, no board, no chat.
84
85
 
85
86
  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
+ do here edits the product: the person turns the model around, downloads it as
88
+ one .glb, and decides.
87
89
 
88
90
  WHAT TO DO:
89
91
 
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.
92
+ 1. Read the card and the repository first. If the card names where the model
93
+ will be used — a game engine, a platform, a printer — meet that target's
94
+ import rules: its scale and units, its up axis, its triangle and texture
95
+ limits. Otherwise match the scale and units of any 3D assets the repository
96
+ already has. Say in your summary what you assumed.
97
+
98
+ 2. Write ONE file, .flowviant/artifacts/<kebab-name>/index.html — a short
99
+ kebab-case name for what it is (for example
100
+ .flowviant/artifacts/reading-chair/index.html). Nothing else goes in that
101
+ folder: no model files, no textures, no scripts, no notes. The page BUILDS
102
+ THE MODEL IN CODE and shows it:
103
+ - three.js as ES modules from cdn.jsdelivr.net/npm, through an import map
104
+ for "three" and "three/addons/"; orbit controls, lights and a neutral
105
+ ground. Nothing else loads from the network, and nothing can be sent.
106
+ - the model is ONE THREE.Group named for what it is, built from three.js
107
+ geometry (lathe, extrude, tube, box and the rest, one mesh per part),
108
+ with MeshStandardMaterial only. Colour, roughness and metalness are
109
+ material values, or textures you DRAW ON A CANVAS in the page
110
+ (THREE.CanvasTexture, 1024px or less) — never an image file.
111
+ - the ground, a grid, the lights, a figure for scale and every other
112
+ helper stay OUTSIDE that group: they are the viewer's, not the model.
113
+ - no download or save button of your own: the page cannot download
114
+ anything. Flowviant's Download .glb is the way out.
115
+ - once the group is built, hand it to Flowviant exactly like this, with
116
+ MODEL the group:
117
+
118
+ import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
119
+ addEventListener('message', (e) => {
120
+ if (e.source !== parent || e.data?.flowviant !== 'export-glb') return;
121
+ new GLTFExporter().parse(
122
+ MODEL,
123
+ (glb) => parent.postMessage({ flowviant: 'glb', glb }, '*', [glb]),
124
+ (err) => parent.postMessage({ flowviant: 'glb-error', message: String(err) }, '*'),
125
+ { binary: true }
126
+ );
127
+ });
128
+ parent.postMessage({ flowviant: 'glb-ready' }, '*');
129
+
130
+ Keep the page under 2 MB.
109
131
 
110
132
  ${ARTIFACT_CONTRACT_TAIL({
111
133
  ask: `If you cannot model it without a DECISION only a person can make — which of
112
134
  two directions, what size or style, which asset it stands beside — STOP AND
113
135
  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',
136
+ summary: 'one or two sentences on what the model shows, the folder it is in, and the scale and target you assumed',
115
137
  })}`;
116
138
 
117
139
  /**
@@ -0,0 +1,89 @@
1
+ /**
2
+ * A CARD'S DISCUSSION, AS THE WIRE HANDS IT OVER (0.106.0) — the owner,
3
+ * 2026-09-27: "yes agents should read card comments".
4
+ *
5
+ * The server relays a card's thread (people's comments AND agents' own notes,
6
+ * replies and delivery cards) onto the jobs that work or plan it:
7
+ * `agentTurnJobs[].cardThread` (the one card a turn is about, named by
8
+ * `taskId` and `title`) and `agentPlanJobs[].tasks[].thread`. Each is
9
+ * `{ entries: [{ who, at, text }], omitted }`, oldest first — the newest run
10
+ * of the thread that fits the server's budget, with `omitted` counting the
11
+ * older entries it left out.
12
+ *
13
+ * THE WIRE IS NOT TRUSTED. The server bounded it (`CARD_THREAD_BUDGET` in the
14
+ * app's agent-runner.schema.ts); this module reads it again, entry by entry,
15
+ * and applies the SAME numbers — a copy the release gate holds to the server's
16
+ * (`scripts/check-app-parity.mjs`, rule 5). A malformed entry is dropped
17
+ * alone; a name or a time is one bounded line (a line break would forge a
18
+ * second entry); a text past its cap is cut and marked; entries past the
19
+ * budget are dropped from the OLDEST end and counted, so an agent is never
20
+ * handed a thread that silently starts in the middle.
21
+ *
22
+ * Absent, empty or wholly malformed reads as null, and a null thread prints
23
+ * NOTHING — which is why every prompt for a card nobody discussed (and every
24
+ * job from a server older than 0.106.0) stays byte-identical.
25
+ *
26
+ * Pure data and pure functions: no imports, no environment, no I/O — prompts
27
+ * imports it, and prompts is strings.
28
+ */
29
+
30
+ /** The server's `CARD_THREAD_BUDGET`, row for row: a TURN reads one card, a
31
+ * PLAN glances at many. */
32
+ export const CARD_THREAD_BUDGET = Object.freeze({
33
+ turn: Object.freeze({ entries: 30, chars: 6000, entryChars: 1500 }),
34
+ plan: Object.freeze({ entries: 6, chars: 1200, entryChars: 400 }),
35
+ });
36
+ /** The server's `CARD_THREAD_WHO_MAX` — a name is a label. */
37
+ export const CARD_THREAD_WHO_MAX = 80;
38
+ /** The server's `CARD_THREAD_AT_MAX` — an ISO time, with room. */
39
+ export const CARD_THREAD_AT_MAX = 40;
40
+ /** A count is a number a prompt prints; past this it is not a count. */
41
+ const OMITTED_MAX = 1_000_000;
42
+
43
+ /**
44
+ * THE MOST ONE TURN'S DISCUSSION CAN ADD TO A CARD SPEC, rendered — so the
45
+ * spec stash (agentCards.mjs `MAX_SPEC_CHARS`) can hold a card with its
46
+ * discussion whole, and the pre-review reads what the agent read. The text is
47
+ * at most `chars`; the renderer adds at most two characters and a newline per
48
+ * text line (<= 3x the text), a header line per entry, and two lines of its
49
+ * own. `cardThread.test.mjs` renders the worst case against it.
50
+ */
51
+ export const CARD_THREAD_RENDER_MAX =
52
+ 3 * CARD_THREAD_BUDGET.turn.chars +
53
+ CARD_THREAD_BUDGET.turn.entries * (CARD_THREAD_WHO_MAX + CARD_THREAD_AT_MAX + 10) +
54
+ 200;
55
+
56
+ const oneLine = (v, max) => String(v).replace(/[\r\n]+/g, ' ').trim().slice(0, max);
57
+
58
+ /**
59
+ * A thread off the wire, re-bounded at `budget`'s row ('turn' | 'plan'; an
60
+ * unknown row is the tighter plan one), or null when there is nothing in it to
61
+ * print. Never throws.
62
+ */
63
+ export function readCardThread(raw, budget = 'turn') {
64
+ const b = CARD_THREAD_BUDGET[budget] ?? CARD_THREAD_BUDGET.plan;
65
+ if (!raw || typeof raw !== 'object' || !Array.isArray(raw.entries)) return null;
66
+ const good = [];
67
+ for (const e of raw.entries) {
68
+ if (!e || typeof e !== 'object') continue;
69
+ if (typeof e.who !== 'string' || typeof e.at !== 'string' || typeof e.text !== 'string') continue;
70
+ const whole = e.text.replace(/\r\n?/g, '\n').trim();
71
+ if (!whole) continue;
72
+ good.push({
73
+ who: oneLine(e.who, CARD_THREAD_WHO_MAX) || 'member',
74
+ at: oneLine(e.at, CARD_THREAD_AT_MAX),
75
+ text: whole.length > b.entryChars ? `${whole.slice(0, b.entryChars - 1)}…` : whole,
76
+ });
77
+ }
78
+ // The newest contiguous run that fits — the server's own walk.
79
+ const kept = [];
80
+ let chars = 0;
81
+ for (let i = good.length - 1; i >= 0 && kept.length < b.entries; i--) {
82
+ if (chars + good[i].text.length > b.chars) break;
83
+ chars += good[i].text.length;
84
+ kept.push(good[i]);
85
+ }
86
+ if (kept.length === 0) return null;
87
+ const sent = Number.isInteger(raw.omitted) && raw.omitted > 0 ? Math.min(raw.omitted, OMITTED_MAX) : 0;
88
+ return { entries: kept.reverse(), omitted: sent + (good.length - kept.length) };
89
+ }
@@ -3,19 +3,26 @@
3
3
  * hands a coding CLI, in one place. Split out of claude.mjs purely for size
4
4
  * (the permission sets are claudePosture.mjs's, the turn plumbing runTurn.mjs's);
5
5
  * every call site imports them from here. These are strings and
6
- * nothing else: no environment, no I/O, and three imports — the card-kind
6
+ * nothing else: no environment, no I/O, and five imports — the card-kind
7
7
  * table (agentTaskKinds.mjs, itself pure data), which selects among them; the
8
+ * card-type table (agentTaskTypes.mjs, pure data, 0.106.0), which holds each
9
+ * type's label and its one contract sentence; the
8
10
  * 3D-model and presentation contracts and the tail every file-handing contract
9
- * ends in (artifactContracts.mjs, strings split out for size); and the kept
11
+ * ends in (artifactContracts.mjs, strings split out for size); the kept
10
12
  * library's reference rule (knowledgeLibrary.mjs, pure but for the pure
11
- * safe-name rule), which decides which card references are printed.
13
+ * safe-name rule), which decides which card references are printed; and the
14
+ * card-thread reader (cardThread.mjs, pure), which decides which of a card's
15
+ * discussion is printed (0.106.0).
12
16
  */
13
17
 
14
18
  // A card's kind is read through agentTaskKinds.mjs — the one table of what a
15
19
  // kind is. This file keeps only the STRINGS each kind is handed.
16
20
  import { agentTaskKindOf } from './agentTaskKinds.mjs';
21
+ // A card's type (0.106.0) likewise: the table holds the words, this file prints them.
22
+ import { AGENT_TASK_TYPES, agentTaskTypeFor } from './agentTaskTypes.mjs';
17
23
  import { ARTIFACT_CONTRACT_TAIL, SYSTEM_AGENT_DECK, SYSTEM_AGENT_MODEL } from './artifactContracts.mjs';
18
24
  import { isLibraryReference } from './knowledgeLibrary.mjs';
25
+ import { readCardThread } from './cardThread.mjs';
19
26
 
20
27
  // Wiki-gen turn: the local Claude READS the repo (cwd) and writes/maintains the
21
28
  // knowledge VAULT — a plain directory of markdown files with [[wikilinks]]
@@ -538,8 +545,8 @@ MECHANICS OF THIS CHAT:
538
545
  - "code": a change to the code — commits they review and merge. The default.
539
546
  - "design": a UI mockup — one HTML page to look at; no code changed.
540
547
  "Mockup", "design page X", "show me what the settings page could look like".
541
- - "model": a 3D model — a viewable bundle (an HTML viewer and the model
542
- files); no code changed. "3D model", "mesh", "a chair model", "glTF", "OBJ".
548
+ - "model": a 3D model — one page to turn it around in, downloaded as a
549
+ .glb; no code changed. "3D model", "mesh", "a chair model", "glTF", "OBJ".
543
550
  - "deck": a presentation — one HTML slide deck; no code changed. "Slides",
544
551
  "a deck", "a presentation", "a pitch".
545
552
  - "research": a write-up — read the repo and the web; no code changed.
@@ -791,7 +798,10 @@ exactly as given.`;
791
798
  * The cards and the live agents are FENCED: their titles, briefs and criteria
792
799
  * are written by whoever files cards in this project and by agents themselves,
793
800
  * and this turn reads a repository afterwards. The instruction that matters —
794
- * "split this" — is ours and sits outside the fence.
801
+ * "split this" — is ours and sits outside the fence. A card's discussion
802
+ * (0.106.0) is the same kind of text and sits inside the same fence. A card's
803
+ * TYPE (0.106.0) is printed under its kind by its label alone — the planner
804
+ * splits work, and the type's contract sentence is for the agent that does it.
795
805
  */
796
806
  export const AGENT_PLAN_KICKOFF = ({ tasks, liveAgents, agentCap }) => {
797
807
  const cards = tasks
@@ -802,10 +812,18 @@ export const AGENT_PLAN_KICKOFF = ({ tasks, liveAgents, agentCap }) => {
802
812
  // by it (SYSTEM_PLAN rule 8), and an absent line would read as "not
803
813
  // stated" rather than as the default. Absent on the wire IS code.
804
814
  ` kind: ${agentTaskKindOf(t.taskKind)}\n` +
815
+ // WHAT KIND OF WORK IT ASKS FOR (0.106.0), by its label — only on a
816
+ // typed card (a type on another kind is none), so an untyped card's
817
+ // listing, and every plan from an older server, is unchanged.
818
+ (agentTaskTypeFor(t) ? ` type: ${AGENT_TASK_TYPES[agentTaskTypeFor(t)].label}\n` : '') +
805
819
  (t.points ? ` points: ${t.points}\n` : '') +
806
820
  (t.anchors?.length ? ` owns: ${t.anchors.join(', ')}\n` : '') +
807
821
  (t.brief ? ` brief: ${t.brief}\n` : '') +
808
- (t.criteria?.length ? ` done when:\n${t.criteria.map((c) => ` - ${c}`).join('\n')}\n` : '')
822
+ (t.criteria?.length ? ` done when:\n${t.criteria.map((c) => ` - ${c}`).join('\n')}\n` : '') +
823
+ // WHAT HAS BEEN SAID ON IT (0.106.0) — at the plan row of the budget,
824
+ // inside the fence with the rest of the card. Absent on a card nobody
825
+ // discussed (and from an older server), so its listing is unchanged.
826
+ cardThreadText(readCardThread(t.thread, 'plan'), ' ')
809
827
  )
810
828
  .join('\n');
811
829
  const live = liveAgents.length
@@ -1030,7 +1048,7 @@ export const AGENT_CONTRACTS = Object.freeze({
1030
1048
  });
1031
1049
  export const AGENT_HANDS_BACK = Object.freeze({
1032
1050
  design: 'a DESIGN card: it hands back a mockup',
1033
- model: 'a 3D MODEL card: it hands back a viewable model bundle',
1051
+ model: 'a 3D MODEL card: it hands back a model to view and download as a .glb',
1034
1052
  deck: 'a PRESENTATION card: it hands back an HTML slide deck',
1035
1053
  research: 'a RESEARCH card: it hands back a write-up',
1036
1054
  });
@@ -1040,18 +1058,20 @@ export const AGENT_HANDS_BACK = Object.freeze({
1040
1058
  *
1041
1059
  * The card is FENCED: its title, brief and criteria are written by whoever
1042
1060
  * files cards in this project, and this turn is about to edit code. The
1043
- * instruction is ours and sits outside the fence.
1061
+ * instruction is ours and sits outside the fence. The card's DISCUSSION
1062
+ * (`cardThread`, 0.106.0) is printed inside the same fence, as part of the
1063
+ * spec (`AGENT_TASK_SPEC`), so the spec stashed for the pre-review carries it.
1044
1064
  *
1045
1065
  * The queue POSITION is stated because it changes behaviour: an agent that
1046
1066
  * thinks it is finishing tidies up, writes summaries and stops; one that knows
1047
1067
  * three more cards are coming leaves the ground ready for them.
1048
1068
  */
1049
- export const AGENT_TASK_KICKOFF = ({ agentName, task, position, total }) =>
1069
+ export const AGENT_TASK_KICKOFF = ({ agentName, task, cardThread, position, total }) =>
1050
1070
  agentTaskKindOf(task?.taskKind) === 'code'
1051
1071
  ? `You are the agent "${safeName(agentName)}", working card ${position} of ${total} ` +
1052
1072
  `on this branch. Everything you commit here is reviewed and merged TOGETHER with ` +
1053
1073
  `the other cards in this run.\n\n` +
1054
- `${fence('THE CARD', taskBlock(task))}\n\n` +
1074
+ `${fence('THE CARD', taskBlock(task, cardThread))}\n\n` +
1055
1075
  `When you commit, put this trailer on the LAST line of each commit message so ` +
1056
1076
  `the card can find its own commits:\n` +
1057
1077
  `Flowviant-Task: ${task?.id ?? ''}\n\n` +
@@ -1063,7 +1083,7 @@ export const AGENT_TASK_KICKOFF = ({ agentName, task, position, total }) =>
1063
1083
  `You are the agent "${safeName(agentName)}", working card ${position} of ${total} ` +
1064
1084
  `in this run. This card is ${AGENT_HANDS_BACK[agentTaskKindOf(task?.taskKind)]}, ` +
1065
1085
  `written under .flowviant/artifacts/, and changes nothing in the repository.\n\n` +
1066
- `${fence('THE CARD', taskBlock(task))}\n\n` +
1086
+ `${fence('THE CARD', taskBlock(task, cardThread))}\n\n` +
1067
1087
  `Do it, and end with the JSON object.`;
1068
1088
 
1069
1089
  /**
@@ -1091,7 +1111,7 @@ const safeName = (n) =>
1091
1111
  * renderings of a card that can drift — and the drift would be invisible,
1092
1112
  * because nobody reads both prompts side by side.
1093
1113
  */
1094
- export const AGENT_TASK_SPEC = (task) =>
1114
+ export const AGENT_TASK_SPEC = (task, cardThread) =>
1095
1115
  `id: ${task?.id ?? ''}\n` +
1096
1116
  `title: ${task?.title ?? ''}\n` +
1097
1117
  // THE KIND, when it is news (0.97.0). A code card's spec stays byte-for-byte
@@ -1099,6 +1119,11 @@ export const AGENT_TASK_SPEC = (task) =>
1099
1119
  // and a line saying "kind: code" on every card is a change to every prompt
1100
1120
  // for a fact that was always true.
1101
1121
  (agentTaskKindOf(task?.taskKind) !== 'code' ? `kind: ${agentTaskKindOf(task.taskKind)}\n` : '') +
1122
+ // THE TYPE AND ITS ONE CONTRACT SENTENCE (0.106.0), right under the kind:
1123
+ // "type: Bug fix — reproduce it first, …". Only a type this daemon knows,
1124
+ // on its own kind (`agentTaskTypeFor`); an untyped card's spec — and every
1125
+ // job from an older server — is byte-for-byte what it was.
1126
+ typeLine(task) +
1102
1127
  (task?.brief ? `\nbrief:\n${task.brief}\n` : '') +
1103
1128
  (task?.criteria?.length ? `\ndone when:\n${task.criteria.map((c) => `- ${c}`).join('\n')}\n` : '') +
1104
1129
  (task?.anchors?.length ? `\nthis card owns:\n${task.anchors.map((a) => `- ${a}`).join('\n')}\n` : '') +
@@ -1111,8 +1136,48 @@ export const AGENT_TASK_SPEC = (task) =>
1111
1136
  .filter(isReference)
1112
1137
  .map((r) => `- ${oneLine(r.name, 200)} — ${oneLine(r.title, 200)}`)
1113
1138
  .join('\n')}\n`
1139
+ : '') +
1140
+ // WHAT HAS BEEN SAID ON THE CARD (0.106.0) — the task turn's `cardThread`,
1141
+ // LAST, after everything the card asks. Printed only when it names THIS
1142
+ // card and holds something, so every spec without a discussion — every
1143
+ // stash and every prompt before 0.106.0 — is byte-for-byte what it was.
1144
+ (cardThread && task?.id && String(cardThread.taskId ?? '') === String(task.id)
1145
+ ? prefixed('\n', cardThreadText(readCardThread(cardThread, 'turn')))
1114
1146
  : '');
1115
1147
 
1148
+ /** A card's type as the spec prints it, or ''. */
1149
+ const typeLine = (task) => {
1150
+ const type = agentTaskTypeFor(task);
1151
+ if (!type) return '';
1152
+ const { label, contract } = AGENT_TASK_TYPES[type];
1153
+ return `type: ${label} — ${contract}\n`;
1154
+ };
1155
+
1156
+ /**
1157
+ * A CARD'S DISCUSSION AS TEXT — the one renderer, for the spec, a human
1158
+ * turn's own block and the plan's listing (0.106.0). '' for a null thread.
1159
+ *
1160
+ * Oldest first, one header line per entry (who, and when when it is known),
1161
+ * the words beneath it indented two spaces, so no line of somebody's comment
1162
+ * can start a new entry under another name. How many earlier entries the
1163
+ * budget left out is said, not hidden: an agent reading a reply without the
1164
+ * message it answers should know there was one.
1165
+ */
1166
+ const cardThreadText = (thread, indent = '') => {
1167
+ if (!thread) return '';
1168
+ const lines = [`${indent}discussion on this card (oldest first):`];
1169
+ if (thread.omitted > 0) {
1170
+ lines.push(`${indent}(${thread.omitted} earlier message${thread.omitted === 1 ? '' : 's'} not shown)`);
1171
+ }
1172
+ for (const e of thread.entries) {
1173
+ lines.push(`${indent}- ${e.who}${e.at ? `, ${e.at}` : ''}:`);
1174
+ for (const line of e.text.split('\n')) lines.push(line ? `${indent} ${line}` : '');
1175
+ }
1176
+ return `${lines.join('\n')}\n`;
1177
+ };
1178
+ /** `head` before `text`, or '' when there is no text. */
1179
+ const prefixed = (head, text) => (text ? `${head}${text}` : '');
1180
+
1116
1181
  /** A reference as the server sends it: a library path and its title. Anything
1117
1182
  * else is not printed — this text is fenced as card content, but a path that
1118
1183
  * is not a path is not worth handing an agent.
@@ -1135,16 +1200,33 @@ const taskBlock = AGENT_TASK_SPEC;
1135
1200
  * It is the same shape as a task turn on purpose: the agent still ends with the
1136
1201
  * JSON object, because whatever it does next either finishes the card it was on
1137
1202
  * or blocks again, and those are the only two things the board can act on.
1203
+ *
1204
+ * THE CARD'S DISCUSSION (0.106.0) — when the turn is about one card (an
1205
+ * answer that names it, or a message to an agent holding one), the server
1206
+ * sends that card's thread, and it is printed AFTER the person's words in a
1207
+ * fence of its own, naming the card (a message often carries no `task` to name
1208
+ * it by). Never inside THE CARD YOU ARE ON: that block is the spec, as it
1209
+ * always was. Absent, nothing is printed, and the prompt is what it was.
1138
1210
  */
1139
- export const AGENT_HUMAN_KICKOFF = ({ agentName, message, askedByName, task, position, total }) =>
1211
+ export const AGENT_HUMAN_KICKOFF = ({ agentName, message, askedByName, task, cardThread, position, total }) =>
1140
1212
  `You are the agent "${safeName(agentName)}"` +
1141
1213
  (task ? `, working card ${position} of ${total} on this branch` : '') +
1142
1214
  `.\n\n` +
1143
1215
  `${fence('WHO IS TALKING', askedByName || 'a member of this project')}\n\n` +
1144
1216
  `${fence('WHAT THEY SAID', message)}\n\n` +
1145
1217
  (task ? `${fence('THE CARD YOU ARE ON', taskBlock(task))}\n\n` : '') +
1218
+ cardDiscussionBlock(cardThread) +
1146
1219
  `Carry on, and end with the JSON object as usual.`;
1147
1220
 
1221
+ /** A human turn's card discussion, fenced, naming the card — '' when there is
1222
+ * none to print. */
1223
+ const cardDiscussionBlock = (cardThread) => {
1224
+ const text = cardThreadText(readCardThread(cardThread, 'turn'));
1225
+ if (!text) return '';
1226
+ const head = `card: ${oneLine(cardThread.taskId, 64)}\ntitle: ${oneLine(cardThread.title, 300)}\n\n`;
1227
+ return `${fence("THE CARD'S DISCUSSION", `${head}${text}`)}\n\n`;
1228
+ };
1229
+
1148
1230
  /**
1149
1231
  * THE AI PRE-REVIEW — a FRESH Claude reads the branch before the human does
1150
1232
  * (2026-09-16).
@@ -282,7 +282,9 @@ export function createAgentTurnExecution({
282
282
  * what was asked for either way.
283
283
  *
284
284
  * ONE SPEC BUILDER (`AGENT_TASK_SPEC`) shared with the kickoff below, so
285
- * what the reviewer reads is byte-identical to what the agent read.
285
+ * what the reviewer reads is byte-identical to what the agent read — the
286
+ * card's discussion included (0.106.0): both are handed the same
287
+ * `job.cardThread`, and the builder prints it inside the spec.
286
288
  * Failure is swallowed inside `stashCard`: a note about a turn may never
287
289
  * cost the turn.
288
290
  */
@@ -290,7 +292,7 @@ export function createAgentTurnExecution({
290
292
  stashCard(
291
293
  sessionMetaPath(wt, 'flowviant-agent-cards', agentId),
292
294
  job.task.id,
293
- AGENT_TASK_SPEC(job.task)
295
+ AGENT_TASK_SPEC(job.task, job.cardThread)
294
296
  );
295
297
  }
296
298
 
@@ -372,6 +374,9 @@ export function createAgentTurnExecution({
372
374
  ? AGENT_TASK_KICKOFF({
373
375
  agentName: job.agentName,
374
376
  task: job.task,
377
+ // The card's discussion (0.106.0), printed inside THE CARD by
378
+ // the same builder the stash above wrote down.
379
+ cardThread: job.cardThread,
375
380
  position: job.position ?? 1,
376
381
  total: job.total ?? 1,
377
382
  })
@@ -380,6 +385,9 @@ export function createAgentTurnExecution({
380
385
  message: job.body ?? '',
381
386
  askedByName: job.askedByName,
382
387
  task: job.task,
388
+ // The discussion on the one card this message is about, in a
389
+ // fence of its own after the person's words (0.106.0).
390
+ cardThread: job.cardThread,
383
391
  position: job.position ?? 1,
384
392
  total: job.total ?? 1,
385
393
  }),
@@ -78,8 +78,8 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
78
78
  *
79
79
  * Its product is under `.flowviant/artifacts/`, and the kind's table
80
80
  * (agentTaskKinds.mjs) says which file proves it: a `.html` mockup for
81
- * design, a `.html` deck, a `.md` write-up for research, and for a 3D
82
- * model a `.html` viewer AND a `.gltf`/`.glb`/`.obj` beside it. A turn that
81
+ * design, a `.html` deck, a `.md` write-up for research, and a `.html`
82
+ * page for a 3D model (the model is built in it, 0.107.0). A turn that
83
83
  * says "delivered" without having written one would land the agent in Review
84
84
  * with nothing to look at and a summary describing a page that does not
85
85
  * exist — so it settles `nothing` with the measured sentence, and the
@@ -108,19 +108,10 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
108
108
  */
109
109
  if (res.outcome === 'delivered' && kind.artifact) {
110
110
  const want = kind.artifact.match;
111
- // THE 3D-MODEL KIND'S SECOND PROOF (0.105.0): its viewer page alone is a
112
- // page, not a model — a `.gltf`/`.glb`/`.obj` must stand beside it. A
113
- // task turn delivers when it wrote EITHER half this turn and BOTH stand
114
- // (a re-modelled mesh — a rewritten .gltf/.glb/.obj — need not rewrite
115
- // its viewer; a texture or .mtl alone is neither half). The halves are
116
- // not paired by folder (pinned in the test). With no companion this is
117
- // exactly the one-file rule design, deck and research have.
118
- const also = kind.artifact.companion;
119
- const need = (l) => l.some((e) => want.test(e.name)) && (!also || l.some((e) => also.test(e.name)));
111
+ const has = (l) => l.some((e) => want.test(e.name));
120
112
  const standing = scanArtifacts(wt);
121
- const touched = changedArtifacts(artifactsBefore, standing);
122
- const wrote = touched.some((e) => want.test(e.name) || also?.test(e.name)) && need(standing);
123
- const present = wrote || ((job.kind !== 'task' || job.redo === true) && need(standing));
113
+ const wrote = has(changedArtifacts(artifactsBefore, standing));
114
+ const present = wrote || ((job.kind !== 'task' || job.redo === true) && has(standing));
124
115
  if (!present) {
125
116
  return report({
126
117
  turnId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowviant",
3
- "version": "0.105.0",
3
+ "version": "0.107.0",
4
4
  "description": "Run your own coding CLIs as build agents for Flowviant — Claude Code, Codex or Antigravity, on your own credentials. Holds your sessions, keeps a worktree per tab, and ships branches on your word.",
5
5
  "type": "module",
6
6
  "bin": {