flowviant 0.97.0 → 0.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -13
- package/bin/cli.mjs +108 -28
- package/bin/lib/agentPlan.mjs +159 -37
- package/bin/lib/artifacts.mjs +136 -26
- package/bin/lib/authReject.mjs +32 -0
- package/bin/lib/authproxy.mjs +52 -16
- package/bin/lib/childEnv.mjs +33 -1
- package/bin/lib/claude.mjs +166 -71
- package/bin/lib/config.mjs +36 -9
- package/bin/lib/credentials.mjs +29 -5
- package/bin/lib/deploy.mjs +364 -30
- package/bin/lib/desktopContract.mjs +227 -0
- package/bin/lib/env.mjs +130 -13
- package/bin/lib/fleet.mjs +121 -30
- package/bin/lib/git.mjs +131 -3
- package/bin/lib/hooks/readGuard.mjs +24 -0
- package/bin/lib/instance.mjs +129 -14
- package/bin/lib/knowledge.mjs +180 -60
- package/bin/lib/landed.mjs +38 -30
- package/bin/lib/launchCommand.mjs +23 -0
- package/bin/lib/listeners.mjs +211 -0
- package/bin/lib/localSessions.mjs +29 -4
- package/bin/lib/login.mjs +36 -12
- package/bin/lib/machines.mjs +24 -14
- package/bin/lib/mcp-cli.mjs +4 -3
- package/bin/lib/preflight.mjs +2 -1
- package/bin/lib/preview.mjs +95 -6
- package/bin/lib/printable.mjs +42 -0
- package/bin/lib/procRegistry.mjs +65 -0
- package/bin/lib/processes.mjs +20 -7
- package/bin/lib/projectTools.mjs +248 -0
- package/bin/lib/prompts.mjs +16 -11
- package/bin/lib/resources.mjs +46 -4
- package/bin/lib/runtimeOfferings.mjs +75 -0
- package/bin/lib/runtimes.mjs +71 -9
- package/bin/lib/shipMerge.mjs +8 -4
- package/bin/lib/trace.mjs +16 -6
- package/bin/lib/update.mjs +114 -25
- package/bin/lib/work.mjs +1192 -4584
- package/bin/lib/workAgentMerges.mjs +504 -0
- package/bin/lib/workAgentPlans.mjs +447 -0
- package/bin/lib/workAgentReview.mjs +702 -0
- package/bin/lib/workAgentTurns.mjs +1151 -0
- package/bin/lib/workDiffs.mjs +107 -0
- package/bin/lib/workPreviews.mjs +351 -0
- package/bin/lib/workProcesses.mjs +236 -0
- package/bin/lib/workPullRequests.mjs +313 -0
- package/bin/lib/workShip.mjs +385 -0
- package/bin/lib/worktreeDiff.mjs +99 -43
- package/package.json +2 -3
package/bin/lib/prompts.mjs
CHANGED
|
@@ -492,9 +492,10 @@ const kickoff = ({ message, askedByName, head, tail }) => {
|
|
|
492
492
|
* A CAPTURE TAB (server: work_session.kind='capture', flagged on the job as
|
|
493
493
|
* `capture: true`) — the board's "New task" conversation. Its whole job is
|
|
494
494
|
* turning what the person says into STAGED cards a human lands; it edits
|
|
495
|
-
* nothing, and the permission profile enforces that (
|
|
496
|
-
* MCP — the same fence the scratch planner runs behind). This
|
|
497
|
-
* QUALITY half; the token scope and the permission list are
|
|
495
|
+
* nothing, and the permission profile enforces that (`planPermFor`:
|
|
496
|
+
* read-only + MCP — the same fence the scratch planner runs behind). This
|
|
497
|
+
* prompt is the QUALITY half; the token scope and the permission list are
|
|
498
|
+
* the safety.
|
|
498
499
|
*
|
|
499
500
|
* THE CHAT ASKS IN CHIPS, AND SAYS WHAT IT ASSUMED (2026-09-23, 0.97.0). Rule
|
|
500
501
|
* 5 used to be one sentence — "clarify before staging" — and the owner asked
|
|
@@ -600,7 +601,9 @@ MECHANICS OF THIS CHAT:
|
|
|
600
601
|
something they said.
|
|
601
602
|
10. REFERENCE WHAT THE PROJECT KEPT. When the ask names a kept design or
|
|
602
603
|
write-up ("implement design A"), call list_library and pass its id as
|
|
603
|
-
\`references\` on the card you stage, so the agent reads it
|
|
604
|
+
\`references\` on the card you stage, so the agent reads it. When the person's
|
|
605
|
+
message includes attached library items, pass those ids as \`references\` on
|
|
606
|
+
the cards you stage from that message.`;
|
|
604
607
|
|
|
605
608
|
export const WORK_TURN_KICKOFF = ({ sessionId, sessionName, message, askedByName }) =>
|
|
606
609
|
kickoff({
|
|
@@ -684,11 +687,11 @@ export const REGROUND_KICKOFF = ({ sha, title, files, vaultDir, predictedPages =
|
|
|
684
687
|
* no code, edits no files and starts nothing: a person reads what it proposes,
|
|
685
688
|
* edits it on the board, and accepting is what spawns anything.
|
|
686
689
|
*
|
|
687
|
-
* It runs READ-ONLY IN THE CHECKOUT under
|
|
688
|
-
* mkdir, no rm, and no MCP at all. The proposal comes back as its
|
|
689
|
-
* message, not through a tool, which is what lets that permission set
|
|
690
|
-
* narrow. A planner authors a decision, and there is no file on this
|
|
691
|
-
* has any business touching.
|
|
690
|
+
* It runs READ-ONLY IN THE CHECKOUT under `consultPermFor` — no Write, no
|
|
691
|
+
* Edit, no mkdir, no rm, and no MCP at all. The proposal comes back as its
|
|
692
|
+
* final message, not through a tool, which is what lets that permission set
|
|
693
|
+
* be this narrow. A planner authors a decision, and there is no file on this
|
|
694
|
+
* machine it has any business touching.
|
|
692
695
|
*
|
|
693
696
|
* The two facts it is asked to weigh are the only two that are actually
|
|
694
697
|
* knowable here: what the cards SAY, and what each live agent has already
|
|
@@ -861,13 +864,15 @@ export const AGENT_PLAN_KICKOFF = ({ tasks, liveAgents, agentCap }) => {
|
|
|
861
864
|
* a person one reply; a turn that guesses costs them a review, a rejection and
|
|
862
865
|
* a second run — and the guess arrives wearing a confident summary.
|
|
863
866
|
*/
|
|
864
|
-
export const SYSTEM_AGENT = `You are the human's own
|
|
867
|
+
export const SYSTEM_AGENT = `You are the human's own coding agent, working one task in a git worktree of their
|
|
865
868
|
repository. Nobody is watching this run. You have the repo and nothing else —
|
|
866
869
|
no project tools, no board, no chat.
|
|
867
870
|
|
|
868
871
|
WHAT TO DO:
|
|
869
872
|
|
|
870
|
-
1. Do the card you are given. Read whatever you need first
|
|
873
|
+
1. Do the card you are given. Read whatever you need first — including
|
|
874
|
+
CLAUDE.md and AGENTS.md at the repository root, when either exists: they are
|
|
875
|
+
the repository's own instructions, whichever CLI you are. Follow the
|
|
871
876
|
repository's own conventions over anything you would do by default.
|
|
872
877
|
|
|
873
878
|
2. COMMIT YOUR WORK before you finish. Small, real commit messages. Uncommitted
|
package/bin/lib/resources.mjs
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
|
|
47
47
|
import { execFileSync } from 'node:child_process';
|
|
48
48
|
import { readFileSync, readdirSync, statfsSync } from 'node:fs';
|
|
49
|
-
import { freemem, loadavg, platform } from 'node:os';
|
|
49
|
+
import { cpus, freemem, loadavg, platform } from 'node:os';
|
|
50
50
|
import { MACHINE } from './config.mjs';
|
|
51
51
|
|
|
52
52
|
const MiB = 1024 * 1024;
|
|
@@ -71,12 +71,38 @@ const readFile = (p) => {
|
|
|
71
71
|
function memoryUsed() {
|
|
72
72
|
const cur = readFile('/sys/fs/cgroup/memory.current');
|
|
73
73
|
if (cur !== null) {
|
|
74
|
-
const n = Number(cur);
|
|
74
|
+
const n = cgroupWorkingSet(Number(cur), readFile);
|
|
75
75
|
if (Number.isFinite(n) && n > 0) return n;
|
|
76
76
|
}
|
|
77
77
|
return Math.max(0, MACHINE.memBytes - freemem());
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* THE CGROUP'S WORKING SET, NOT ITS CHARGE.
|
|
82
|
+
*
|
|
83
|
+
* `memory.current` counts the page cache charged to the cgroup — every file a
|
|
84
|
+
* worktree add, an npm install or a repo read touched. On a container that has
|
|
85
|
+
* been up a while it sits just under `memory.max` while almost all of it is
|
|
86
|
+
* reclaimable, and the kernel reclaims only when something ALLOCATES. A guard
|
|
87
|
+
* reading the raw charge therefore defers every unattended turn, which spawns
|
|
88
|
+
* nothing, which allocates nothing, which never reclaims: a permanent refusal
|
|
89
|
+
* relaying a "low memory" figure that is false. This is the same trap
|
|
90
|
+
* `memAvailableBytes` rejects `freemem()` for, one layer down.
|
|
91
|
+
*
|
|
92
|
+
* So subtract `inactive_file` from `memory.stat`, which is what kubelet and
|
|
93
|
+
* `docker stats` call the working set. An unreadable or malformed stat leaves
|
|
94
|
+
* the charge as it was — the conservative direction, and never a guess.
|
|
95
|
+
*/
|
|
96
|
+
function cgroupWorkingSet(current, read) {
|
|
97
|
+
if (!Number.isFinite(current)) return current;
|
|
98
|
+
const stat = read('/sys/fs/cgroup/memory.stat');
|
|
99
|
+
const m = stat && stat.match(/^inactive_file\s+(\d+)$/m);
|
|
100
|
+
if (!m) return current;
|
|
101
|
+
const inactive = Number(m[1]);
|
|
102
|
+
if (!Number.isFinite(inactive) || inactive < 0) return current;
|
|
103
|
+
return Math.max(0, current - inactive);
|
|
104
|
+
}
|
|
105
|
+
|
|
80
106
|
/**
|
|
81
107
|
* Resident memory of a process AND everything it spawned.
|
|
82
108
|
*
|
|
@@ -176,7 +202,8 @@ export function memAvailableBytes(read = readFile) {
|
|
|
176
202
|
const max = read('/sys/fs/cgroup/memory.max');
|
|
177
203
|
if (max && max !== 'max') {
|
|
178
204
|
const limit = Number(max);
|
|
179
|
-
const
|
|
205
|
+
const raw = read('/sys/fs/cgroup/memory.current');
|
|
206
|
+
const cur = raw === null ? NaN : cgroupWorkingSet(Number(raw), read);
|
|
180
207
|
if (Number.isFinite(limit) && limit > 0 && Number.isFinite(cur)) {
|
|
181
208
|
const room = Math.max(0, limit - cur);
|
|
182
209
|
avail = avail === null ? room : Math.min(avail, room);
|
|
@@ -248,6 +275,21 @@ const sizeWord = (n) =>
|
|
|
248
275
|
const PRESSURE_CACHE_MS = 2_000;
|
|
249
276
|
let lastMeasure = { at: 0, m: null };
|
|
250
277
|
|
|
278
|
+
/**
|
|
279
|
+
* THE CORES THE LOAD AVERAGE IS A LOAD ON.
|
|
280
|
+
*
|
|
281
|
+
* `/proc/loadavg` is not namespaced: inside a container it reports the whole
|
|
282
|
+
* HOST. `MACHINE.cores` is clamped to the cgroup's `cpu.max`, which is right
|
|
283
|
+
* for sizing how many turns this box runs and wrong as the divisor of a host
|
|
284
|
+
* figure — a `--cpus=2` container on a busy 32-core host read "load 10 on 2
|
|
285
|
+
* cores" and deferred every unattended turn over load it did not cause. Compare
|
|
286
|
+
* like with like: a host figure against the host's core count. Exported so the
|
|
287
|
+
* pairing can be pinned.
|
|
288
|
+
*/
|
|
289
|
+
export function loadCores(hostCores = cpus().length) {
|
|
290
|
+
return Number.isFinite(hostCores) && hostCores > 0 ? hostCores : MACHINE.cores;
|
|
291
|
+
}
|
|
292
|
+
|
|
251
293
|
/** What the box says about itself, cached briefly. Exported so a caller can
|
|
252
294
|
* hold one reading across several verdicts — and so tests can pass their own
|
|
253
295
|
* instead of depending on the machine they run on. */
|
|
@@ -260,7 +302,7 @@ export function measurePressure() {
|
|
|
260
302
|
// Unix only; Windows reports zeroes, which are sent as null rather than as
|
|
261
303
|
// a very calm-looking 0.00 — the same rule the snapshot keeps.
|
|
262
304
|
load1: loadavg()[0] || null,
|
|
263
|
-
cores:
|
|
305
|
+
cores: loadCores(),
|
|
264
306
|
};
|
|
265
307
|
lastMeasure = { at: now, m };
|
|
266
308
|
return m;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** Local, bounded Codex readouts. No agent turn is started for these reports. */
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { readBaseTools } from './projectTools.mjs';
|
|
7
|
+
|
|
8
|
+
const TTL = 5 * 60_000;
|
|
9
|
+
let cachedAt = 0;
|
|
10
|
+
let cached = null;
|
|
11
|
+
let repoCached = null;
|
|
12
|
+
const home = (env) => env.CODEX_HOME || join(homedir(), '.codex');
|
|
13
|
+
const nameOk = (name) => typeof name === 'string' && /^[A-Za-z0-9_.:-]{1,80}$/.test(name) && name !== 'flowviant';
|
|
14
|
+
const run = (args, env) => execFileSync('codex', args, {
|
|
15
|
+
encoding: 'utf8', timeout: 5000, maxBuffer: 128 * 1024,
|
|
16
|
+
stdio: ['ignore', 'pipe', 'ignore'], env,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export function localCodexOfferings({ env = process.env, runCodex = run, read = readFileSync, list = readdirSync } = {}) {
|
|
20
|
+
const result = {};
|
|
21
|
+
try {
|
|
22
|
+
const cachePath = join(home(env), 'models_cache.json');
|
|
23
|
+
if (read === readFileSync && statSync(cachePath).size > 2 * 1024 * 1024) throw Error('model cache too large');
|
|
24
|
+
const data = JSON.parse(read(cachePath, 'utf8'));
|
|
25
|
+
if (Array.isArray(data.models)) result.models = data.models
|
|
26
|
+
.filter((m) => m?.visibility === 'list' && nameOk(m.slug))
|
|
27
|
+
.slice(0, 60)
|
|
28
|
+
.map((m) => ({ slug: m.slug, displayName: String(m.display_name || m.slug).slice(0, 100),
|
|
29
|
+
efforts: Array.isArray(m.supported_reasoning_levels) ? m.supported_reasoning_levels
|
|
30
|
+
.map((e) => e?.effort).filter((e) => typeof e === 'string' && /^[a-z]{1,20}$/.test(e)).slice(0, 8) : [] }));
|
|
31
|
+
} catch { /* absent or unreadable is unknown */ }
|
|
32
|
+
try {
|
|
33
|
+
const lines = runCodex(['features', 'list'], env).split('\n');
|
|
34
|
+
result.capabilities = lines.some((line) => /^image_generation\s+\S+\s+true\s*$/.test(line)) ? ['image'] : [];
|
|
35
|
+
} catch { /* a failed local probe is unknown */ }
|
|
36
|
+
try {
|
|
37
|
+
result.skills = list(join(home(env), 'skills'), { withFileTypes: true })
|
|
38
|
+
.filter((entry) => entry.isDirectory() && nameOk(entry.name))
|
|
39
|
+
.map((entry) => entry.name).slice(0, 80);
|
|
40
|
+
} catch (error) { if (error?.code === 'ENOENT') result.skills = []; }
|
|
41
|
+
try {
|
|
42
|
+
const servers = JSON.parse(runCodex(['mcp', 'list', '--json'], env));
|
|
43
|
+
if (Array.isArray(servers)) result.mcp = servers
|
|
44
|
+
// Unknown/unsupported auth is not proof of disconnection. Keep their
|
|
45
|
+
// names local, just as connected servers stay local.
|
|
46
|
+
.filter((s) => nameOk(s?.name) && ['needs_auth', 'failed'].includes(s.auth_status))
|
|
47
|
+
.slice(0, 80)
|
|
48
|
+
.map((s) => ({ n: s.name, s: s.auth_status === 'needs_auth' ? 'needs-auth' : 'failed' }));
|
|
49
|
+
} catch { /* no read is unknown */ }
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function runtimeOfferings(repoRoot, baseRef, { refresh = false } = {}) {
|
|
54
|
+
if (!cached || refresh || Date.now() - cachedAt >= TTL) {
|
|
55
|
+
cached = localCodexOfferings();
|
|
56
|
+
cachedAt = Date.now();
|
|
57
|
+
}
|
|
58
|
+
const out = { rtm: {}, rtc: {}, rts: {}, rtp: {} };
|
|
59
|
+
if (cached.models) out.rtm.codex = cached.models;
|
|
60
|
+
if (cached.capabilities) out.rtc.codex = cached.capabilities;
|
|
61
|
+
if (cached.skills) out.rts.codex = cached.skills;
|
|
62
|
+
if (cached.mcp) out.rtp.codex = cached.mcp;
|
|
63
|
+
// Repo skills and MCP are read from the base commit, like the turn itself.
|
|
64
|
+
try {
|
|
65
|
+
if (!repoCached || repoCached.root !== repoRoot || repoCached.ref !== baseRef || refresh || Date.now() - repoCached.at >= TTL) {
|
|
66
|
+
repoCached = { root: repoRoot, ref: baseRef, at: Date.now(), snapshot: readBaseTools(repoRoot, baseRef) };
|
|
67
|
+
}
|
|
68
|
+
const snapshot = repoCached.snapshot;
|
|
69
|
+
if (out.rts.codex) out.rts.codex = [...new Set([...out.rts.codex, ...snapshot.skills.map((s) => s.path.split('/')[2])])].slice(0, 80);
|
|
70
|
+
if (out.rtp.codex) out.rtp.codex = [...new Map([...out.rtp.codex, ...snapshot.tools
|
|
71
|
+
.filter((t) => t.state !== 'ready' || t.config.type === 'sse')
|
|
72
|
+
.map((t) => ({ n: t.name, s: 'failed' }))].map((server) => [server.n, server])).values()].slice(0, 80);
|
|
73
|
+
} catch { /* keep local measurements */ }
|
|
74
|
+
return out;
|
|
75
|
+
}
|
package/bin/lib/runtimes.mjs
CHANGED
|
@@ -342,11 +342,40 @@ function parseCodexLine(line, cwd) {
|
|
|
342
342
|
const activity = humanizeCodexItem(item, cwd);
|
|
343
343
|
// Only the agent's MESSAGES are sentinel-bearing text. Reasoning is not:
|
|
344
344
|
// a model that muses "I could output NOTHING here" must not end the turn.
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
345
|
+
const isMessage = (item.item_type ?? item.type) === 'agent_message';
|
|
346
|
+
const text = isMessage ? `${item.text ?? item.message ?? ''}\n` : '';
|
|
347
|
+
// `answer` is the SAME message, surfaced alone: `text` accumulates every
|
|
348
|
+
// message plus every error and stderr line, so a caller that needs "what
|
|
349
|
+
// the agent said LAST" (the agent lane's final JSON object) cannot recover
|
|
350
|
+
// it from `out`. Claude's stream has this for free under
|
|
351
|
+
// `answerFromResult`; codex needed it said.
|
|
352
|
+
return { activity, text, ...(isMessage ? { answer: String(item.text ?? item.message ?? '') } : {}) };
|
|
353
|
+
}
|
|
354
|
+
// THE TURN'S OWN TOKEN COUNT (2026-09-24). `codex exec --json` closes a
|
|
355
|
+
// turn with `turn.completed` carrying `usage` — the codex twin of Claude's
|
|
356
|
+
// `result.usage`, and until now dropped on the floor, so a codex agent
|
|
357
|
+
// reported no spend at all. Mapped onto the daemon's four counters:
|
|
358
|
+
// OpenAI's `input_tokens` INCLUDES the cached part, Claude's excludes it,
|
|
359
|
+
// so the cached share is subtracted out rather than counted twice. Codex
|
|
360
|
+
// has no cache-creation figure; zero is what it reported, not a guess.
|
|
361
|
+
// Output already includes reasoning tokens on OpenAI's side.
|
|
362
|
+
case 'turn.completed': {
|
|
363
|
+
const u = ev.usage;
|
|
364
|
+
if (!u || typeof u !== 'object') return null;
|
|
365
|
+
const n = (v) => (Number.isFinite(Number(v)) && Number(v) > 0 ? Math.floor(Number(v)) : 0);
|
|
366
|
+
const cached = n(u.cached_input_tokens);
|
|
367
|
+
return {
|
|
368
|
+
activity: null,
|
|
369
|
+
text: '',
|
|
370
|
+
// `usageFromResult`'s shape (claude.mjs), so every consumer of
|
|
371
|
+
// `onUsage` reads one vocabulary whichever CLI counted it.
|
|
372
|
+
usage: {
|
|
373
|
+
input: Math.max(0, n(u.input_tokens) - cached),
|
|
374
|
+
output: n(u.output_tokens),
|
|
375
|
+
cacheCreate: 0,
|
|
376
|
+
cacheRead: cached,
|
|
377
|
+
},
|
|
378
|
+
};
|
|
350
379
|
}
|
|
351
380
|
case 'turn.failed': {
|
|
352
381
|
// An error is prose too, and the sentence that explains a failed turn is
|
|
@@ -367,7 +396,12 @@ function parseCodexLine(line, cwd) {
|
|
|
367
396
|
label: oneLine(ev.error?.message ?? 'turn failed'),
|
|
368
397
|
...(msg.trim() ? { full: msg } : {}),
|
|
369
398
|
},
|
|
370
|
-
text
|
|
399
|
+
// THE FAILURE'S OWN WORDS REACH `text` (2026-09-24), the rule the bare
|
|
400
|
+
// `error` arm below already keeps. This returned '' — so a codex turn
|
|
401
|
+
// that failed on a usage limit handed the limit matcher nothing, the
|
|
402
|
+
// agent landed in Stuck as "produced no output", and every other codex
|
|
403
|
+
// agent kept spending into the same wall.
|
|
404
|
+
text: msg.trim() ? `${msg}\n` : '',
|
|
371
405
|
};
|
|
372
406
|
}
|
|
373
407
|
// A bare `error` event — the shape an auth failure arrives in ("401
|
|
@@ -555,7 +589,7 @@ export const RUNTIMES = {
|
|
|
555
589
|
* strongest form of it available anywhere: `--append-system-prompt` sits
|
|
556
590
|
* above the conversation rather than inside it.
|
|
557
591
|
*/
|
|
558
|
-
args({ prompt, system, model, effort, resume, streamJson, perm, mcp = [], resultSchemaArgs = [], adoptResumeId, resumeThreadId, knowledgeDir }) {
|
|
592
|
+
args({ prompt, system, model, effort, resume, streamJson, perm, profile = 'build', mcp = [], resultSchemaArgs = [], adoptResumeId, resumeThreadId, knowledgeDir, agentTools }) {
|
|
559
593
|
const a = [];
|
|
560
594
|
// THREE ANSWERS TO ONE QUESTION — "what conversation is this?" — and they
|
|
561
595
|
// are mutually exclusive, never combined.
|
|
@@ -582,6 +616,17 @@ export const RUNTIMES = {
|
|
|
582
616
|
else if (resumeThreadId) a.push('--resume', resumeThreadId);
|
|
583
617
|
else if (resume) a.push('--continue');
|
|
584
618
|
a.push('-p', prompt, '--append-system-prompt', system);
|
|
619
|
+
if (agentTools) {
|
|
620
|
+
// Keep personal settings, but never load the agent-editable project's
|
|
621
|
+
// settings, hooks, skills or CLAUDE.md discovery. A resumed agent must
|
|
622
|
+
// render the CURRENT base snapshot, not the first turn's prompt cache.
|
|
623
|
+
a.push('--setting-sources', 'user', '--system-prompt-snapshot', 'off');
|
|
624
|
+
if (agentTools.pluginDir) a.push('--plugin-dir', agentTools.pluginDir);
|
|
625
|
+
}
|
|
626
|
+
// These fenced postures have no project MCP, even if Claude discovers a
|
|
627
|
+
// .mcp.json in the agent's worktree. Build gets its reviewed base config
|
|
628
|
+
// explicitly from the caller; the plan profile keeps its own principal.
|
|
629
|
+
if (['consult', 'wiki', 'design', 'research'].includes(profile)) a.push('--strict-mcp-config');
|
|
585
630
|
a.push(...mcp, ...resultSchemaArgs);
|
|
586
631
|
a.push('--model', model || MODEL);
|
|
587
632
|
if (effort) a.push('--effort', effort);
|
|
@@ -634,6 +679,11 @@ export const RUNTIMES = {
|
|
|
634
679
|
* user's repository, which is not a risk worth taking for a slightly
|
|
635
680
|
* stickier instruction.
|
|
636
681
|
*
|
|
682
|
+
* Agent turns additionally pass reviewed base instructions as a developer
|
|
683
|
+
* config override when the personal config has no developer instruction;
|
|
684
|
+
* the prompt always carries them too. Workbench tabs keep their normal
|
|
685
|
+
* checkout discovery.
|
|
686
|
+
*
|
|
637
687
|
* `--skip-git-repo-check` is deliberately NOT passed: a task always builds
|
|
638
688
|
* in a git worktree, and if it somehow is not one, failing loudly beats
|
|
639
689
|
* silently editing files nobody can diff.
|
|
@@ -644,7 +694,7 @@ export const RUNTIMES = {
|
|
|
644
694
|
* placed before it. Appending them after the positional is the kind of argv
|
|
645
695
|
* that parses today and stops parsing on some future clap upgrade.
|
|
646
696
|
*/
|
|
647
|
-
args({ prompt, system, model, effort, resume, resumeThreadId, profile = 'build', vaultDir, mcp = [], resultSchemaArgs = [], adoptResumeId }) {
|
|
697
|
+
args({ prompt, system, model, effort, resume, resumeThreadId, profile = 'build', vaultDir, mcp = [], resultSchemaArgs = [], adoptResumeId, agentTools }) {
|
|
648
698
|
// Adoption resumes a conversation in ITS OWN CLI's store (claude forks,
|
|
649
699
|
// agy moves) — codex has no adoptable store wired yet. Reaching here
|
|
650
700
|
// with an adopt id is a wiring mistake upstream, and it fails loudly on
|
|
@@ -661,6 +711,13 @@ export const RUNTIMES = {
|
|
|
661
711
|
if (resumeThreadId) a.push('resume', resumeThreadId);
|
|
662
712
|
else if (resume) a.push('resume', '--last');
|
|
663
713
|
a.push('--json');
|
|
714
|
+
if (agentTools) {
|
|
715
|
+
a.push('-c', 'project_doc_max_bytes=0');
|
|
716
|
+
// A person's existing developer instructions keep their authority.
|
|
717
|
+
// The base snapshot is also present in the per-turn prompt preamble.
|
|
718
|
+
if (!agentTools.personalDeveloperInstructions)
|
|
719
|
+
a.push('-c', `developer_instructions=${JSON.stringify(agentTools.instructions)}`);
|
|
720
|
+
}
|
|
664
721
|
if (model) a.push('--model', model);
|
|
665
722
|
// Effort is a config value on Codex rather than a flag.
|
|
666
723
|
if (effort) a.push('-c', `model_reasoning_effort="${effort}"`);
|
|
@@ -1462,9 +1519,14 @@ export function probeSkillsOnce(cwd) {
|
|
|
1462
1519
|
timer.unref?.();
|
|
1463
1520
|
child.on('error', () => finish(null));
|
|
1464
1521
|
child.on('exit', () => finish(null));
|
|
1522
|
+
// UTF-8 mode on the STREAM, not a per-chunk `.toString()` — a multi-byte
|
|
1523
|
+
// character straddling a chunk boundary decodes to mojibake (or a stray
|
|
1524
|
+
// replacement byte) when each chunk is decoded on its own; the stream's
|
|
1525
|
+
// own decoder holds the partial sequence over to the next chunk instead.
|
|
1526
|
+
child.stdout.setEncoding('utf8');
|
|
1465
1527
|
child.stdout.on('data', (d) => {
|
|
1466
1528
|
if (settled) return;
|
|
1467
|
-
buf += d
|
|
1529
|
+
buf += d;
|
|
1468
1530
|
let nl;
|
|
1469
1531
|
while (!settled && (nl = buf.indexOf('\n')) >= 0) {
|
|
1470
1532
|
const line = buf.slice(0, nl);
|
package/bin/lib/shipMerge.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { baseBranchName } from './git.mjs';
|
|
1
|
+
import { baseBranchName, gitNet as gitNetDefault } from './git.mjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* CARRY A SHIPPED TIP OUT ONTO BASE AND PUSH IT.
|
|
@@ -28,6 +28,10 @@ export function mergeOutward({
|
|
|
28
28
|
baseRef,
|
|
29
29
|
workingTree,
|
|
30
30
|
warn,
|
|
31
|
+
// NETWORK calls only (push, fetch) — timed and non-interactive, so a
|
|
32
|
+
// stale credential or a half-open connection cannot freeze the ship
|
|
33
|
+
// path's writer lock. Injectable for tests; defaults to the real one.
|
|
34
|
+
gitNet = gitNetDefault,
|
|
31
35
|
}) {
|
|
32
36
|
const dropTmp = () => {
|
|
33
37
|
try {
|
|
@@ -43,7 +47,7 @@ export function mergeOutward({
|
|
|
43
47
|
['merge', '--no-ff', tip, '-m', `ship(${label}): ${count} commit${count === 1 ? '' : 's'}`],
|
|
44
48
|
tmpDir
|
|
45
49
|
);
|
|
46
|
-
|
|
50
|
+
gitNet(['push', 'origin', `HEAD:${baseBranchName(baseRef())}`], tmpDir, 120_000);
|
|
47
51
|
};
|
|
48
52
|
try {
|
|
49
53
|
try {
|
|
@@ -74,7 +78,7 @@ export function mergeOutward({
|
|
|
74
78
|
if (!isRaceRejection(e)) throw e;
|
|
75
79
|
warn?.('ship: base moved under us — refetching and merging again');
|
|
76
80
|
try {
|
|
77
|
-
|
|
81
|
+
gitNet(['fetch', 'origin', '--quiet'], repoRoot, 60_000);
|
|
78
82
|
} catch {
|
|
79
83
|
/* offline — the retry fails honestly on the same push */
|
|
80
84
|
}
|
|
@@ -102,7 +106,7 @@ export function mergeOutward({
|
|
|
102
106
|
*/
|
|
103
107
|
if (workingTree && branch && branch === baseBranchName(baseRef())) {
|
|
104
108
|
try {
|
|
105
|
-
|
|
109
|
+
gitNet(['fetch', 'origin', '--quiet'], repoRoot, 60_000);
|
|
106
110
|
git(['merge', '--ff-only', baseRef()], workingTree);
|
|
107
111
|
} catch {
|
|
108
112
|
/* a readout, not the ship — the merge already landed, and the next
|
package/bin/lib/trace.mjs
CHANGED
|
@@ -38,10 +38,15 @@
|
|
|
38
38
|
* second attempt then opened at `seq: 0` against a mark of three hundred, so
|
|
39
39
|
* every batch it sent was trimmed to nothing and the surface showed the
|
|
40
40
|
* ABANDONED attempt's steps with the new one's tail welded on, no seam, no
|
|
41
|
-
* `dropped` to say so. `run` is a nonce per relay:
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
41
|
+
* `dropped` to say so. `run` is a nonce per relay: EACH RUN KEEPS ITS OWN
|
|
42
|
+
* HIGH-WATER MARK IN ITS OWN SEQ SPACE (2026-09-24) — an unfamiliar one starts
|
|
43
|
+
* its own mark at the server's shared `hw` rather than being rebased onto it,
|
|
44
|
+
* so a second attempt appends AFTER the first instead of being deleted by it,
|
|
45
|
+
* and a retry inside one run still trims exactly as before. The first cut
|
|
46
|
+
* rebased an unfamiliar run onto the one shared mark, so a takeover's old box,
|
|
47
|
+
* still flushing its last seconds, spliced its late steps between the new
|
|
48
|
+
* run's — a superseded run's late steps are COUNTED against the shared mark
|
|
49
|
+
* now (the trace is not lost) and never SPLICED between the live run's.
|
|
45
50
|
*
|
|
46
51
|
* SCRUB EVERY STRING. This is the CLI's own stdout — a command echoing an env
|
|
47
52
|
* var, a read of a config file — riding the same uplink the answer does. Prose
|
|
@@ -163,8 +168,13 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
|
|
|
163
168
|
dirty = true;
|
|
164
169
|
return;
|
|
165
170
|
}
|
|
166
|
-
|
|
167
|
-
base
|
|
171
|
+
// Remove only what of the batch is STILL at the head: a push during the
|
|
172
|
+
// await may have shed some of it already (advancing `base`), and
|
|
173
|
+
// splicing the whole batch length would then drop entries that were
|
|
174
|
+
// never sent — a gap the buffer had room to avoid.
|
|
175
|
+
const drop = Math.max(0, seq + entries.length - base);
|
|
176
|
+
queue.splice(0, drop);
|
|
177
|
+
base += drop;
|
|
168
178
|
}
|
|
169
179
|
};
|
|
170
180
|
|