flowviant 0.96.0 → 0.99.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/bin/cli.mjs +13 -3
- package/bin/lib/agentPlan.mjs +159 -37
- package/bin/lib/artifacts.mjs +174 -33
- 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 +377 -45
- package/bin/lib/config.mjs +24 -1
- package/bin/lib/credentials.mjs +27 -4
- package/bin/lib/deploy.mjs +364 -30
- package/bin/lib/env.mjs +146 -12
- package/bin/lib/fleet.mjs +125 -30
- package/bin/lib/git.mjs +131 -3
- package/bin/lib/hooks/readGuard.mjs +162 -0
- package/bin/lib/instance.mjs +115 -14
- package/bin/lib/knowledge.mjs +460 -23
- package/bin/lib/landed.mjs +38 -30
- package/bin/lib/listeners.mjs +211 -0
- package/bin/lib/localSessions.mjs +29 -4
- package/bin/lib/login.mjs +2 -2
- package/bin/lib/machines.mjs +16 -7
- 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 +354 -32
- package/bin/lib/resources.mjs +46 -4
- package/bin/lib/runtimeOfferings.mjs +75 -0
- package/bin/lib/runtimes.mjs +152 -11
- package/bin/lib/shipMerge.mjs +8 -4
- package/bin/lib/trace.mjs +16 -6
- package/bin/lib/update.mjs +5 -0
- package/bin/lib/work.mjs +1261 -4476
- 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 +1148 -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 -2
package/bin/lib/instance.mjs
CHANGED
|
@@ -97,7 +97,7 @@ import {
|
|
|
97
97
|
import { homedir, platform } from 'node:os';
|
|
98
98
|
import { join } from 'node:path';
|
|
99
99
|
import { createHash } from 'node:crypto';
|
|
100
|
-
import { VERSION } from './config.mjs';
|
|
100
|
+
import { VERSION, PROCESS_STARTED_AT } from './config.mjs';
|
|
101
101
|
|
|
102
102
|
/** Deliberately a HASH: a credential must never become a filename. */
|
|
103
103
|
export function instanceLockPath(fleetToken) {
|
|
@@ -139,11 +139,42 @@ function readHolder(path) {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* argv[1] AS IT WAS TYPED, when that differs from the resolved form.
|
|
144
|
+
*
|
|
145
|
+
* Node resolves `process.argv[1]` to an absolute path, while `/proc/<pid>/cmdline`
|
|
146
|
+
* and `ps` show the argv the process was actually given. A daemon started as
|
|
147
|
+
* `node bin/cli.mjs` (a dev checkout, `npm start`, a Docker CMD with a WORKDIR)
|
|
148
|
+
* therefore recorded `/abs/bin/cli.mjs` and was matched against `node
|
|
149
|
+
* bin/cli.mjs`, judged stale WHILE ALIVE — a second start ran beside it and
|
|
150
|
+
* `flowviant stop` signalled nothing. The raw token is recorded beside `entry`
|
|
151
|
+
* so both spellings identify the process. Linux only: `ps` joins argv with
|
|
152
|
+
* spaces, so no token can be recovered from it reliably, and there the absolute
|
|
153
|
+
* form plus the start-time check stand as before.
|
|
154
|
+
*/
|
|
155
|
+
function rawEntry() {
|
|
156
|
+
if (platform() !== 'linux') return null;
|
|
157
|
+
try {
|
|
158
|
+
const tokens = readFileSync('/proc/self/cmdline', 'utf8').split('\0');
|
|
159
|
+
const raw = tokens[1 + process.execArgv.length];
|
|
160
|
+
return raw && raw !== process.argv[1] ? raw : null;
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
142
166
|
const record = (repoRoot) =>
|
|
143
167
|
JSON.stringify({
|
|
144
168
|
pid: process.pid,
|
|
145
169
|
repoRoot,
|
|
146
|
-
|
|
170
|
+
// THE PROCESS'S START, not the moment the lock was written. The start path
|
|
171
|
+
// can wait on a person at the picker or the binding confirm for as long as
|
|
172
|
+
// they take (there is no answer time limit), and a lock line written three
|
|
173
|
+
// minutes after the process began put a LIVE daemon outside the 120s window
|
|
174
|
+
// startedAroundLockWrite brackets — so a second start cleared its "stale"
|
|
175
|
+
// lock and ran beside it, and stop and disconnect skipped it. The window now
|
|
176
|
+
// measures clock granularity only, whatever a prompt cost.
|
|
177
|
+
startedAt: PROCESS_STARTED_AT,
|
|
147
178
|
// The script we were started from, and what we are. A takeover matches the
|
|
148
179
|
// live command line against `entry` before signalling anything — a lock
|
|
149
180
|
// records a PID, and a crashed daemon's PID can be reused by anything.
|
|
@@ -151,9 +182,68 @@ const record = (repoRoot) =>
|
|
|
151
182
|
// matched on the holder's process START TIME instead; stillTheHolder says
|
|
152
183
|
// why that is the weaker of the two claims and still strong enough.
|
|
153
184
|
entry: process.argv[1] || '',
|
|
185
|
+
...(rawEntry() ? { entryRaw: rawEntry() } : {}),
|
|
154
186
|
version: VERSION,
|
|
155
187
|
});
|
|
156
188
|
|
|
189
|
+
/** Up to this many parents are walked looking for the daemon we re-exec'd from.
|
|
190
|
+
* `npx -y flowviant@latest` puts `npm exec` (and a `sh -c`) between the old
|
|
191
|
+
* daemon and the new one; eight is several times that chain. */
|
|
192
|
+
const ANCESTOR_DEPTH = 8;
|
|
193
|
+
|
|
194
|
+
/** The parent pid of `pid`, or null where it cannot be read. */
|
|
195
|
+
function parentOf(pid) {
|
|
196
|
+
try {
|
|
197
|
+
if (platform() === 'linux') {
|
|
198
|
+
const raw = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
199
|
+
const close = raw.lastIndexOf(')');
|
|
200
|
+
if (close < 0) return null;
|
|
201
|
+
const ppid = Number.parseInt(raw.slice(close + 1).trim().split(/\s+/)[1], 10);
|
|
202
|
+
return Number.isInteger(ppid) && ppid > 0 ? ppid : null;
|
|
203
|
+
}
|
|
204
|
+
if (platform() === 'darwin') {
|
|
205
|
+
const out = execFileSync('ps', ['-o', 'ppid=', '-p', String(pid)], {
|
|
206
|
+
encoding: 'utf8',
|
|
207
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
208
|
+
timeout: 3000,
|
|
209
|
+
});
|
|
210
|
+
const ppid = Number.parseInt(out.trim(), 10);
|
|
211
|
+
return Number.isInteger(ppid) && ppid > 0 ? ppid : null;
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
/* unreadable — ancestry unknown */
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* IS THIS HOLDER THE DAEMON THAT RE-EXEC'D US? — the self-update case.
|
|
221
|
+
*
|
|
222
|
+
* The direct parent is the global-install path: update.mjs spawns
|
|
223
|
+
* `process.execPath` itself, so the successor's ppid IS the holder. Under npx
|
|
224
|
+
* — the documented launch — the successor is spawned by `npm exec`, which the
|
|
225
|
+
* old daemon spawned, so the holder is a GRANDPARENT and a ppid check read it
|
|
226
|
+
* as a rival: the new daemon SIGTERMed its own proxy, the foreground process
|
|
227
|
+
* exited 143 while the successor ran on detached (or, with --no-takeover, both
|
|
228
|
+
* exited and the machine went dark after every update). So update.mjs names
|
|
229
|
+
* the pid it re-exec'd from, and that pid is adopted only when it really is an
|
|
230
|
+
* ANCESTOR of this process: the env var alone is inherited by anything the
|
|
231
|
+
* daemon spawns and proves nothing on its own. Ancestry that cannot be read
|
|
232
|
+
* adopts nothing — the pre-existing takeover rules then apply.
|
|
233
|
+
*/
|
|
234
|
+
function isOurReexecParent(pid) {
|
|
235
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
236
|
+
if (pid === process.ppid) return true;
|
|
237
|
+
if (process.env.FLOWVIANT_REEXEC_FROM !== String(pid)) return false;
|
|
238
|
+
let p = process.ppid;
|
|
239
|
+
for (let i = 0; i < ANCESTOR_DEPTH && p && p > 1; i++) {
|
|
240
|
+
const up = parentOf(p);
|
|
241
|
+
if (up === pid) return true;
|
|
242
|
+
p = up;
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
157
247
|
/** Same directory, whatever it is spelled as — symlinks and trailing slashes
|
|
158
248
|
* included. A repo compared by string would let `/repo` and `/repo/` past. */
|
|
159
249
|
function samePath(a, b) {
|
|
@@ -208,7 +298,7 @@ export function daemonInSameRepo(repoRoot, ownPath) {
|
|
|
208
298
|
if (path === ownPath) continue; // our own credential — the lock above owns that question
|
|
209
299
|
const holder = readHolder(path);
|
|
210
300
|
if (!holder || !alive(holder.pid)) continue;
|
|
211
|
-
if (holder.pid
|
|
301
|
+
if (isOurReexecParent(holder.pid)) continue; // ourselves mid self-update re-exec
|
|
212
302
|
if (samePath(holder.repoRoot, repoRoot)) {
|
|
213
303
|
NEIGHBOUR_PATHS.set(holder, path);
|
|
214
304
|
return holder;
|
|
@@ -226,7 +316,10 @@ export function daemonInSameRepo(repoRoot, ownPath) {
|
|
|
226
316
|
* around 41s. 120s is ~3x that worst path and ~4000x the typical one, and it is
|
|
227
317
|
* still short enough that pid reuse cannot reach into it: reuse means cycling
|
|
228
318
|
* the entire pid space (4194304 by default), which no machine does inside two
|
|
229
|
-
* minutes.
|
|
319
|
+
* minutes. (Since the lock records the PROCESS start rather than the moment it
|
|
320
|
+
* was written, a lock from this version sits a few milliseconds from its
|
|
321
|
+
* process whatever the start path cost; the window's width is for locks older
|
|
322
|
+
* daemons wrote, which still stamp the write.) */
|
|
230
323
|
const TAKEOVER_START_WINDOW_MS = 120_000;
|
|
231
324
|
|
|
232
325
|
/** ...and how far the OTHER way, which is a unit problem rather than a real
|
|
@@ -421,15 +514,20 @@ function stillTheHolder(holder) {
|
|
|
421
514
|
// alive, so it takes the same road as a missing one.
|
|
422
515
|
if (!want) return startedAroundLockWrite(holder);
|
|
423
516
|
let cmdline;
|
|
517
|
+
let tokens;
|
|
424
518
|
try {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
519
|
+
if (platform() === 'linux') {
|
|
520
|
+
const raw = readFileSync(`/proc/${holder.pid}/cmdline`, 'utf8');
|
|
521
|
+
tokens = raw.split('\0');
|
|
522
|
+
cmdline = raw.replace(/\0/g, ' ');
|
|
523
|
+
} else {
|
|
524
|
+
cmdline = execFileSync('ps', ['-o', 'command=', '-p', String(holder.pid)], {
|
|
525
|
+
encoding: 'utf8',
|
|
526
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
527
|
+
timeout: 3000,
|
|
528
|
+
});
|
|
529
|
+
tokens = cmdline.trim().split(/\s+/);
|
|
530
|
+
}
|
|
433
531
|
} catch {
|
|
434
532
|
// NOT `false`. takeOverFrom already returned early if the pid were gone, so
|
|
435
533
|
// reaching here means the process is alive and we could not READ it —
|
|
@@ -437,7 +535,10 @@ function stillTheHolder(holder) {
|
|
|
437
535
|
// here is what made the refusal claim the pid belonged to somebody else.
|
|
438
536
|
return null;
|
|
439
537
|
}
|
|
440
|
-
|
|
538
|
+
// The typed spelling is matched as a whole TOKEN, never a substring: a
|
|
539
|
+
// relative `bin/cli.mjs` is a fragment of countless command lines.
|
|
540
|
+
const raw = typeof holder.entryRaw === 'string' && holder.entryRaw ? holder.entryRaw : null;
|
|
541
|
+
if (!cmdline.includes(want) && !(raw && tokens.includes(raw))) return false;
|
|
441
542
|
// AN ENTRY MATCH ALONE IS NOT IDENTITY. Every daemon on the box shares one
|
|
442
543
|
// entry path under a global install, so "cmdline contains this cli.mjs"
|
|
443
544
|
// proves "is SOME flowviant daemon", not "is the daemon that wrote THIS
|
|
@@ -711,7 +812,7 @@ export function acquireInstanceLock(fleetToken, repoRoot, opts = {}) {
|
|
|
711
812
|
// ownership-checked, so it will not delete the lock it handed over.
|
|
712
813
|
// (`flowviant login` also proxies a child, but that parent never reached
|
|
713
814
|
// the daemon and holds nothing — the child simply acquires.)
|
|
714
|
-
if (holder.pid
|
|
815
|
+
if (isOurReexecParent(holder.pid)) {
|
|
715
816
|
try {
|
|
716
817
|
writeFileSync(path, record(repoRoot));
|
|
717
818
|
} catch {
|