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
package/bin/lib/trace.mjs CHANGED
@@ -60,6 +60,7 @@
60
60
  */
61
61
 
62
62
  import { THINK_MARKER } from './runtimeEvents.mjs';
63
+ import { withoutTurnDeclaration } from './turnDeclaration.mjs';
63
64
 
64
65
  /** One batch every two seconds — the discipline the tab's narrator keeps, for
65
66
  * the same reason: a turn emits hundreds of entries and nobody is reading them
@@ -207,7 +208,8 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
207
208
  * did when a thought was 300 characters.
208
209
  */
209
210
  prose(kind, text) {
210
- const t = scrub(String(text ?? ''))
211
+ const readable = kind === 'think' ? String(text ?? '') : withoutTurnDeclaration(text);
212
+ const t = scrub(readable)
211
213
  // One line ending, whatever the CLI printed.
212
214
  .replace(/\r\n?/g, '\n')
213
215
  // Spaces and tabs collapse; `\n` is deliberately excluded from the class.
@@ -216,7 +218,7 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
216
218
  .replace(/\n{3,}/g, '\n\n')
217
219
  .trim()
218
220
  .slice(0, TRACE_PROSE_CAP);
219
- if (!t) return;
221
+ if (!t) return null;
220
222
  const k = kind === 'think' ? 'think' : kind === 'say' ? 'say' : 'note';
221
223
  /**
222
224
  * A RUN OF BARE "thinking…" IS ONE STEP, not forty.
@@ -238,9 +240,12 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
238
240
  * steps are not shown" count stays a count of steps that existed.
239
241
  */
240
242
  if (k === 'think' && t === THINK_MARKER && lastEntry?.k === 'think' && lastEntry.t === t) {
241
- return;
243
+ return readable;
242
244
  }
243
245
  push({ k, t });
246
+ // The activity pulse uses this same filtering; its original compact
247
+ // label stands unless protocol text was removed from the full message.
248
+ return readable;
244
249
  },
