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.
- package/README.md +41 -0
- package/bin/cli.mjs +44 -0
- package/bin/lib/agentAsk.mjs +123 -0
- package/bin/lib/agentBrief.mjs +36 -0
- package/bin/lib/agentFootprint.mjs +76 -0
- package/bin/lib/agentInternalWait.mjs +4 -3
- package/bin/lib/agentPlan.mjs +86 -7
- package/bin/lib/agentPrerequisiteContract.mjs +43 -0
- package/bin/lib/agentRepositories.mjs +48 -0
- package/bin/lib/agentTaskKinds.mjs +1 -1
- package/bin/lib/artifactContracts.mjs +91 -20
- package/bin/lib/artifactIteration.mjs +182 -0
- package/bin/lib/artifactRecovery.mjs +36 -0
- package/bin/lib/artifactReporter.mjs +183 -19
- package/bin/lib/artifacts.mjs +6 -3
- package/bin/lib/checkoutIdentity.mjs +25 -0
- package/bin/lib/claudeAuth.mjs +15 -5
- package/bin/lib/claudeInput.mjs +32 -0
- package/bin/lib/claudePosture.mjs +110 -14
- package/bin/lib/claudeSessions.mjs +20 -0
- package/bin/lib/claudeStream.mjs +20 -2
- package/bin/lib/codexProjectRead.mjs +63 -0
- package/bin/lib/codexResearch.mjs +16 -3
- package/bin/lib/commandSpecs.mjs +6 -0
- package/bin/lib/credentials.mjs +52 -17
- package/bin/lib/daemonLocks.mjs +26 -6
- package/bin/lib/desktopContract.mjs +3 -0
- package/bin/lib/discoveryContract.mjs +19 -0
- package/bin/lib/fleet.mjs +65 -10
- package/bin/lib/fleetReports.mjs +44 -3
- package/bin/lib/fleetRoster.mjs +1 -89
- package/bin/lib/fleetStartup.mjs +11 -14
- package/bin/lib/holder.mjs +4 -37
- package/bin/lib/hooks/mcpAllow.mjs +60 -0
- package/bin/lib/hooks/readGuard.mjs +13 -4
- package/bin/lib/imageArtifact.mjs +74 -0
- package/bin/lib/instance.mjs +20 -7
- package/bin/lib/landed.mjs +2 -2
- package/bin/lib/login.mjs +39 -4
- package/bin/lib/maintenance.mjs +47 -0
- package/bin/lib/maintenanceInventory.mjs +39 -0
- package/bin/lib/mcpInstall.mjs +163 -0
- package/bin/lib/mcpInventory.mjs +119 -0
- package/bin/lib/placeLock.mjs +9 -7
- package/bin/lib/planDependencies.mjs +70 -0
- package/bin/lib/postureTools.mjs +85 -0
- package/bin/lib/prWorkflow.mjs +15 -0
- package/bin/lib/projectLanded.mjs +16 -0
- package/bin/lib/projectRepositories.mjs +60 -0
- package/bin/lib/projectToolRuntimes.mjs +227 -3
- package/bin/lib/projectTools.mjs +63 -5
- package/bin/lib/projectWorktreeHome.mjs +25 -0
- package/bin/lib/prompts.mjs +170 -42
- package/bin/lib/repositoryBaseline.mjs +68 -0
- package/bin/lib/repositoryDelivery.mjs +64 -0
- package/bin/lib/repositoryMerge.mjs +116 -0
- package/bin/lib/repositoryPlaces.mjs +179 -0
- package/bin/lib/repositorySetup.mjs +58 -0
- package/bin/lib/repositoryTool.mjs +71 -0
- package/bin/lib/runTurn.mjs +58 -18
- package/bin/lib/runtimeAntigravity.mjs +2 -1
- package/bin/lib/runtimeCapabilities.mjs +77 -3
- package/bin/lib/runtimeClaude.mjs +31 -10
- package/bin/lib/runtimeCodex.mjs +28 -59
- package/bin/lib/runtimeDetection.mjs +6 -0
- package/bin/lib/runtimeEvents.mjs +12 -1
- package/bin/lib/runtimeOfferings.mjs +75 -3
- package/bin/lib/shipMerge.mjs +5 -2
- package/bin/lib/startProject.mjs +4 -4
- package/bin/lib/trace.mjs +8 -3
- package/bin/lib/turnDeclaration.mjs +88 -0
- package/bin/lib/turnDiagnostics.mjs +93 -0
- package/bin/lib/turnLock.mjs +48 -8
- package/bin/lib/turnProfile.mjs +15 -3
- package/bin/lib/work.mjs +48 -29
- package/bin/lib/workAgentBrief.mjs +92 -0
- package/bin/lib/workAgentBriefRunner.mjs +59 -0
- package/bin/lib/workAgentCheck.mjs +6 -5
- package/bin/lib/workAgentMerges.mjs +19 -2
- package/bin/lib/workAgentPlans.mjs +21 -70
- package/bin/lib/workAgentPrecheck.mjs +10 -5
- package/bin/lib/workAgentPublish.mjs +18 -5
- package/bin/lib/workAgentRecovery.mjs +85 -0
- package/bin/lib/workAgentReview.mjs +5 -3
- package/bin/lib/workAgentTurnBegun.mjs +6 -4
- package/bin/lib/workAgentTurnExecution.mjs +167 -29
- package/bin/lib/workAgentTurnOutcome.mjs +20 -5
- package/bin/lib/workAgentTurns.mjs +92 -12
- package/bin/lib/workArtifactReconcile.mjs +79 -0
- package/bin/lib/workCappedTurn.mjs +10 -0
- package/bin/lib/workCommandAudit.mjs +8 -1
- package/bin/lib/workDelivery.mjs +21 -0
- package/bin/lib/workDiffs.mjs +18 -11
- package/bin/lib/workLiveMessages.mjs +129 -0
- package/bin/lib/workPlaceReports.mjs +5 -0
- package/bin/lib/workPlaces.mjs +24 -3
- package/bin/lib/workRetire.mjs +5 -0
- package/bin/lib/workSessionTurns.mjs +219 -52
- package/bin/lib/workShip.mjs +17 -16
- package/bin/lib/workToolLog.mjs +70 -3
- package/bin/lib/workTurnShape.mjs +6 -4
- package/package.json +7 -3
- 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
|
|
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
|
+
}
|
package/bin/lib/turnLock.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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));
|
package/bin/lib/turnProfile.mjs
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|