flowviant 0.125.5 → 0.125.7
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 +4 -5
- package/bin/lib/agentInternalWait.mjs +1 -5
- package/bin/lib/agentPlan.mjs +13 -163
- package/bin/lib/agentRepositories.mjs +3 -1
- package/bin/lib/claudeInput.mjs +89 -10
- package/bin/lib/claudeMachineMcp.mjs +85 -0
- package/bin/lib/claudePersonal.mjs +6 -57
- package/bin/lib/claudePosture.mjs +6 -15
- package/bin/lib/claudeSettingsArg.mjs +67 -0
- package/bin/lib/claudeStream.mjs +30 -1
- package/bin/lib/claudeSubagentProgress.mjs +12 -28
- package/bin/lib/claudeSubagents.mjs +4 -2
- package/bin/lib/codexContract.mjs +52 -0
- package/bin/lib/codexFencedMount.mjs +275 -0
- package/bin/lib/codexPersonal.mjs +1 -265
- package/bin/lib/codexProjectRead.mjs +5 -4
- package/bin/lib/codexResearch.mjs +9 -3
- package/bin/lib/codexSubagentEvents.mjs +11 -7
- package/bin/lib/codexSubagents.mjs +55 -52
- package/bin/lib/codexToml.mjs +328 -0
- package/bin/lib/config.mjs +22 -1
- package/bin/lib/conversationIds.mjs +10 -0
- package/bin/lib/fleet.mjs +2 -2
- package/bin/lib/git.mjs +32 -0
- package/bin/lib/heldTool.mjs +49 -0
- package/bin/lib/hooks/mcpAllow.mjs +11 -5
- package/bin/lib/hooks/orchestrator.mjs +20 -10
- package/bin/lib/intakeDraft.mjs +1 -1
- package/bin/lib/jsonlTail.mjs +52 -0
- package/bin/lib/mcpInventory.mjs +11 -6
- package/bin/lib/mcpReasons.mjs +26 -0
- package/bin/lib/mcpReportBounds.mjs +10 -0
- package/bin/lib/mcpStdio.mjs +35 -0
- package/bin/lib/mergeBatch.mjs +6 -24
- package/bin/lib/mergeOrder.mjs +18 -0
- package/bin/lib/modelJson.mjs +173 -0
- package/bin/lib/nativeAgentTools.mjs +72 -0
- package/bin/lib/orchestrator.mjs +221 -133
- package/bin/lib/orchestratorClient.mjs +17 -21
- package/bin/lib/orchestratorHooks.mjs +21 -4
- package/bin/lib/orchestratorNative.mjs +58 -0
- package/bin/lib/orchestratorPrompt.mjs +7 -4
- package/bin/lib/orchestratorProtocol.mjs +25 -3
- package/bin/lib/postureTools.mjs +58 -13
- package/bin/lib/prWorkflow.mjs +13 -2
- package/bin/lib/processGroup.mjs +41 -0
- package/bin/lib/projectRepositories.mjs +2 -2
- package/bin/lib/projectToolRuntimes.mjs +92 -245
- package/bin/lib/projectTools.mjs +60 -19
- package/bin/lib/prompts.mjs +47 -18
- package/bin/lib/reportJournal.mjs +61 -0
- package/bin/lib/repositoryDelivery.mjs +6 -4
- package/bin/lib/repositoryMerge.mjs +8 -12
- package/bin/lib/repositoryPlaces.mjs +2 -2
- package/bin/lib/repositoryPrompts.mjs +42 -0
- package/bin/lib/repositoryTool.mjs +9 -32
- package/bin/lib/researchUse.mjs +39 -0
- package/bin/lib/resumeLost.mjs +21 -1
- package/bin/lib/resumeMarker.mjs +43 -0
- package/bin/lib/runTurn.mjs +55 -52
- package/bin/lib/runtimeAntigravity.mjs +1 -1
- package/bin/lib/runtimeCapabilities.mjs +13 -18
- package/bin/lib/runtimeClaude.mjs +68 -24
- package/bin/lib/runtimeCodex.mjs +12 -6
- package/bin/lib/runtimeEvents.mjs +13 -2
- package/bin/lib/runtimeOfferings.mjs +12 -5
- package/bin/lib/runtimes.mjs +3 -1
- package/bin/lib/startProject.mjs +10 -0
- package/bin/lib/subagentBounds.mjs +20 -0
- package/bin/lib/trace.mjs +30 -5
- package/bin/lib/turnDeclaration.mjs +1 -1
- package/bin/lib/turnDiagnostics.mjs +9 -12
- package/bin/lib/turnReceipts.mjs +64 -0
- package/bin/lib/turnSubagents.mjs +91 -0
- package/bin/lib/turnUsage.mjs +9 -6
- package/bin/lib/update.mjs +18 -4
- package/bin/lib/versionOrder.mjs +11 -1
- package/bin/lib/work.mjs +9 -9
- package/bin/lib/workAgentMerges.mjs +5 -7
- package/bin/lib/workAgentPrecheck.mjs +5 -5
- package/bin/lib/workAgentRecovery.mjs +2 -1
- package/bin/lib/workAgentResume.mjs +121 -0
- package/bin/lib/workAgentTools.mjs +111 -0
- package/bin/lib/workAgentTurnExecution.mjs +82 -246
- package/bin/lib/workAgentTurnOutcome.mjs +30 -3
- package/bin/lib/workAgentTurnReports.mjs +16 -0
- package/bin/lib/workAgentTurnShape.mjs +90 -0
- package/bin/lib/workAgentTurns.mjs +28 -12
- package/bin/lib/workArtifactReconcile.mjs +3 -2
- package/bin/lib/workAttachments.mjs +27 -5
- package/bin/lib/workMergeBatch.mjs +7 -5
- package/bin/lib/workNarration.mjs +32 -0
- package/bin/lib/workPlaceReports.mjs +3 -3
- package/bin/lib/workPlaces.mjs +14 -0
- package/bin/lib/workReportQueue.mjs +18 -1
- package/bin/lib/workSessionRuntime.mjs +2 -5
- package/bin/lib/workSessionTurns.mjs +68 -80
- package/bin/lib/workShip.mjs +8 -18
- package/bin/lib/workTurnShape.mjs +6 -4
- package/bin/lib/worktreeDiff.mjs +29 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
Run your own coding CLIs as build agents for [Flowviant](https://flowviant.com) — [Claude Code](https://claude.com/claude-code), Codex or Antigravity, on your own credentials. This daemon holds your sessions, keeps a worktree per tab, and ships branches on your word. The daemon does not send your Claude, Codex or GitHub logins to Flowviant; it shells out to CLIs you signed in yourself.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
|
-
curl -fsSL https://api.flowviant.com/install.sh | sh
|
|
7
|
-
|
|
6
|
+
curl -fsSL https://api.flowviant.com/install.sh | sh && ~/.flowviant/bin/flowviant
|
|
7
|
+
# approve the code in Flowviant → connected; new terminals just run `flowviant`
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
Login keeps going straight into the daemon — there is no second command to run.
|
|
@@ -31,11 +31,10 @@ On the machine that runs the daemon:
|
|
|
31
31
|
Download the standalone binary, then start it in your checkout:
|
|
32
32
|
|
|
33
33
|
```sh
|
|
34
|
-
curl -fsSL https://api.flowviant.com/install.sh | sh
|
|
35
|
-
flowviant
|
|
34
|
+
curl -fsSL https://api.flowviant.com/install.sh | sh && ~/.flowviant/bin/flowviant
|
|
36
35
|
```
|
|
37
36
|
|
|
38
|
-
It shows a short code. Open your project's **Workbench** in Flowviant and enter the code where it offers to connect a machine. The credential is stored at `~/.flowviant/credentials.json`, and from then on `flowviant` just runs. The installer prints a PATH hint if needed; `FLOWVIANT_INSTALL_DIR` changes its destination. It verifies SHA-256 and never uses sudo.
|
|
37
|
+
The full path is for this first run only: the installer adds its folder to your shell's startup file, which new terminals read. It shows a short code. Open your project's **Workbench** in Flowviant and enter the code where it offers to connect a machine. The credential is stored at `~/.flowviant/credentials.json`, and from then on `flowviant` just runs. The installer prints a PATH hint if needed; `FLOWVIANT_INSTALL_DIR` changes its destination. It verifies SHA-256 and never uses sudo.
|
|
39
38
|
|
|
40
39
|
Run `flowviant login` from another checkout and choose the same project in the
|
|
41
40
|
browser to add its repository to that board. Project settings → General lists
|
|
@@ -25,11 +25,7 @@ export async function runAgentWithInternalWaits(run, options) {
|
|
|
25
25
|
let cliFailed = false;
|
|
26
26
|
let lastAnswer = null;
|
|
27
27
|
let currentUsage = null;
|
|
28
|
-
const combine = () =>
|
|
29
|
-
if (!currentUsage) return total;
|
|
30
|
-
return Object.fromEntries(['input', 'output', 'cacheCreate', 'cacheRead'].map((k) =>
|
|
31
|
-
[k, (total?.[k] ?? 0) + (currentUsage[k] ?? 0)]));
|
|
32
|
-
};
|
|
28
|
+
const combine = () => currentUsage ? addUsage(total, currentUsage) : total;
|
|
33
29
|
const out = await run({
|
|
34
30
|
...options,
|
|
35
31
|
...(attempt ? {
|
package/bin/lib/agentPlan.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* READING A MODEL'S ANSWER, in the agent lane's shapes: a planner's
|
|
3
|
-
* PROPOSAL, an agent's own TURN RESULT
|
|
4
|
-
*
|
|
3
|
+
* PROPOSAL, an agent's own TURN RESULT and the AI pre-review's TRIAGE. The
|
|
4
|
+
* objects themselves are found by modelJson.mjs (2026-10-18).
|
|
5
5
|
*
|
|
6
6
|
* The scratch agent behind a Deploy press is asked for one JSON object and
|
|
7
7
|
* nothing else. This is what turns its final message into a proposal, and it is
|
|
@@ -32,6 +32,8 @@ import { scrub as envScrub, scrubCut } from './uplinkScrub.mjs';
|
|
|
32
32
|
import { groupDependencies } from './planDependencies.mjs';
|
|
33
33
|
import { AGENT_TASK_KINDS } from './agentTaskKinds.mjs';
|
|
34
34
|
import { blockedAnswer } from './agentAsk.mjs';
|
|
35
|
+
import { isResearchUse } from './researchUse.mjs';
|
|
36
|
+
import { candidateObjects } from './modelJson.mjs';
|
|
35
37
|
|
|
36
38
|
/** The biggest a single proposal may be. Bounds on a machine, not a policy:
|
|
37
39
|
* the server caps these again at its own boundary. */
|
|
@@ -40,164 +42,6 @@ const MAX_TASKS_PER_AGENT = 60;
|
|
|
40
42
|
const MAX_NAME = 80;
|
|
41
43
|
const MAX_NOTE = 1000;
|
|
42
44
|
|
|
43
|
-
/**
|
|
44
|
-
* Find the objects.
|
|
45
|
-
*
|
|
46
|
-
* A fenced block first, because that is what was asked for. Otherwise every `{`
|
|
47
|
-
* in the text is tried as a start, and its BALANCED end is found by counting
|
|
48
|
-
* braces while skipping string literals — each caller then takes the first
|
|
49
|
-
* candidate whose SHAPE is the one it asked for.
|
|
50
|
-
*
|
|
51
|
-
* The obvious cheap version — first `{` to last `}` — is wrong in a way a test
|
|
52
|
-
* caught: a planner that writes "I looked at {the auth module} first" before its
|
|
53
|
-
* JSON produces a span starting at the wrong brace, and the whole plan is lost
|
|
54
|
-
* to a sentence. Scanning candidates costs nothing at this size and cannot be
|
|
55
|
-
* defeated by prose.
|
|
56
|
-
*
|
|
57
|
-
* ONE SCANNER FOR EVERY READER (2026-09-16). It was written
|
|
58
|
-
* twice — once here and once inline in `parseTurnResult` — and a third copy was
|
|
59
|
-
* about to be written for the precheck. A brace scanner that skips string
|
|
60
|
-
* literals is exactly the kind of thing where two copies quietly stop agreeing
|
|
61
|
-
* about escapes and nobody notices, because the disagreement only shows up on a
|
|
62
|
-
* card title with a quote in it.
|
|
63
|
-
*/
|
|
64
|
-
/**
|
|
65
|
-
* BOUNDED, BECAUSE THE TEXT IS A MODEL'S (2026-09-24, the audit).
|
|
66
|
-
*
|
|
67
|
-
* The scan used to walk from EVERY `{` to the end of the text, and parse every
|
|
68
|
-
* balanced span it found — quadratic on both counts. A final message of eighty
|
|
69
|
-
* thousand `{` (a quoted fixture, brace padding, a turn steered into writing
|
|
70
|
-
* one) took nine seconds of synchronous CPU, and while it ran nothing else on
|
|
71
|
-
* the machine moved: no poll, no lease renewal, no other lane. Three bounds,
|
|
72
|
-
* none of which a real answer comes near:
|
|
73
|
-
*
|
|
74
|
-
* · top-level objects are scanned whole, including long un-fenced replies;
|
|
75
|
-
* `fencedBody` and the brace scan are linear, with no prose-length slice;
|
|
76
|
-
* · each `{`'s closing brace is found ONCE (see `closers`), so the scan is a
|
|
77
|
-
* single right-to-left pass instead of one walk per brace;
|
|
78
|
-
* · only spans that can open an object (`{` then a key or `}`) are handed to
|
|
79
|
-
* JSON.parse. Every TOP-LEVEL span is parsed — top-level spans are
|
|
80
|
-
* disjoint, so together they are at most the scanned window — and the
|
|
81
|
-
* NESTED ones (a span inside an earlier candidate) share a budget of
|
|
82
|
-
* `MAX_NESTED_PARSES` spans and `MAX_NESTED_CHARS` characters, so a run of
|
|
83
|
-
* nested braces that parse is not re-parsed thousands of times.
|
|
84
|
-
*
|
|
85
|
-
* THE BUDGET NEVER REACHES A TOP-LEVEL SPAN, and the review found why it must
|
|
86
|
-
* not: the first cut counted every span from the left against one budget, so
|
|
87
|
-
* an answer that followed 250 JSON lines of a quoted fixture (or one large
|
|
88
|
-
* nested document) was never parsed at all — the turn read as `nothing` and a
|
|
89
|
-
* delivered agent went to Stuck. The contract puts the answer LAST and at the
|
|
90
|
-
* top level, which is the one span a budget must never cost.
|
|
91
|
-
*/
|
|
92
|
-
const MAX_NESTED_PARSES = 200;
|
|
93
|
-
const MAX_NESTED_CHARS = 4 * 1024 * 1024;
|
|
94
|
-
|
|
95
|
-
export function candidateObjects(text) {
|
|
96
|
-
const fenced = fencedBody(text);
|
|
97
|
-
const raw = text;
|
|
98
|
-
const bodies = [];
|
|
99
|
-
if (fenced !== null) bodies.push(fenced);
|
|
100
|
-
const close = closers(raw);
|
|
101
|
-
let topEnd = -1;
|
|
102
|
-
let nestedParses = 0;
|
|
103
|
-
let nestedChars = 0;
|
|
104
|
-
for (let i = 0; i < raw.length; i++) {
|
|
105
|
-
if (raw[i] !== '{') continue;
|
|
106
|
-
const end = close.get(i);
|
|
107
|
-
if (!(end > i) || !/^\{\s*["}]/.test(raw.slice(i, i + 64))) continue;
|
|
108
|
-
if (i > topEnd) {
|
|
109
|
-
topEnd = end;
|
|
110
|
-
} else {
|
|
111
|
-
const len = end - i + 1;
|
|
112
|
-
if (nestedParses >= MAX_NESTED_PARSES || nestedChars + len > MAX_NESTED_CHARS) continue;
|
|
113
|
-
nestedParses++;
|
|
114
|
-
nestedChars += len;
|
|
115
|
-
}
|
|
116
|
-
bodies.push(raw.slice(i, end + 1));
|
|
117
|
-
}
|
|
118
|
-
const out = [];
|
|
119
|
-
for (const body of bodies) {
|
|
120
|
-
if (!body.trim()) continue;
|
|
121
|
-
try {
|
|
122
|
-
const v = JSON.parse(body);
|
|
123
|
-
if (v && typeof v === 'object') out.push(v);
|
|
124
|
-
} catch {
|
|
125
|
-
/* the next candidate may be the object */
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return out;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* THE FIRST FENCED BLOCK'S BODY, or null — what
|
|
133
|
-
* `text.match(/```(?:json)?\s*([\s\S]*?)```/)[1]` answered, found in ONE
|
|
134
|
-
* forward pass (2026-10-10, the audit's F079).
|
|
135
|
-
*
|
|
136
|
-
* That regex ran over the WHOLE text and backtracks quadratically on an opening fence
|
|
137
|
-
* followed by a long whitespace run with no closing fence: the greedy `\s*` and
|
|
138
|
-
* the lazy body re-scan the run from every split. Measured: 80 KB took 0.6 s,
|
|
139
|
-
* 320 KB nine seconds, of synchronous CPU in which no poll, lease or other
|
|
140
|
-
* lane moved — and a turn's stdout is model-written and uncapped. Same
|
|
141
|
-
* answer, by `indexOf`: the first ``` anywhere, an optional `json`, the
|
|
142
|
-
* whitespace after it skipped, and the body runs to the next ```. If that
|
|
143
|
-
* opening has no closer, no later opening can have one either, so there is no
|
|
144
|
-
* fence. The search stays over the whole text.
|
|
145
|
-
*/
|
|
146
|
-
const FENCE_SPACE = /\s*/y;
|
|
147
|
-
function fencedBody(text) {
|
|
148
|
-
const open = text.indexOf('```');
|
|
149
|
-
if (open < 0) return null;
|
|
150
|
-
let at = open + 3;
|
|
151
|
-
if (text.startsWith('json', at)) at += 4;
|
|
152
|
-
FENCE_SPACE.lastIndex = at;
|
|
153
|
-
FENCE_SPACE.exec(text);
|
|
154
|
-
at = FENCE_SPACE.lastIndex;
|
|
155
|
-
const close = text.indexOf('```', at);
|
|
156
|
-
return close < 0 ? null : text.slice(at, close);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* For every `{`, the index of the `}` that closes it when read as the START of
|
|
161
|
-
* an object (outside any string), or -1 — the answer `balanced` gives, for all
|
|
162
|
-
* of them in one pass.
|
|
163
|
-
*
|
|
164
|
-
* Right to left, so every `{` AFTER the one being closed already has its
|
|
165
|
-
* answer. A walk that meets such a `{` outside a string jumps straight to that
|
|
166
|
-
* brace's closer — the string state from there on is the same for both walks,
|
|
167
|
-
* so the nested object ends where it ends — and if that brace never closes,
|
|
168
|
-
* neither does this one. What is left for each walk is its own top level.
|
|
169
|
-
*/
|
|
170
|
-
// The conversation-prose filter uses the same brace/string boundaries.
|
|
171
|
-
export { closers as jsonObjectClosers };
|
|
172
|
-
function closers(s) {
|
|
173
|
-
const close = new Map();
|
|
174
|
-
for (let i = s.lastIndexOf('{'); i >= 0; i = i > 0 ? s.lastIndexOf('{', i - 1) : -1) {
|
|
175
|
-
let inStr = false;
|
|
176
|
-
let esc = false;
|
|
177
|
-
let end = -1;
|
|
178
|
-
for (let k = i + 1; k < s.length; k++) {
|
|
179
|
-
const ch = s[k];
|
|
180
|
-
if (inStr) {
|
|
181
|
-
if (esc) esc = false;
|
|
182
|
-
else if (ch === '\\') esc = true;
|
|
183
|
-
else if (ch === '"') inStr = false;
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
if (ch === '"') inStr = true;
|
|
187
|
-
else if (ch === '{') {
|
|
188
|
-
const inner = close.get(k);
|
|
189
|
-
if (inner === undefined || inner < 0) break;
|
|
190
|
-
k = inner;
|
|
191
|
-
} else if (ch === '}') {
|
|
192
|
-
end = k;
|
|
193
|
-
break;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
close.set(i, end);
|
|
197
|
-
}
|
|
198
|
-
return close;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
45
|
function extract(raw) {
|
|
202
46
|
return candidateObjects(raw).find((v) => Array.isArray(v.agents)) ?? null;
|
|
203
47
|
}
|
|
@@ -411,7 +255,7 @@ export function parseTurnResult(text, scrub = envScrub) {
|
|
|
411
255
|
if (v.status === 'delivered') {
|
|
412
256
|
return {
|
|
413
257
|
outcome: 'delivered',
|
|
414
|
-
...(
|
|
258
|
+
...(isResearchUse(v.researchUse) ? { researchUse: v.researchUse } : {}),
|
|
415
259
|
...(Array.isArray(v.mergeOrder) ? { mergeOrder: v.mergeOrder.filter((id) => typeof id === 'string').slice(0, 64) } : {}),
|
|
416
260
|
...(typeof v.mergeOrderReason === 'string' ? { mergeOrderReason: clean(v.mergeOrderReason, 1000) } : {}),
|
|
417
261
|
...(Array.isArray(v.mergeOrders) ? { mergeOrders: v.mergeOrders.slice(0, 60) } : {}),
|
|
@@ -447,6 +291,12 @@ export const MAX_PRECHECK_OVERALL = 1200;
|
|
|
447
291
|
export const MAX_PRECHECK_CRITERIA = 12;
|
|
448
292
|
export const MAX_PRECHECK_CRITERION = 200;
|
|
449
293
|
export const MAX_PRECHECK_LINE = 160;
|
|
294
|
+
/** The verdict words the server's boundary admits (shared `agents.schema.ts`
|
|
295
|
+
* PRECHECK_CARD_VERDICTS / PRECHECK_CRITERION_VERDICTS, 2026-10-18), held
|
|
296
|
+
* equal by the release gate (rule 31): a word kept here and refused there
|
|
297
|
+
* is a reading the panel never draws. */
|
|
298
|
+
export const PRECHECK_CARD_VERDICTS = ['ok', 'concerns'];
|
|
299
|
+
export const PRECHECK_CRITERION_VERDICTS = ['met', 'concern', 'unclear'];
|
|
450
300
|
|
|
451
301
|
/**
|
|
452
302
|
* ONE CARD'S CRITERIA, as the reader judged them (2026-10-10).
|
|
@@ -464,7 +314,7 @@ function precheckCriteria(raw, scrub) {
|
|
|
464
314
|
if (out.length >= MAX_PRECHECK_CRITERIA) break;
|
|
465
315
|
if (!k || typeof k !== 'object') continue;
|
|
466
316
|
if (typeof k.criterion !== 'string' || !k.criterion.trim()) continue;
|
|
467
|
-
if (
|
|
317
|
+
if (!PRECHECK_CRITERION_VERDICTS.includes(k.verdict)) continue;
|
|
468
318
|
if (k.line != null && typeof k.line !== 'string') continue;
|
|
469
319
|
const criterion = scrubCut(k.criterion.trim(), MAX_PRECHECK_CRITERION, scrub);
|
|
470
320
|
const line = typeof k.line === 'string' ? scrubCut(k.line.trim(), MAX_PRECHECK_LINE, scrub) : '';
|
|
@@ -528,7 +378,7 @@ export function parsePrecheck(text, scrub = envScrub) {
|
|
|
528
378
|
if (!c || typeof c !== 'object') continue;
|
|
529
379
|
const taskId = typeof c.taskId === 'string' ? c.taskId.trim().slice(0, 64) : '';
|
|
530
380
|
if (!taskId || seen.has(taskId)) continue;
|
|
531
|
-
if (
|
|
381
|
+
if (!PRECHECK_CARD_VERDICTS.includes(c.verdict)) continue;
|
|
532
382
|
seen.add(taskId);
|
|
533
383
|
// SCRUB, THEN CUT — see the docblock. The whole field is in hand here, so an
|
|
534
384
|
// exact-value match still finds a secret that spans the cap.
|
|
@@ -4,6 +4,7 @@ import { FLEET_URL, DAEMON_INSTANCE } from './config.mjs';
|
|
|
4
4
|
import { fleetEndpoint } from './fleetWire.mjs';
|
|
5
5
|
import { fleetRequest } from './fleetPost.mjs';
|
|
6
6
|
import { createRepositoryTool } from './repositoryTool.mjs';
|
|
7
|
+
import { repositoryNote } from './repositoryPrompts.mjs';
|
|
7
8
|
|
|
8
9
|
export function createAgentRepositories({ repositories, places, report = async (body) => {
|
|
9
10
|
const r = await fleetRequest(fleetEndpoint('agent-repositories', FLEET_URL), { body: { ...body, instance: DAEMON_INSTANCE } });
|
|
@@ -36,10 +37,11 @@ export function createAgentRepositories({ repositories, places, report = async (
|
|
|
36
37
|
return { repository: repo.name, path: row.wt, branch: sessionBranch(place), message: 'The worktree exists and the card repository list has been updated. Your next turn receives this directory as writable. Do not edit its former read-only snapshot.' };
|
|
37
38
|
}) : null;
|
|
38
39
|
const rows = places.entries(place);
|
|
40
|
+
const conflicted = job.kind === 'merge_resolve' ? rows.filter((r) => r.writable).map((r) => ({ repository: r.name, path: r.wt, files: gitOrNull(['diff', '--name-only', '--diff-filter=U'], r.wt) })) : null;
|
|
39
41
|
return { ...dir, tool, before,
|
|
40
42
|
sources: rows.filter((r) => ids.includes(r.id)),
|
|
41
43
|
writableDirs: rows.filter((r) => ids.includes(r.id) && r.wt !== dir.wt).map((r) => r.wt),
|
|
42
|
-
note:
|
|
44
|
+
note: repositoryNote({ repositoryCards: job.repositoryCards, conflicted, rows, ids, branch: sessionBranch(place), toolOffered: Boolean(tool) }),
|
|
43
45
|
sourcesNow: () => places.entries(place).filter((r) => r.writable),
|
|
44
46
|
receipts: () => places.receipts(place, before),
|
|
45
47
|
};
|
package/bin/lib/claudeInput.mjs
CHANGED
|
@@ -1,32 +1,111 @@
|
|
|
1
1
|
/** Claude's stream-json stdin. A write is only an attempt; its replay echo is
|
|
2
|
-
* the receipt. Stop admitting at
|
|
3
|
-
*
|
|
4
|
-
|
|
2
|
+
* the receipt. Stop admitting at a result, drain already-written messages,
|
|
3
|
+
* then close stdin so the child and its one logical turn can end.
|
|
4
|
+
*
|
|
5
|
+
* …UNLESS NATIVE AGENTS STILL RUN IN THE BACKGROUND (2026-10-17). Claude Code
|
|
6
|
+
* 2.1.302 lists them on the parent stream (`background_tasks_changed`), emits
|
|
7
|
+
* a parent result while they run, and with stdin open takes a later message
|
|
8
|
+
* as a new parent turn (measured): live notes, child questions and slot wakes
|
|
9
|
+
* still have a reader. Input closes at a result that finds no agent listed and
|
|
10
|
+
* nothing written pending — never at the emptied list itself: the agents'
|
|
11
|
+
* notification turn follows it and may start the next wave, and a slot wake
|
|
12
|
+
* for that wave arrives just then (both measured). Only agents hold it: a
|
|
13
|
+
* background shell (a dev server) may never end, and the CLI's own wind-down
|
|
14
|
+
* waits on those. A CLI that never lists tasks closes at its first result. */
|
|
15
|
+
/**
|
|
16
|
+
* THE CLI'S OWN CONTEXT MEASURE (2026-10-19). `get_context_usage` with
|
|
17
|
+
* `detail: 'summary'` answers from the last response's usage and local
|
|
18
|
+
* estimates — no model call and no token-count request (measured offline on
|
|
19
|
+
* 2.1.303). It names the window, the tokens in it and the native auto-compact
|
|
20
|
+
* threshold, so `compactAt` is the CLI's, never a guess; an auto-compact that
|
|
21
|
+
* is off has none (null). A CLI that answers an error, or never answers,
|
|
22
|
+
* reports nothing: unknown draws nothing (ContextReadout.tsx).
|
|
23
|
+
*/
|
|
24
|
+
export function contextFromUsage(r) {
|
|
25
|
+
const used = r?.totalTokens;
|
|
26
|
+
const window = r?.maxTokens;
|
|
27
|
+
if (!Number.isSafeInteger(used) || used < 0 || !Number.isSafeInteger(window) || window <= 0) return null;
|
|
28
|
+
const threshold = r.isAutoCompactEnabled === true && Number.isFinite(r.autoCompactThreshold) && r.autoCompactThreshold > 0
|
|
29
|
+
? Math.min(1, r.autoCompactThreshold / window)
|
|
30
|
+
: null;
|
|
31
|
+
return { usedTokens: used, windowTokens: window, compactAt: threshold, compacting: false };
|
|
32
|
+
}
|
|
33
|
+
const CONTEXT_EVERY_MS = 3_000;
|
|
34
|
+
|
|
35
|
+
export function createClaudeInput(child, { onTaken = () => {}, onContext = null } = {}) {
|
|
5
36
|
const sent = new Set();
|
|
6
37
|
const pending = new Set();
|
|
7
|
-
let
|
|
38
|
+
let draining = false; // a result found no live agent: admit nothing, close once pending is read
|
|
39
|
+
let broken = false;
|
|
8
40
|
let ended = false;
|
|
9
41
|
let resultSeen = false;
|
|
10
|
-
|
|
42
|
+
let agents = 0; // live background agents, as the parent stream last listed them
|
|
43
|
+
child.stdin.on('error', () => { broken = true; }); // EPIPE leaves the message queued
|
|
44
|
+
const end = () => { if (!ended) { ended = true; child.stdin.end(); } };
|
|
45
|
+
// The context readout's asks: one in flight, at most one per window, and
|
|
46
|
+
// one more owed when something moved while it was out — at once when what
|
|
47
|
+
// moved was a compaction ('now'), else on the window ('later').
|
|
48
|
+
let ctxSeq = 0;
|
|
49
|
+
let ctxAsked = null;
|
|
50
|
+
let ctxAt = 0;
|
|
51
|
+
let ctxOwed = null;
|
|
52
|
+
let compacting = false;
|
|
53
|
+
let lastContext = null;
|
|
54
|
+
const sayContext = (ctx) => {
|
|
55
|
+
lastContext = ctx;
|
|
56
|
+
try { onContext(ctx); } catch { /* a readout never fails a turn */ }
|
|
57
|
+
};
|
|
58
|
+
const askContext = (now = false) => {
|
|
59
|
+
if (!onContext || ended || broken || child.stdin.destroyed || child.stdin.writableEnded) return;
|
|
60
|
+
if (ctxAsked || (!now && Date.now() - ctxAt < CONTEXT_EVERY_MS)) {
|
|
61
|
+
if (ctxOwed !== 'now') ctxOwed = now ? 'now' : 'later';
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
ctxAsked = `fv-context-${++ctxSeq}`;
|
|
65
|
+
ctxAt = Date.now();
|
|
66
|
+
ctxOwed = null;
|
|
67
|
+
const request = { type: 'control_request', request_id: ctxAsked, request: { subtype: 'get_context_usage', detail: 'summary' } };
|
|
68
|
+
try { child.stdin.write(JSON.stringify(request) + '\n'); } catch { ctxAsked = null; }
|
|
69
|
+
};
|
|
11
70
|
const send = (id, prompt) => {
|
|
12
|
-
if (
|
|
71
|
+
if (draining || broken || ended || sent.has(id) || child.stdin.destroyed || child.stdin.writableEnded) return false;
|
|
13
72
|
const message = { type: 'user', uuid: id, session_id: '', message: { role: 'user', content: prompt }, parent_tool_use_id: null };
|
|
14
73
|
sent.add(id);
|
|
15
74
|
pending.add(id);
|
|
16
75
|
try { child.stdin.write(JSON.stringify(message) + '\n'); return true; }
|
|
17
|
-
catch { pending.delete(id);
|
|
76
|
+
catch { pending.delete(id); broken = true; return false; }
|
|
18
77
|
};
|
|
19
78
|
return {
|
|
20
79
|
send,
|
|
21
80
|
event(event) {
|
|
22
81
|
if (event.type === 'user' && event.isReplay === true && pending.delete(event.uuid)) onTaken(event.uuid);
|
|
82
|
+
if (event.type === 'control_response' && ctxAsked && event.response?.request_id === ctxAsked) {
|
|
83
|
+
ctxAsked = null;
|
|
84
|
+
const ctx = event.response.subtype === 'success' ? contextFromUsage(event.response.response) : null;
|
|
85
|
+
if (ctx) sayContext({ ...ctx, compacting });
|
|
86
|
+
if (ctxOwed) askContext(ctxOwed === 'now');
|
|
87
|
+
}
|
|
88
|
+
if (event.type === 'system' && event.subtype === 'init') askContext(true);
|
|
89
|
+
if (event.type === 'assistant') askContext();
|
|
90
|
+
if (event.type === 'system' && event.subtype === 'status') {
|
|
91
|
+
const now = event.status === 'compacting';
|
|
92
|
+
if (now && !compacting && lastContext) sayContext({ ...lastContext, compacting: true });
|
|
93
|
+
if (!now && compacting) askContext(true);
|
|
94
|
+
compacting = now;
|
|
95
|
+
}
|
|
96
|
+
if (event.type === 'system' && event.subtype === 'compact_boundary') { compacting = false; askContext(true); }
|
|
97
|
+
if (event.type === 'system' && event.subtype === 'background_tasks_changed' && Array.isArray(event.tasks)) {
|
|
98
|
+
agents = event.tasks.filter(task => task?.task_type === 'local_agent').length;
|
|
99
|
+
if (agents > 0) draining = false; // a drained message's turn started agents
|
|
100
|
+
}
|
|
23
101
|
if (event.type === 'result') {
|
|
24
102
|
resultSeen = true;
|
|
25
|
-
|
|
26
|
-
if (
|
|
103
|
+
if (event.is_error === true) end();
|
|
104
|
+
else if (agents > 0) draining = false;
|
|
105
|
+
else { draining = true; if (pending.size === 0) end(); }
|
|
27
106
|
}
|
|
28
107
|
},
|
|
29
|
-
close() {
|
|
108
|
+
close() { ended = true; },
|
|
30
109
|
get resultSeen() { return resultSeen; },
|
|
31
110
|
};
|
|
32
111
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE MACHINE'S OWN MCP SERVERS THAT A CARD TURN'S SETTINGS SOURCES NEVER
|
|
3
|
+
* REACH (2026-10-17). The 2026-10-11 ruling mounts "the user and local
|
|
4
|
+
* servers" on every card turn, and neither arrived where the person keeps
|
|
5
|
+
* them. MEASURED on Claude Code 2.1.302, a scratch CLAUDE_CONFIG_DIR whose
|
|
6
|
+
* `.claude.json` held one user and one local server:
|
|
7
|
+
* · `--setting-sources ''` (every fenced posture: write-up, mockup, 3D model,
|
|
8
|
+
* deck) loaded NEITHER — a research card on the owner's box saw the
|
|
9
|
+
* claude.ai connectors and no Roblox_Studio, a user-scope server;
|
|
10
|
+
* · `--setting-sources user` (a code card) loaded the user server only;
|
|
11
|
+
* · a local server (`claude mcp add`'s default scope) is keyed on the
|
|
12
|
+
* CANONICAL git root — the main checkout, which the CLI also resolves a
|
|
13
|
+
* linked worktree to (re-measured 2026-10-17: a worktree cwd with no
|
|
14
|
+
* `--setting-sources` loaded it). What drops it on a card is the `local`
|
|
15
|
+
* source the card's list leaves out, not the worktree's path.
|
|
16
|
+
* So the lane names what is missing and the adapter hands it over through a
|
|
17
|
+
* private `--mcp-config` file (runtimeClaude.mjs): `user` when the turn's
|
|
18
|
+
* sources drop the user scope, and the checkout's local servers always. A
|
|
19
|
+
* local name shadows a user one, the CLI's own precedence; an `--mcp-config`
|
|
20
|
+
* server beat a user server of the same name (measured), so a code card keeps
|
|
21
|
+
* that order too.
|
|
22
|
+
*
|
|
23
|
+
* ITS OWN MODULE (2026-10-18, the SOLID pass). It sat in claudePersonal.mjs,
|
|
24
|
+
* whose ruling is the person's settings allowlist and private settings file
|
|
25
|
+
* (2026-09-29, 2026-10-10), and was the only reason that module imported git;
|
|
26
|
+
* the agent lane imported claudePersonal.mjs for this function alone. Where
|
|
27
|
+
* Claude Code keeps user and local servers in `.claude.json` is a different
|
|
28
|
+
* fact, which moves on its own: a change to how the CLI keys them, or a new
|
|
29
|
+
* scope, opens this file and no other (claudeMachineMcp.test.mjs pins the
|
|
30
|
+
* file name to here).
|
|
31
|
+
*/
|
|
32
|
+
import { readFileSync, realpathSync, statSync } from 'node:fs';
|
|
33
|
+
import { homedir } from 'node:os';
|
|
34
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
35
|
+
import { gitOrNull } from './git.mjs';
|
|
36
|
+
|
|
37
|
+
/** Far past any real CLI config — `.claude.json` also holds every project's
|
|
38
|
+
* history; a bigger one is not read. */
|
|
39
|
+
const CLI_CONFIG_MAX_BYTES = 32 * 1024 * 1024;
|
|
40
|
+
const isRecord = (v) => v != null && typeof v === 'object' && !Array.isArray(v);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* THREE STATES (2026-10-17): `{}` when the file is absent (measured: the
|
|
44
|
+
* machine has none), `null` when it exists but could not be read, is too big
|
|
45
|
+
* or is not an object (unknown), so a caller never says a server is mounted
|
|
46
|
+
* that was never read.
|
|
47
|
+
*/
|
|
48
|
+
export function machineMcpServers({ user = false, checkout = null, env = process.env } = {}) {
|
|
49
|
+
const path = env.CLAUDE_CONFIG_DIR ? join(env.CLAUDE_CONFIG_DIR, '.claude.json') : join(env.HOME || homedir(), '.claude.json');
|
|
50
|
+
let raw;
|
|
51
|
+
try {
|
|
52
|
+
if (statSync(path).size > CLI_CONFIG_MAX_BYTES) return null;
|
|
53
|
+
raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return error?.code === 'ENOENT' ? {} : null;
|
|
56
|
+
}
|
|
57
|
+
if (!isRecord(raw)) return null;
|
|
58
|
+
const out = {};
|
|
59
|
+
const take = (servers) => {
|
|
60
|
+
if (!isRecord(servers)) return;
|
|
61
|
+
for (const [name, config] of Object.entries(servers)) if (name !== '__proto__' && isRecord(config)) out[name] = config;
|
|
62
|
+
};
|
|
63
|
+
if (user) take(raw.mcpServers);
|
|
64
|
+
if (typeof checkout === 'string' && checkout && isRecord(raw.projects)) {
|
|
65
|
+
const keys = [resolve(checkout)];
|
|
66
|
+
try { keys.push(realpathSync(checkout)); } catch { /* the resolved path alone */ }
|
|
67
|
+
const root = canonicalGitRoot(checkout);
|
|
68
|
+
if (root) keys.unshift(root);
|
|
69
|
+
const key = keys.find((k) => Object.hasOwn(raw.projects, k));
|
|
70
|
+
if (key) take(raw.projects[key]?.mcpServers);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The main checkout a directory's repository resolves to — the key the CLI
|
|
76
|
+
* files a local server under when the connected folder is itself a linked
|
|
77
|
+
* worktree. Null when git cannot say. */
|
|
78
|
+
function canonicalGitRoot(dir) {
|
|
79
|
+
const common = gitOrNull(['rev-parse', '--path-format=absolute', '--git-common-dir'], dir);
|
|
80
|
+
try {
|
|
81
|
+
return common && basename(common) === '.git' ? realpathSync(dirname(common)) : null;
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -46,9 +46,10 @@
|
|
|
46
46
|
* swept on the next write.
|
|
47
47
|
*/
|
|
48
48
|
import { randomBytes } from 'node:crypto';
|
|
49
|
-
import { chmodSync, lstatSync, mkdirSync, readFileSync, readdirSync,
|
|
49
|
+
import { chmodSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
50
50
|
import { homedir } from 'node:os';
|
|
51
|
-
import { basename, dirname, join
|
|
51
|
+
import { basename, dirname, join } from 'node:path';
|
|
52
|
+
import { replaceSettings } from './claudeSettingsArg.mjs';
|
|
52
53
|
import { MACHINE_CREDENTIAL_ENV } from './machineEnv.mjs';
|
|
53
54
|
import { WINDOW_ENV_REMOVED, withoutWindows } from './noWindowEnv.mjs';
|
|
54
55
|
|
|
@@ -141,56 +142,6 @@ export function personalClaudeSettings(env = process.env) {
|
|
|
141
142
|
return out;
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
/** Far past any real CLI config — `.claude.json` also holds every project's
|
|
145
|
-
* history; a bigger one is not read. */
|
|
146
|
-
const CLI_CONFIG_MAX_BYTES = 32 * 1024 * 1024;
|
|
147
|
-
const isRecord = (v) => v != null && typeof v === 'object' && !Array.isArray(v);
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* THE MACHINE'S OWN MCP SERVERS THAT A CARD TURN'S SETTINGS SOURCES NEVER
|
|
151
|
-
* REACH (2026-10-17). The 2026-10-11 ruling mounts "the user and local
|
|
152
|
-
* servers" on every card turn, and neither arrived where the person keeps
|
|
153
|
-
* them. MEASURED on Claude Code 2.1.302, a scratch CLAUDE_CONFIG_DIR whose
|
|
154
|
-
* `.claude.json` held one user and one local server:
|
|
155
|
-
* · `--setting-sources ''` (every fenced posture: write-up, mockup, 3D model,
|
|
156
|
-
* deck) loaded NEITHER — a research card on the owner's box saw the
|
|
157
|
-
* claude.ai connectors and no Roblox_Studio, a user-scope server;
|
|
158
|
-
* · `--setting-sources user` (a code card) loaded the user server only;
|
|
159
|
-
* · a local server is keyed on the git root it was added from (`claude mcp
|
|
160
|
-
* add`'s default scope), which is the person's checkout — never the
|
|
161
|
-
* agent's worktree, so no card turn could see one even with every source.
|
|
162
|
-
* So the lane names what is missing and the adapter hands it over through a
|
|
163
|
-
* private `--mcp-config` file (runtimeClaude.mjs): `user` when the turn's
|
|
164
|
-
* sources drop the user scope, and the checkout's local servers always. A
|
|
165
|
-
* local name shadows a user one, the CLI's own precedence; an `--mcp-config`
|
|
166
|
-
* server beat a user server of the same name (measured), so a code card keeps
|
|
167
|
-
* that order too. `{}` when the file is absent, unreadable or not an object.
|
|
168
|
-
*/
|
|
169
|
-
export function machineMcpServers({ user = false, checkout = null, env = process.env } = {}) {
|
|
170
|
-
const path = env.CLAUDE_CONFIG_DIR ? join(env.CLAUDE_CONFIG_DIR, '.claude.json') : join(env.HOME || homedir(), '.claude.json');
|
|
171
|
-
let raw;
|
|
172
|
-
try {
|
|
173
|
-
if (statSync(path).size > CLI_CONFIG_MAX_BYTES) return {};
|
|
174
|
-
raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
175
|
-
} catch {
|
|
176
|
-
return {};
|
|
177
|
-
}
|
|
178
|
-
if (!isRecord(raw)) return {};
|
|
179
|
-
const out = {};
|
|
180
|
-
const take = (servers) => {
|
|
181
|
-
if (!isRecord(servers)) return;
|
|
182
|
-
for (const [name, config] of Object.entries(servers)) if (name !== '__proto__' && isRecord(config)) out[name] = config;
|
|
183
|
-
};
|
|
184
|
-
if (user) take(raw.mcpServers);
|
|
185
|
-
if (typeof checkout === 'string' && checkout && isRecord(raw.projects)) {
|
|
186
|
-
const keys = [resolve(checkout)];
|
|
187
|
-
try { keys.push(realpathSync(checkout)); } catch { /* the resolved path alone */ }
|
|
188
|
-
const key = keys.find((k) => Object.hasOwn(raw.projects, k));
|
|
189
|
-
if (key) take(raw.projects[key]?.mcpServers);
|
|
190
|
-
}
|
|
191
|
-
return out;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
145
|
/** Where a fenced turn's settings file lives — the daemon's own state
|
|
195
146
|
* directory, private to this user. */
|
|
196
147
|
export const turnSettingsDir = () => join(homedir(), '.flowviant', 'run');
|
|
@@ -233,7 +184,8 @@ export function writeTurnSettings(settings, { dir = turnSettingsDir() } = {}) {
|
|
|
233
184
|
}
|
|
234
185
|
|
|
235
186
|
/** Remove the settings file a turn's argv names, when it is one of ours — and
|
|
236
|
-
* the machine-MCP file (`machineMcpServers`), written
|
|
187
|
+
* the machine-MCP file (claudeMachineMcp.mjs `machineMcpServers`), written
|
|
188
|
+
* the same way. */
|
|
237
189
|
export function releaseTurnSettings(args, { dir = turnSettingsDir() } = {}) {
|
|
238
190
|
if (!Array.isArray(args)) return;
|
|
239
191
|
args.forEach((a, i) => {
|
|
@@ -254,8 +206,5 @@ export function releaseTurnSettings(args, { dir = turnSettingsDir() } = {}) {
|
|
|
254
206
|
export function withPersonalSettings(perm, kept, { write = writeTurnSettings } = {}) {
|
|
255
207
|
const personal = kept && typeof kept === 'object' && !Array.isArray(kept) ? kept : {};
|
|
256
208
|
if (!Object.keys(personal).length) return [...perm];
|
|
257
|
-
|
|
258
|
-
if (at < 0) return ['--settings', write(personal), ...perm];
|
|
259
|
-
const posture = JSON.parse(perm[at + 1]);
|
|
260
|
-
return [...perm.slice(0, at), '--settings', write({ ...personal, ...posture }), ...perm.slice(at + 2)];
|
|
209
|
+
return replaceSettings(perm, (posture) => write({ ...personal, ...posture }));
|
|
261
210
|
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
import { SAFE } from './config.mjs';
|
|
15
15
|
import { runningCompiledBinary } from './launchCommand.mjs';
|
|
16
|
+
import { withSettings } from './claudeSettingsArg.mjs';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* THE READ GUARD, ON EVERY CURATED POSTURE (2026-09-23) — see
|
|
@@ -479,8 +480,10 @@ export const RESEARCH_PERM = researchPerm(null);
|
|
|
479
480
|
|
|
480
481
|
/**
|
|
481
482
|
* CLAUDE'S PERMISSION LIST FOR A TURN PROFILE, by the profile's name — one
|
|
482
|
-
* entry per
|
|
483
|
-
*
|
|
483
|
+
* entry per profile Claude declares, plus plan-mode (runtimeClaude.mjs
|
|
484
|
+
* `profiles`; a test holds the two equal). runTurn asks for it through the
|
|
485
|
+
* row (`CLAUDE_RUNTIME.perm`), on Claude alone. The builders that take the
|
|
486
|
+
* knowledge directory get it; the fixed lists ignore it.
|
|
484
487
|
* `projectMcp` is the names of the project servers the lane mounted; only the
|
|
485
488
|
* design and research lists take them (`projectMcp` in turnProfile.mjs).
|
|
486
489
|
*/
|
|
@@ -492,13 +495,6 @@ const CLAUDE_PERM = {
|
|
|
492
495
|
'plan-mode': () => PLAN_MODE_PERM,
|
|
493
496
|
design: (kd, _vault, mcp) => designPermFor(kd, mcp),
|
|
494
497
|
research: (kd, _vault, mcp) => researchPerm(kd, mcp),
|
|
495
|
-
// THE IMAGE POSTURE IS CODEX'S (0.114.0) and Claude does not declare it
|
|
496
|
-
// (runtimeClaude.mjs `profiles`), so turnProfile.mjs refuses a Claude turn
|
|
497
|
-
// under it before argv is built. The entry exists because `runTurn` looks
|
|
498
|
-
// Claude's list up BY NAME for every runtime (the adapter that is not Claude
|
|
499
|
-
// ignores it), and a missing name throws. Were it ever reached on Claude it
|
|
500
|
-
// is the consult list: read-only, no write at all — failing closed.
|
|
501
|
-
image: (kd) => consultPermFor(kd),
|
|
502
498
|
};
|
|
503
499
|
export const CLAUDE_PERM_PROFILES = Object.freeze(Object.keys(CLAUDE_PERM));
|
|
504
500
|
export function claudePermFor(profileName, knowledgeDir, vaultDir, projectMcp = []) {
|
|
@@ -541,10 +537,5 @@ export function claudePermFor(profileName, knowledgeDir, vaultDir, projectMcp =
|
|
|
541
537
|
* of a fenced posture.
|
|
542
538
|
*/
|
|
543
539
|
export function withMachineMcp(perm, disabledProjectMcp) {
|
|
544
|
-
|
|
545
|
-
const at = perm.indexOf('--settings');
|
|
546
|
-
if (at < 0) return ['--settings', JSON.stringify({ ...extra, hooks: { PreToolUse: [MCP_ALLOW_HOOK] } }), ...perm];
|
|
547
|
-
const posture = JSON.parse(perm[at + 1]);
|
|
548
|
-
const hooks = { ...(posture.hooks ?? {}), PreToolUse: [...(posture.hooks?.PreToolUse ?? []), MCP_ALLOW_HOOK] };
|
|
549
|
-
return [...perm.slice(0, at), '--settings', JSON.stringify({ ...posture, ...extra, hooks }), ...perm.slice(at + 2)];
|
|
540
|
+
return withSettings(perm, { disabledMcpjsonServers: [...disabledProjectMcp], hooks: { PreToolUse: [MCP_ALLOW_HOOK] } });
|
|
550
541
|
}
|