245
250
  /** One structured tool event, exactly as `toolEventOf` built it. A tool this
246
251
  * builder does not know returns null there and nothing is pushed here — a
@@ -0,0 +1,88 @@
1
+ /** A final outcome is protocol, not conversation prose. Filter before trace
2
+ * normalization/caps; the unmodified answer still goes to parseTurnResult.
3
+ * Only standalone objects and JSON/unlabelled fences count. Code examples
4
+ * inside prose or another language's fence stay as the CLI wrote them. */
5
+ import { jsonObjectClosers } from './agentPlan.mjs';
6
+
7
+ const statuses = new Set(['delivered', 'blocked', 'prerequisite', 'nothing']);
8
+ function declaration(body) {
9
+ const raw = body.trim();
10
+ if (!raw.startsWith('{')) return false;
11
+ try {
12
+ const value = JSON.parse(raw);
13
+ return value && typeof value === 'object' && !Array.isArray(value)
14
+ && ((Object.hasOwn(value, 'summary') && Object.hasOwn(value, 'progress'))
15
+ || (typeof value.status === 'string'
16
+ && (statuses.has(value.status) || ['summary', 'progress', 'question'].some(key => Object.hasOwn(value, key)))));
17
+ } catch {
18
+ // A truncated/invalid declaration must not fall through as an answer.
19
+ // Require a protocol key at the root's opening and the declared status;
20
+ // arbitrary malformed JSON remains useful diagnostic output.
21
+ return /^\{\s*"(?:status|summary|progress|question)"\s*:/.test(raw)
22
+ && /"status"\s*:\s*"(?:delivered|blocked|prerequisite|nothing)"/.test(raw);
23
+ }
24
+ }
25
+
26
+ /** Objects on their own lines, including a final object after a preamble.
27
+ * Skip a whole outer span before looking again; nested examples are data. */
28
+ function bareDeclarations(raw) {
29
+ const closes = jsonObjectClosers(raw);
30
+ let kept = '', from = 0, removed = false;
31
+ for (let i = 0; i < raw.length; i++) {
32
+ if (raw[i] !== '{') continue;
33
+ const end = closes.get(i) ?? -1;
34
+ const lineStart = raw.lastIndexOf('\n', i - 1) + 1;
35
+ const nextLine = end < 0 ? -1 : raw.indexOf('\n', end + 1);
36
+ const alone = !raw.slice(lineStart, i).trim()
37
+ && (end < 0 || !raw.slice(end + 1, nextLine < 0 ? raw.length : nextLine).trim());
38
+ const finish = end < 0 ? raw.length : end + 1;
39
+ if (alone && declaration(raw.slice(i, finish))) {
40
+ kept += raw.slice(from, i); from = finish; removed = true;
41
+ }
42
+ if (end >= 0) i = end;
43
+ else {
44
+ // An unclosed outer object owns the remaining candidate. For a stray
45
+ // brace in prose, skip its line instead of rescanning a brace wall.
46
+ const newline = raw.indexOf('\n', i);
47
+ if (alone || newline < 0) break;
48
+ i = newline;
49
+ }
50
+ }
51
+ return removed ? kept + raw.slice(from) : null;
52
+ }
53
+
54
+ export function withoutTurnDeclaration(text) {
55
+ const raw = String(text ?? '');
56
+ const kept = [];
57
+ let block = [], fence = null, json = false, removed = false;
58
+ let prose = [];
59
+ const flushProse = () => {
60
+ const plain = bareDeclarations(prose.join('\n'));
61
+ if (plain !== null) { removed = true; kept.push(plain); }
62
+ else kept.push(...prose);
63
+ prose = [];
64
+ };
65
+ // Consume every fence, including other languages, in one forward pass.
66
+ // A nested JSON example is part of its outer code block, never a result.
67
+ for (const line of raw.split('\n')) {
68
+ if (fence) {
69
+ if (line.trim() === fence) {
70
+ if (json && declaration(block.slice(1).join('\n'))) removed = true;
71
+ else kept.push(...block, line);
72
+ block = []; fence = null;
73
+ } else block.push(line);
74
+ } else {
75
+ const open = line.trim().match(/^(`{3,}|~{3,})[ \t]*([\w-]*)$/);
76
+ if (open) {
77
+ flushProse();
78
+ fence = open[1]; json = !open[2] || open[2].toLowerCase() === 'json'; block = [line];
79
+ } else prose.push(line);
80
+ }
81
+ }
82
+ flushProse();
83
+ if (fence) {
84
+ if (json && declaration(block.slice(1).join('\n'))) removed = true;
85
+ else kept.push(...block);
86
+ }
87
+ return removed ? kept.join('\n').replace(/\n{3,}/g, '\n\n').trim() : raw;
88
+ }
@@ -0,0 +1,93 @@
1
+ /** Local, correlated CLI lifecycle evidence. No prompts, answers, argv, token
2
+ * values or token fingerprints are logged. Credential metadata is remeasured
3
+ * at close so a refresh/rotation can be distinguished from a stale environment.
4
+ * Logging is best effort and rides the daemon's existing bounded private log.
5
+ */
6
+ import { randomUUID } from 'node:crypto';
7
+ import { accessSync, constants, readFileSync, realpathSync, statSync } from 'node:fs';
8
+ import { delimiter, isAbsolute, resolve } from 'node:path';
9
+ import { AUTH_ENV_VARS, claudeAuthContext, claudeCredentialPath } from './claudeAuth.mjs';
10
+ import { scrub } from './uplinkScrub.mjs';
11
+ import { printableText } from './printable.mjs';
12
+ import { measuredRuntimeVersion } from './runtimeDetection.mjs';
13
+
14
+ let active = 0;
15
+ const authNames = (env) => AUTH_ENV_VARS.filter((name) =>
16
+ typeof env[name] === 'string' && env[name].trim() !== '');
17
+ function executableFor(bin, env, cwd) {
18
+ const candidates = isAbsolute(bin) || bin.includes('/')
19
+ ? [resolve(cwd, bin)]
20
+ : (env.PATH ?? '').split(delimiter).map((dir) => resolve(cwd, dir, bin));
21
+ for (const candidate of candidates) {
22
+ try { accessSync(candidate, constants.X_OK); return realpathSync(candidate); } catch { /* next */ }
23
+ }
24
+ return null;
25
+ }
26
+ function authFor(env) {
27
+ const context = claudeAuthContext({ env });
28
+ const credentialPath = claudeCredentialPath(env);
29
+ let file = null;
30
+ try {
31
+ const stat = statSync(credentialPath);
32
+ file = { modifiedAt: new Date(stat.mtimeMs).toISOString(), inode: stat.ino };
33
+ } catch { /* absent or keychain: unknown */ }
34
+ return { ...context, configDir: env.CLAUDE_CONFIG_DIR || null, credentialPath, file };
35
+ }
36
+
37
+ export function startTurnDiagnostics(options) {
38
+ try { return createTurnDiagnostics(options); }
39
+ catch { return () => {}; }
40
+ }
41
+
42
+ function createTurnDiagnostics({ runtime, bin, env, cwd = process.cwd(), profile,
43
+ planId = null, args = [], version, write = (row) => console.info(`[cli-turn] ${JSON.stringify(row)}`) }) {
44
+ const startedAt = Date.now();
45
+ const turnId = randomUUID();
46
+ const executable = executableFor(bin, env, cwd);
47
+ let settingsEnv = {};
48
+ if (runtime === 'claude') {
49
+ const at = args.indexOf('--settings');
50
+ if (at >= 0) {
51
+ try {
52
+ const input = args[at + 1];
53
+ const settings = JSON.parse(input.startsWith('{') ? input : readFileSync(input, 'utf8'));
54
+ if (settings.env && typeof settings.env === 'object' && !Array.isArray(settings.env)) {
55
+ for (const [key, value] of Object.entries(settings.env))
56
+ if (typeof value === 'string') settingsEnv[key] = value;
57
+ }
58
+ } catch { /* CLI settings unavailable */ }
59
+ }
60
+ }
61
+ const base = { turnId, runtime, profile, planId, cwd, daemonPid: process.pid,
62
+ uid: process.getuid?.() ?? null, platform: process.platform,
63
+ nodeVersion: process.version, executable, version: version ?? null,
64
+ runtimeMeasurement: measuredRuntimeVersion(runtime) };
65
+ const auth = () => {
66
+ try {
67
+ return runtime === 'claude' ? { ...authFor({ ...env, ...settingsEnv }),
68
+ inheritedAuthVars: authNames(env),
69
+ settingsAuthVars: authNames(settingsEnv) } : null;
70
+ } catch { return null; }
71
+ };
72
+ const emit = (row) => {
73
+ try {
74
+ // Scrub strings even in the allowlisted metadata: paths/user names are
75
+ // operator supplied. No environment object or error body reaches here.
76
+ write(JSON.parse(scrub(printableText(JSON.stringify(row)))));
77
+ } catch { /* diagnostics never fail a turn */ }
78
+ };
79
+ active++;
80
+ emit({ ...base, phase: 'start', at: new Date(startedAt).toISOString(), activeTurns: active, auth: auth() });
81
+ let finished = false;
82
+ return ({ code = null, signal = null, pid = null, output = '', spawnError = null, cliFailed = false } = {}) => {
83
+ if (finished) return;
84
+ finished = true;
85
+ active--;
86
+ const failed = cliFailed || (code !== null && code !== 0);
87
+ emit({ ...base, phase: 'end', at: new Date().toISOString(), durationMs: Date.now() - startedAt,
88
+ pid, code, signal, spawnError, activeTurns: active, auth: auth(),
89
+ failureKind: failed && /OAuth session expired and could not be refreshed/i.test(output)
90
+ ? 'oauth_refresh' : failed && /Failed to authenticate/i.test(output) ? 'authentication'
91
+ : spawnError ? 'spawn' : code !== null && code !== 0 ? 'cli_exit' : null });
92
+ };
93
+ }
@@ -1,22 +1,32 @@
1
1
  /**
2
2
  * THE CROSS-PROCESS TURN LOCK — the one reading of "is a CLI of ours already
3
- * working in this directory".
3
+ * working in this tab's conversation" (and, for ship, "anywhere in this
4
+ * directory").
4
5
  *
5
6
  * Split out of work.mjs (2026-09-26, SOLID F037). It is read by the session
6
7
  * lane before a spawn and by the ship executor before a merge, and it is pure
7
8
  * over the disk and the kernel: no manager state, so it needs no factory and
8
9
  * is tested directly (turnLock.test.mjs). The lock FILE's path is the
9
- * caller's (`sessionMetaPath(wt, 'flowviant-turn.lock')`); what a pid in it
10
- * means is this module's.
10
+ * caller's (`sessionMetaPath(wt, 'flowviant-turn.lock', sessionId)`); what a
11
+ * pid in it means is this module's.
12
+ *
13
+ * PER TAB, NOT PER DIRECTORY (2026-10-14). It was keyed by the directory alone,
14
+ * on the assumption that a place holds one session. Every tab a person opens
15
+ * shares the checkout, so the one file serialized all of their tabs: a
16
+ * fourteen-minute turn in one tab held a message in two others for eight
17
+ * minutes each. The lock guards a CONVERSATION (two CLIs resuming one held
18
+ * context), so it is scoped by the tab; the unscoped file is what a daemon
19
+ * from before this change wrote, and is still honored while its CLI lives.
11
20
  */
12
- import { existsSync, readFileSync, rmSync } from 'node:fs';
21
+ import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
22
+ import { basename, dirname, join } from 'node:path';
13
23
  import { aliveAndOurs, processStartTime } from './procRegistry.mjs';
14
24
 
15
25
  /**
16
- * The spawn lock: the pid of the CLI currently live in this worktree. A
17
- * restarted daemon must not put a second Claude into a directory the orphan
18
- * of its previous life is still editing — two CLIs appending to one held
19
- * conversation is exactly the incoherence workChains prevents in-process,
26
+ * The spawn lock: the pid of the CLI currently live in this tab. A restarted
27
+ * daemon must not resume the conversation its previous life's orphan still
28
+ * holds — two CLIs appending to one held conversation is exactly the
29
+ * incoherence the synchronous start claim prevents in-process,
20
30
  * and the lock extends that guarantee across a restart. A dead pid is a
21
31
  * stale lock (removed here); a live one means "come back next poll".
22
32
  *
@@ -59,3 +69,33 @@ export const turnLockedByLivePid = (lockPath) => {
59
69
  }
60
70
  return false;
61
71
  };
72
+
73
+ /**
74
+ * ANY TAB'S TURN IN THIS DIRECTORY — ship's question, asked of the unscoped
75
+ * lock path (`sessionMetaPath(wt, 'flowviant-turn.lock')`). Ship folds and
76
+ * merges with git in the directory every tab there shares, so a live CLI of
77
+ * ANY tab defers it, not only the shipping tab's own. Reads the unscoped file
78
+ * and every `<name>-<scope>` beside it; nothing else in the git dir. Return
79
+ * the holding tab's id so the waiting log names the holder, not the shipper.
80
+ * A legacy lock has no tab id: null is an honest unknown.
81
+ */
82
+ export const placeTurnLockHolder = (placeLockPath) => {
83
+ if (!placeLockPath) return null;
84
+ const dir = dirname(placeLockPath);
85
+ const name = basename(placeLockPath);
86
+ let entries;
87
+ try {
88
+ entries = readdirSync(dir);
89
+ } catch {
90
+ return null;
91
+ }
92
+ for (const entry of entries) {
93
+ if (entry !== name && !entry.startsWith(`${name}-`)) continue;
94
+ if (turnLockedByLivePid(join(dir, entry))) {
95
+ return { sessionId: entry === name ? null : entry.slice(name.length + 1) };
96
+ }
97
+ }
98
+ return null;
99
+ };
100
+
101
+ export const placeTurnLockedByLivePid = (placeLockPath) => Boolean(placeTurnLockHolder(placeLockPath));
@@ -26,6 +26,14 @@
26
26
  * Claude Code's and, since 0.115.0, Codex's; image (0.114.0)
27
27
  * is Codex's.
28
28
  *
29
+ * `projectMcp` (2026-10-10, the owner): whether an agent's turn under this
30
+ * profile mounts MCP servers — the project's reviewed base `.mcp.json` copy
31
+ * and, since 2026-10-11 ("the server that is running flowviant's claude HAS
32
+ * these MCPs yet the agents are not using them"), this machine's own CLI
33
+ * servers and claude.ai connectors. Every card's posture: the code card's
34
+ * build, and the write-up's, mockup/3D-model/deck's and image's fences. The
35
+ * consult, plan, wiki and plan-mode turns are not a card's and mount none.
36
+ *
29
37
  * Claude's permission list for each name lives beside the lists themselves
30
38
  * (`claudePermFor`, claudePosture.mjs); this module owns the vocabulary and the
31
39
  * refusals, and imports the card-kind table and the runtime registry (whose
@@ -44,10 +52,12 @@ const declaredBy = (name) =>
44
52
  .join(' or ') || 'no CLI this daemon knows';
45
53
  const article = (word) => (/^[aeiou]/i.test(word) ? 'an' : 'a');
46
54
 
47
- const plain = (adapterProfile) => Object.freeze({ adapterProfile, onlyOn: null, strictMcp: false });
55
+ const plain = (adapterProfile) => Object.freeze({ adapterProfile, onlyOn: null, strictMcp: false, projectMcp: false });
56
+ /** The kind postures that mount MCP servers (2026-10-10; image 2026-10-11). */
57
+ const KIND_PROJECT_MCP = new Set(['design', 'research', 'image']);
48
58
 
49
59
  export const TURN_PROFILES = Object.freeze({
50
- build: plain('build'),
60
+ build: Object.freeze({ ...plain('build'), projectMcp: true }),
51
61
  wiki: plain('wiki'),
52
62
  consult: plain('consult'),
53
63
  plan: plain('plan'),
@@ -58,6 +68,7 @@ export const TURN_PROFILES = Object.freeze({
58
68
  // wearing the word "plan".
59
69
  onlyOn: (rt) => (rt.id === 'claude' ? null : `plan mode runs on Claude Code only — not '${rt.label}'`),
60
70
  strictMcp: true,
71
+ projectMcp: false,
61
72
  }),
62
73
  // The kind postures: fenced to "write only the artifacts directory", and a
63
74
  // runtime that does not declare one would run its build branch instead.
@@ -73,6 +84,7 @@ export const TURN_PROFILES = Object.freeze({
73
84
  ? null
74
85
  : `${article(name)} ${name} card runs on ${declaredBy(name)} only — not '${rt.label}'`,
75
86
  strictMcp: false,
87
+ projectMcp: KIND_PROJECT_MCP.has(name),
76
88
  }),
77
89
  ])
78
90
  ),
@@ -83,7 +95,7 @@ export const TURN_PROFILES = Object.freeze({
83
95
  export const RETIRED_POSTURE_KEYS = Object.freeze(['wikiPerm', 'readOnly', 'planPerm', 'planMode', 'posture']);
84
96
 
85
97
  /**
86
- * `{ name, adapterProfile, onlyOn, strictMcp }` for a turn's options, or
98
+ * `{ name, adapterProfile, onlyOn, strictMcp, projectMcp }` for a turn's options, or
87
99
  * `{ error }` in words. Absent `profile` is build.
88
100
  */
89
101
  export function resolveTurnProfile(opts = {}) {
package/bin/lib/work.mjs CHANGED
@@ -33,6 +33,7 @@
33
33
  * workChildren.mjs the live CLI children, teardown, the admission
34
34
  * workAgentPublish.mjs pushing and fetching an agent's branch
35
35
  * workPlaceReports.mjs the worktree readout: settle, burst, sweep
36
+ * workArtifactReconcile.mjs an idle agent's stored delivery against its worktree
36
37
  * workReportQueue.mjs every settle, held and retried until delivered
37
38
  * workSessionTokens.mjs the per-session work credential
38
39
  * workNarration.mjs the tab's live line and the deferral sentence
@@ -50,17 +51,21 @@
50
51
  * workCappedTurn.mjs one capped read-only consult turn: its child, its slot,
51
52
  * its cap and its transcript — the planner, the pre-review
52
53
  * and intake all run one (2026-10-10)
53
- * turnLock.mjs whether a live CLI of ours holds a directory's turn
54
+ * turnLock.mjs tab pid locks; workLiveMessages.mjs — live input; workDelivery.mjs — queue
54
55
  * (and, split earlier: workDiffs, workPreviews, workPullRequests, workShip,
55
56
  * workAgentPlans, workAgentCheck, workAgentPrecheck, workAgentReview,
56
- * workAgentTurns, workAgentMerges, workProcesses; and out of workAgentTurns,
57
- * SOLID F036: workAgentTurnReports, workAgentTurnExecution,
58
- * workAgentTurnBegun, workAgentTurnOutcome)
57
+ * workAgentTurns, workAgentRecovery, workAgentMerges, workProcesses; and out of workAgentTurns,
58
+ * SOLID F036: workAgentTurnReports, workAgentTurnExecution, workAgentTurnBegun,
59
+ * workAgentTurnOutcome; repositoryPlaces + agentRepositories: multi-repo places; the live brief: workAgentBrief, workAgentBriefRunner)
59
60
  */
60
61
 
62
+ import { trackedRepositorySetup } from './repositorySetup.mjs';
63
+ import { createRepositoryPlaces } from './repositoryPlaces.mjs';
64
+ import { createAgentRepositories } from './agentRepositories.mjs';
61
65
  import { createWorkProcesses } from './workProcesses.mjs';
62
66
  import { createWorkAgentMerges } from './workAgentMerges.mjs';
63
67
  import { createWorkAgentTurns } from './workAgentTurns.mjs';
68
+ import { createAgentRecovery } from './workAgentRecovery.mjs';
64
69
  import { createWorkAgentCheck } from './workAgentCheck.mjs';
65
70
  import { createWorkAgentPrecheck } from './workAgentPrecheck.mjs';
66
71
  import { createWorkAgentReview } from './workAgentReview.mjs';
@@ -86,17 +91,19 @@ import { createWorkSessionTurns } from './workSessionTurns.mjs';
86
91
  import { createWorkRetire } from './workRetire.mjs';
87
92
  import { FLEET_URL, FLEET_TOKEN, USER_AGENT } from './config.mjs';
88
93
  import { gitNet as gitNetIn, excludeInWorktree } from './git.mjs';
89
- import { createLandedObserver } from './landed.mjs';
94
+ import { createProjectLandedObserver } from './projectLanded.mjs';
90
95
  import { createPlaceLock } from './placeLock.mjs';
91
96
  import { warn } from './ui.mjs';
92
97
  import { FLOWVIANT_OWN_PATHS } from './knowledgeLibrary.mjs';
93
98
  import { buildArtifactUpload, changedSince, snapshotArtifacts } from './artifacts.mjs';
94
99
  import { createArtifactReporter } from './artifactReporter.mjs';
100
+ import { createWorkArtifactReconcile } from './workArtifactReconcile.mjs';
95
101
  import { renderDesignPreview } from './artifactPreview.mjs';
96
102
  import { scrub as envScrub, secretIn as envSecretIn } from './uplinkScrub.mjs';
97
103
 
98
104
  export function createWorkManager({
99
105
  repoRoot,
106
+ repositories,
100
107
  baseDir,
101
108
  getBaseRef,
102
109
  getMcpUrl,
@@ -133,10 +140,12 @@ export function createWorkManager({
133
140
  * `origin/<branch you happened to be on>` for the life of the daemon.
134
141
  */
135
142
  const baseRef = () => getBaseRef();
143
+ const repositoryPlaces = repositories && createRepositoryPlaces({ repoRoot, baseDir, repositories, changed: onRepoChanged, setup: (...args) => trackedRepositorySetup(workChildren, groupKillChildren)(...args) });
144
+ const agentRepositories = repositories && createAgentRepositories({ repositories, places: repositoryPlaces });
136
145
  // What arrived on base, whichever road it took — observed after every beat
137
146
  // that can move origin/<base>: the sweep's fetch, a ship's push, a PR merge
138
147
  // this daemon performed. See landed.mjs for the seeding and delivery rules.
139
- const landed = createLandedObserver({ repoRoot, baseRef });
148
+ const landed = createProjectLandedObserver({ repoRoot, baseRef, repositories });
140
149
  /**
141
150
  * THE ARTIFACT RELAY (2026-09-22, 0.94.0) — what a turn wrote under
142
151
  * `.flowviant/artifacts/`, uploaded after it. One reporter for the life of
@@ -156,6 +165,13 @@ export function createWorkManager({
156
165
  listChanged: changedSince,
157
166
  buildUpload: buildArtifactUpload,
158
167
  renderPreview: renderDesignPreview,
168
+ // What each place held, per owner, beside its worktree's git dir: it dies
169
+ // with the worktree, so a recreated one proves no discard (artifactIteration.mjs).
170
+ // `sessionMetaPath` and `worktreeBornAt` are declared below; both are called only once the manager is built.
171
+ seenPath: (dir, owner) =>
172
+ /^[A-Za-z0-9_-]{1,64}$/.test(String(owner)) ? sessionMetaPath(dir, 'flowviant-artifacts-seen', owner) : null,
173
+ // …and when that worktree was cut, so rows stored before it are never its own.
174
+ placeBornAt: (dir) => worktreeBornAt(dir),
159
175
  });
160
176
  /**
161
177
  * Snapshot a place's artifact directory before a CLI spawns in it — and make
@@ -191,17 +207,26 @@ export function createWorkManager({
191
207
  */
192
208
  const gitNet = (args, ms) => gitNetIn(args, repoRoot, ms);
193
209
 
194
- // ── THE SESSION'S OWN HALF — built first, because the lanes below are handed it.
195
- const { placeOf, notePlace, learnPlaces, placeDir, branchAt, placeWtFor, sessionMetaPath } =
210
+ const { placeOf, notePlace, learnPlaces, placeDir, branchAt, placeWtFor, sessionMetaPath, worktreeBornAt } =
196
211
  createWorkPlaces({ repoRoot, baseDir, baseRef, onRepoChanged });
197
212
  const { sessionGroups, noteSessionGroup, pruneSessionGroups, sessionProcesses } =
198
213
  createWorkSessionGroups({ repoRoot });
199
214
  const { workChildren, groupKillChildren, shutdownWork, liveTurnCount, liveTurns, admit } =
200
215
  createWorkChildren({ extraLiveTurns });
201
- const { agentPublished, agentRemoteAt, publishAgentBranch, fetchPublishedBranch } =
216
+ const { agentPublished, agentRemoteAt, publishAgentBranch, stopAgentPublishing, fetchPublishedBranch, fetchPublishedBranchAsync } =
202
217
  createWorkAgentPublish({ repoRoot, gitNet });
218
+ const { reconcileIdleArtifacts } = createWorkArtifactReconcile({
219
+ artifacts,
220
+ placeLocks,
221
+ inPlace,
222
+ placeDir,
223
+ beforeArtifacts,
224
+ getArtifactsAccepted,
225
+ log: (line) => warn(line),
226
+ });
203
227
  const { reportSessionWorktree, reportPlaceWorktrees, burstListeners, reportWorktrees } =
204
228
  createWorkPlaceReports({
229
+ repositoryPlaces,
205
230
  repoRoot,
206
231
  baseRef,
207
232
  placeOf,
@@ -237,8 +262,9 @@ export function createWorkManager({
237
262
  const { fetchAttachments, fetchAgentFiles } = createWorkAttachments();
238
263
  const { sessionRuntime, agyRegistryLookup } = createWorkSessionRuntime({ sessionMetaPath });
239
264
  const { carryDirtyState } = createAdoptCarry({ sessionMetaPath });
240
- const { processWorkTurns, workAnswering } = createWorkSessionTurns({
265
+ const { processWorkTurns, interruptWorkTurns, workAnswering } = createWorkSessionTurns({
241
266
  repoRoot,
267
+ baseRef,
242
268
  baseDir,
243
269
  getMcpUrl,
244
270
  getArtifactsAccepted,
@@ -276,15 +302,10 @@ export function createWorkManager({
276
302
  * which lanes share which map (workChildren: plans, review, turns; the
277
303
  * publication maps: turns and merges).
278
304
  */
279
-
280
- const { processDiffJobs } = createWorkDiffs({
281
- repoRoot,
282
- });
283
-
305
+ const { processDiffJobs } = createWorkDiffs({ repoRoot });
284
306
  const { processPreviewJobs, livePreviewIds, retirePreviews, shutdownPreviews } = createWorkPreviews({
285
307
  placeDir,
286
308
  });
287
-
288
309
  const { processPrJobs } = createWorkPullRequests({
289
310
  placeOf,
290
311
  placeDir,
@@ -295,7 +316,6 @@ export function createWorkManager({
295
316
  landed,
296
317
  onRepoChanged,
297
318
  });
298
-
299
319
  const { processShipJobs } = createWorkShipper({
300
320
  inPlace,
301
321
  placeOf,
@@ -311,7 +331,6 @@ export function createWorkManager({
311
331
  shipping,
312
332
  pendingShipReports,
313
333
  });
314
-
315
334
  const { processAgentPlanJobs, planning } = createWorkAgentPlans({
316
335
  postBestEffort,
317
336
  baseDir,
@@ -322,7 +341,6 @@ export function createWorkManager({
322
341
  placeLocks,
323
342
  workChildren,
324
343
  });
325
-
326
344
  const { processIntakeJobs, intake } = createWorkIntake({
327
345
  postBestEffort,
328
346
  inPlace,
@@ -348,9 +366,9 @@ export function createWorkManager({
348
366
  workChildren,
349
367
  });
350
368
  const { runReviewEntry } = createWorkAgentReview({ runCheck, runPrecheck });
351
-
352
- const { processAgentTurnJobs, settleAgentTurns, agentTurns, agentChildren, agentReported } = createWorkAgentTurns({
353
- REJECT_RETRY_MS,
369
+ const processAgentRecoveryJobs = createAgentRecovery({ baseDir, repoRoot, fetchPublishedBranch: fetchPublishedBranchAsync, inPlace, placeWtFor, reportSessionWorktree });
370
+ const { processAgentTurnJobs, interruptAgentTurns, settleAgentTurns, agentTurns, agentChildren, agentReported } = createWorkAgentTurns({
371
+ agentRepositories, REJECT_RETRY_MS,
354
372
  baseRef,
355
373
  inPlace,
356
374
  baseDir,
@@ -364,15 +382,15 @@ export function createWorkManager({
364
382
  reportSessionWorktree,
365
383
  artifacts,
366
384
  runReviewEntry,
367
- publishAgentBranch,
385
+ publishAgentBranch, stopAgentPublishing,
368
386
  admit,
369
387
  workChildren,
370
388
  agentPublished,
371
389
  agentRemoteAt,
372
390
  fetchAgentFiles,
373
391
  });
374
-
375
392
  const { processAgentMergeJobs, agentMerges } = createWorkAgentMerges({
393
+ repositoryPlaces,
376
394
  repoRoot,
377
395
  inPlace,
378
396
  baseDir,
@@ -385,10 +403,9 @@ export function createWorkManager({
385
403
  agentPublished,
386
404
  });
387
405
 
388
- // Built after the agent lane: retirement stops a stopped agent's CLI, and
389
- // only the agent lane knows which child that is.
406
+ // Retirement asks the agent lane which CLI to stop.
390
407
  const { heldSessionIds, retireWorkSessions, freshenManualPlaces } = createWorkRetire({
391
- repoRoot,
408
+ repositoryPlaces, repoRoot,
392
409
  baseDir,
393
410
  baseRef,
394
411
  placeOf,
@@ -441,7 +458,7 @@ export function createWorkManager({
441
458
  return {
442
459
  flushWorkReports,
443
460
  learnPlaces,
444
- processWorkTurns,
461
+ processWorkTurns, interruptWorkTurns,
445
462
  processShipJobs,
446
463
  processDiffJobs,
447
464
  processKillJobs,
@@ -453,8 +470,10 @@ export function createWorkManager({
453
470
  shutdownPreviews,
454
471
  retireWorkSessions,
455
472
  reportWorktrees,
473
+ reconcileIdleArtifacts,
456
474
  shutdownWork,
457
475
  workBusy,
476
+ runningAgentCount: () => agentTurns.size,
458
477
  // The machine's own admission answer, and what it is counting. Handed to
459
478
  // the loop so the lanes it owns — the wiki cartographer (wikiRunner.mjs) — ask the
460
479
  // same question, and so the machine snapshot can charge memory to the work
@@ -464,7 +483,7 @@ export function createWorkManager({
464
483
  liveTurnCount,
465
484
  processAgentPlanJobs,
466
485
  processIntakeJobs,
467
- processAgentTurnJobs,
486
+ processAgentTurnJobs, processAgentRecoveryJobs, interruptAgentTurns,
468
487
  // Only the roster's stand-down calls this — a displacement, a removal or a
469
488
  // commanded stop (holder.mjs, `standDownDisplaced`); see its comment.
470
489
  // Exported rather than hooked into `shutdownWork` because the signal