flowviant 0.125.21 → 0.125.23
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 +5 -0
- package/bin/lib/agentBrief.mjs +4 -0
- package/bin/lib/agentCards.mjs +24 -1
- package/bin/lib/agentPlan.mjs +3 -0
- package/bin/lib/agentQueueContext.mjs +20 -1
- package/bin/lib/agentTaskKinds.mjs +23 -3
- package/bin/lib/agentTaskTypes.mjs +9 -1
- package/bin/lib/artifactContracts.mjs +116 -1
- package/bin/lib/artifactPolicy.mjs +5 -0
- package/bin/lib/artifactPreview.mjs +4 -3
- package/bin/lib/artifacts.mjs +4 -0
- package/bin/lib/codexFencedMount.mjs +3 -0
- package/bin/lib/codexSubagents.mjs +6 -2
- package/bin/lib/knowledgeFetch.mjs +8 -3
- package/bin/lib/knowledgeFiles.mjs +4 -4
- package/bin/lib/knowledgeLibrary.mjs +14 -10
- package/bin/lib/orchestrator.mjs +40 -14
- package/bin/lib/orchestratorHooks.mjs +15 -2
- package/bin/lib/orchestratorNative.mjs +30 -9
- package/bin/lib/pinnedCapture.mjs +58 -7
- package/bin/lib/projectTools.mjs +4 -1
- package/bin/lib/prompts.mjs +35 -7
- package/bin/lib/runtimeCodex.mjs +1 -1
- package/bin/lib/turnGaps.mjs +47 -0
- package/bin/lib/uiReviewContract.mjs +1 -1
- package/bin/lib/videoEncode.mjs +406 -0
- package/bin/lib/videoRender.mjs +529 -0
- package/bin/lib/workAgentPrecheck.mjs +4 -1
- package/bin/lib/workAgentTools.mjs +8 -4
- package/bin/lib/workAgentTurnExecution.mjs +19 -4
- package/bin/lib/workAgentTurnOutcome.mjs +13 -3
- package/bin/lib/worktreeDiff.mjs +154 -55
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { reportLocalCleanup } from './localCleanupReport.mjs';
|
|
|
2
2
|
import { createManagedLocalCleanup } from './managedLocalCleanup.mjs';
|
|
3
3
|
import { createCaptureRetention } from './referenceCaptures.mjs';
|
|
4
4
|
import { imageRevisionInstructions } from './artifactIteration.mjs';
|
|
5
|
-
import { agentChildScope } from './agentQueueContext.mjs';
|
|
5
|
+
import { agentChildScope, sourceChatText } from './agentQueueContext.mjs';
|
|
6
6
|
import { measureTestFindings } from './testFindings.mjs';
|
|
7
7
|
import { fetchResultInputs } from './resultInputs.mjs';
|
|
8
8
|
import { agentWork } from './workActivity.mjs';
|
|
@@ -33,7 +33,7 @@ import { setRuntimeLimit } from './daemonState.mjs';
|
|
|
33
33
|
import { gitOrNull } from './git.mjs';
|
|
34
34
|
import { turnCommits } from './turnReceipts.mjs';
|
|
35
35
|
import { addUsage } from './turnUsage.mjs';
|
|
36
|
-
import { stashCard } from './agentCards.mjs';
|
|
36
|
+
import { stashCard, stashGaps } from './agentCards.mjs';
|
|
37
37
|
import { c } from './ui.mjs';
|
|
38
38
|
import { runTurn } from './runTurn.mjs';
|
|
39
39
|
import { liveGroups, processesSupported } from './processes.mjs';
|
|
@@ -67,6 +67,7 @@ import { codexSubagentsSafe } from './runtimeCodex.mjs';
|
|
|
67
67
|
import { measuredRuntimeVersion } from './runtimeDetection.mjs';
|
|
68
68
|
import { createIntermediateArtifacts } from './intermediateArtifacts.mjs';
|
|
69
69
|
import { createUiReview } from './uiReview.mjs';
|
|
70
|
+
import { createVideoRender } from './videoRender.mjs';
|
|
70
71
|
import { withUiReview, appendUiReviewEvidence } from './uiReviewTurn.mjs';
|
|
71
72
|
|
|
72
73
|
export function createAgentTurnExecution({
|
|
@@ -98,6 +99,7 @@ export function createAgentTurnExecution({
|
|
|
98
99
|
runTurnImpl = runTurn,
|
|
99
100
|
exportModel,
|
|
100
101
|
makeUiReview = createUiReview,
|
|
102
|
+
makeVideoRender = createVideoRender,
|
|
101
103
|
}) {
|
|
102
104
|
const reportDelivery = createDeliveryReporter();
|
|
103
105
|
/** How long the final flush may hold the settle. Bounded because the settle
|
|
@@ -326,7 +328,7 @@ export function createAgentTurnExecution({
|
|
|
326
328
|
stashCard(
|
|
327
329
|
sessionMetaPath(wt, 'flowviant-agent-cards', agentId),
|
|
328
330
|
job.task.id,
|
|
329
|
-
AGENT_TASK_SPEC(job.task, job.cardThread, files.card)
|
|
331
|
+
AGENT_TASK_SPEC(job.task, job.cardThread, files.card) + sourceChatText(job.queueContext, job.task.id)
|
|
330
332
|
);
|
|
331
333
|
}
|
|
332
334
|
|
|
@@ -404,9 +406,14 @@ export function createAgentTurnExecution({
|
|
|
404
406
|
let uiReview = null;
|
|
405
407
|
let uiReviewNote = '';
|
|
406
408
|
let uiReviewEvidence = '';
|
|
409
|
+
// A video card's render tool (videoRender.mjs), and the words a turn
|
|
410
|
+
// gets when it could not be held, so it stops rather than guesses.
|
|
411
|
+
let videoRender = null;
|
|
412
|
+
let videoNote = '';
|
|
407
413
|
const runCli = (args) => {
|
|
408
414
|
if (run.interrupted) return Promise.resolve('');
|
|
409
415
|
args = withUiReview(args, uiReviewNote, uiReview?.env);
|
|
416
|
+
args = withUiReview(args, videoNote);
|
|
410
417
|
return runTurnImpl(args);
|
|
411
418
|
};
|
|
412
419
|
/**
|
|
@@ -468,6 +475,10 @@ export function createAgentTurnExecution({
|
|
|
468
475
|
uiReviewEvidence = uiReviewNote;
|
|
469
476
|
}
|
|
470
477
|
}
|
|
478
|
+
if (taskKind === 'video') {
|
|
479
|
+
try { videoRender = await makeVideoRender({ wt, signal: setupController.signal }); }
|
|
480
|
+
catch (error) { videoNote = `The card_video render tool could not be started on this machine: ${envScrub(error.message)}. Without it no GIF, MP4 or WebM can be rendered: say so in your answer and stop.`; }
|
|
481
|
+
}
|
|
471
482
|
if (orchestrating) {
|
|
472
483
|
orchestrator = await createOrchestrator({ turnId, runtime: rt, trace, admit,
|
|
473
484
|
assignmentContext: () => agentChildScope(job),
|
|
@@ -478,7 +489,7 @@ export function createAgentTurnExecution({
|
|
|
478
489
|
notify: message => liveInput ? run.live?.inject(message) === true : false,
|
|
479
490
|
taken: id => run.live?.onTaken(id) });
|
|
480
491
|
}
|
|
481
|
-
const tools = prepareCardTools({ rt, posture, wt, repoRoot, baseRef, sources: repositoryRun?.sources ?? [], repositoryTool: repositoryRun?.tool ?? null, orchestratorConfig: orchestrator?.config ?? null, uiReviewConfig: uiReview?.config, orchestrated: Boolean(orchestrator), codexHome: agentResume.codexHome });
|
|
492
|
+
const tools = prepareCardTools({ rt, posture, wt, repoRoot, baseRef, sources: repositoryRun?.sources ?? [], repositoryTool: repositoryRun?.tool ?? null, orchestratorConfig: orchestrator?.config ?? null, uiReviewConfig: uiReview?.config, videoConfig: videoRender?.config ?? null, orchestrated: Boolean(orchestrator), codexHome: agentResume.codexHome });
|
|
482
493
|
projectTools = tools.projectTools;
|
|
483
494
|
localCleanup?.begin(turnId);
|
|
484
495
|
const { toolsNote } = tools;
|
|
@@ -733,6 +744,7 @@ export function createAgentTurnExecution({
|
|
|
733
744
|
// The builder has finished its fix/recheck loop. Close only resources
|
|
734
745
|
// owned by its review tool, then bind evidence to the final source
|
|
735
746
|
// before the existing artifact upload and delivery settlement.
|
|
747
|
+
try { await videoRender?.close(); } catch { /* a closed listener */ }
|
|
736
748
|
try { await uiReview?.close(); }
|
|
737
749
|
finally {
|
|
738
750
|
try { uiReviewEvidence = uiReview?.finalize() ?? uiReviewEvidence; }
|
|
@@ -845,6 +857,9 @@ export function createAgentTurnExecution({
|
|
|
845
857
|
modelExports: intermediateDelivery?.modelExports,
|
|
846
858
|
});
|
|
847
859
|
appendUiReviewEvidence(body, uiReviewEvidence);
|
|
860
|
+
// The agent's own account of what it did not deliver, for the pre-review.
|
|
861
|
+
if (job.kind === 'task' && job.task && body.outcome === 'delivered' && res?.gaps)
|
|
862
|
+
stashGaps(sessionMetaPath(wt, 'flowviant-agent-cards', agentId), job.task.id, res.gaps);
|
|
848
863
|
if (body.outcome === 'delivered') intermediateDelivery?.commit();
|
|
849
864
|
// This place's writer lock is still held. Measure the exited children
|
|
850
865
|
// and their groups before releasing any file requirements. Dirty repository files are unrelated to managed scratch eligibility.
|
|
@@ -34,6 +34,7 @@ import { usableImageArtifact } from './imageArtifact.mjs';
|
|
|
34
34
|
import { withoutTurnDeclaration } from './turnDeclaration.mjs';
|
|
35
35
|
import { ANSWER_DELIVERED_USES, researchUseOf, withResearchHeader } from './researchUse.mjs';
|
|
36
36
|
import { measuredModelExports } from './intermediateArtifacts.mjs';
|
|
37
|
+
import { gapsMarkdown } from './turnGaps.mjs';
|
|
37
38
|
|
|
38
39
|
/** Materialization runs only for a declared delivery; settlement remains the
|
|
39
40
|
* sole parser and the sole authority deciding whether it really delivered. */
|
|
@@ -166,6 +167,11 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
|
|
|
166
167
|
: prerequisite?.taskKind === 'image' && (job.runtime ?? 'claude') !== 'codex'
|
|
167
168
|
? 'This agent runs on a CLI that cannot generate images. Image cards require Codex’s own image generation; this agent cannot run the requested texture work.'
|
|
168
169
|
: null;
|
|
170
|
+
/* WHAT IT SAYS IT DID NOT DELIVER (turnGaps.mjs), where the person reads
|
|
171
|
+
the delivery: Verification on a code card, which carries it; the summary
|
|
172
|
+
on every other kind, which has no Verification lane. */
|
|
173
|
+
const gaps = res.outcome === 'delivered' && !cannotRequest ? gapsMarkdown(res.gaps) : null;
|
|
174
|
+
const verification = [res.verification, kind.posture === 'build' ? gaps : null].filter(Boolean).join('\n\n');
|
|
169
175
|
return report(
|
|
170
176
|
{
|
|
171
177
|
turnId,
|
|
@@ -173,16 +179,20 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
|
|
|
173
179
|
outcome: res.outcome,
|
|
174
180
|
answer: cannotRequest ?? envScrub((kind.posture === 'research' && researchUse
|
|
175
181
|
? withResearchHeader(researchUse, res.answer ?? '') : res.answer ?? '') +
|
|
176
|
-
(job.task?.taskType === 'tests' && res.raised?.length ? '\n\nAdditional CLI-reported findings (not filed as fixes):\n' + res.raised.map(r => `${r.title}\n${r.brief ?? ''}`).join('\n\n') : '')
|
|
182
|
+
(job.task?.taskType === 'tests' && res.raised?.length ? '\n\nAdditional CLI-reported findings (not filed as fixes):\n' + res.raised.map(r => `${r.title}\n${r.brief ?? ''}`).join('\n\n') : '') +
|
|
183
|
+
(gaps && kind.posture !== 'build' ? `\n\n${gaps}` : '')),
|
|
177
184
|
// CLI-authored evidence travels separately from the short answer and
|
|
178
185
|
// measured commits/checks. Older turns simply send no report.
|
|
179
|
-
...(kind.posture === 'build' && res.outcome === 'delivered' &&
|
|
180
|
-
? { verification: envScrub(
|
|
186
|
+
...(kind.posture === 'build' && res.outcome === 'delivered' && verification
|
|
187
|
+
? { verification: envScrub(verification) } : {}),
|
|
181
188
|
...(kind.posture === 'build' && res.outcome === 'delivered' && res.handover
|
|
182
189
|
? { handover: envScrub(res.handover) } : {}),
|
|
183
190
|
...(kind.posture === 'build' && res.outcome === 'delivered' && res.automation
|
|
184
191
|
? { automation: envScrub(res.automation) } : {}),
|
|
185
192
|
...(commits.length ? { commits } : {}),
|
|
193
|
+
// …and the same gaps as data, so the server can propose the parts no
|
|
194
|
+
// card owned (`gapFollowUps.ts`). A report: an older server drops it.
|
|
195
|
+
...(gaps && res.gaps?.length ? { gaps: res.gaps } : {}),
|
|
186
196
|
// SCRUBBED like the answer beside it: a raised card lands in the
|
|
187
197
|
// project doc every member reads, and a brief quoting the `.env` the
|
|
188
198
|
// agent just read ("value sk_live_… is logged in pay.ts") went out
|
package/bin/lib/worktreeDiff.mjs
CHANGED
|
@@ -13,6 +13,17 @@
|
|
|
13
13
|
* uncommitted work in the same total. Untracked files count too: git calls them
|
|
14
14
|
* nothing until they are added, and a human calls them new work.
|
|
15
15
|
*
|
|
16
|
+
* …AND THAT WHOLE SPLIT IN TWO (2026-10-25). The agent page's Changes counts
|
|
17
|
+
* the branch's COMMITS, so beside the whole it read as a disagreement: "Native
|
|
18
|
+
* 3D viewer" said +78 −12 in Changes and +114 −15 on the branch row (a running
|
|
19
|
+
* card's commit on top), and "New task submission" said "Nothing committed
|
|
20
|
+
* yet" beside +613 −294 of work nobody had committed. `committed` is
|
|
21
|
+
* `<merge-base>..HEAD` — the figure Changes agrees with — and `uncommitted` is
|
|
22
|
+
* HEAD against the working tree, untracked files again counted as additions,
|
|
23
|
+
* with its own rows so the page can list it apart. The halves need not sum to
|
|
24
|
+
* the whole: a line a commit changed and the working tree changed back counts
|
|
25
|
+
* in both and in neither, which is git's arithmetic, relayed.
|
|
26
|
+
*
|
|
16
27
|
* It also carries THIS BRANCH'S OWN COMMITS and the cards they name — see
|
|
17
28
|
* `branchCommits` below. That rides here rather than on a route of its own
|
|
18
29
|
* because this sweep is already standing in the right directory on the right
|
|
@@ -252,16 +263,104 @@ function countLines(buf) {
|
|
|
252
263
|
return s.endsWith('\n') ? n - 1 : n;
|
|
253
264
|
}
|
|
254
265
|
|
|
266
|
+
/** One diffstat row. The path is clamped to the server's zod cap (file path ≤
|
|
267
|
+
* 300 — see the caps at worktreeDiff's return): one over-cap string would 400
|
|
268
|
+
* the whole report batch, and a readout must degrade to a shorter label, never
|
|
269
|
+
* to silence. Null for an empty path, which names nothing. */
|
|
270
|
+
function fileRow(path, added, deleted, binary = false) {
|
|
271
|
+
if (!path) return null;
|
|
272
|
+
path = path.slice(0, 300);
|
|
273
|
+
return binary ? { path, added, deleted, binary } : { path, added, deleted };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* `git diff --numstat -z <revs…> --`, read as rows — the one reader of that
|
|
278
|
+
* output, for the whole and both halves alike. Throws when git does.
|
|
279
|
+
*
|
|
280
|
+
* `-z`, for the reason gitRaw exists: git's LINE-based output C-quotes any path
|
|
281
|
+
* that is not plain ASCII, so an accented filename arrives as "n\303\251w.txt"
|
|
282
|
+
* — a string that is not the path and reads as escaped garbage in the rail.
|
|
283
|
+
* And `--numstat -z` frames a normal change as one field, "added\tdeleted\tpath",
|
|
284
|
+
* but a RENAME as three: "added\tdeleted\t" (empty path), then the old path,
|
|
285
|
+
* then the new one. An empty path is therefore the rename marker and the next
|
|
286
|
+
* two fields belong to it — read line-wise, a rename would report a file
|
|
287
|
+
* literally named "old => new".
|
|
288
|
+
*/
|
|
289
|
+
function numstatRows(revs, wt) {
|
|
290
|
+
const rows = [];
|
|
291
|
+
const fields = splitNul(gitRaw(['diff', '--numstat', '-z', ...revs, '--'], wt));
|
|
292
|
+
for (let i = 0; i < fields.length; i++) {
|
|
293
|
+
const [a, d, ...rest] = fields[i].split('\t');
|
|
294
|
+
let path = rest.join('\t');
|
|
295
|
+
if (!path) {
|
|
296
|
+
path = fields[i + 2] ?? fields[i + 1]; // the post-rename name is what exists now
|
|
297
|
+
i += 2;
|
|
298
|
+
if (!path) continue;
|
|
299
|
+
}
|
|
300
|
+
const binary = a === '-' || d === '-';
|
|
301
|
+
const row = fileRow(path, binary ? 0 : Number(a) || 0, binary ? 0 : Number(d) || 0, binary);
|
|
302
|
+
if (row) rows.push(row);
|
|
303
|
+
}
|
|
304
|
+
return rows;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** The same rows, or NULL when git would not answer — kept apart from an
|
|
308
|
+
* empty diff, so a failed read omits its half rather than reporting zeros. */
|
|
309
|
+
function numstatOrNull(revs, wt) {
|
|
310
|
+
try {
|
|
311
|
+
return numstatRows(revs, wt);
|
|
312
|
+
} catch {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** A diffstat summed from its rows: totals and `fileCount` over ALL of them,
|
|
318
|
+
* and the biggest MAX_FILES as the rows reported, `truncated` saying how many
|
|
319
|
+
* were left out. */
|
|
320
|
+
function diffstat(rows) {
|
|
321
|
+
// Biggest first: with a 20-row cap, the rows that survive should be the ones
|
|
322
|
+
// worth looking at. Ties break by path so the list doesn't shuffle per sweep.
|
|
323
|
+
const files = [...rows].sort(
|
|
324
|
+
(x, y) => y.added + y.deleted - (x.added + x.deleted) || (x.path < y.path ? -1 : 1)
|
|
325
|
+
);
|
|
326
|
+
let additions = 0;
|
|
327
|
+
let deletions = 0;
|
|
328
|
+
for (const f of files) {
|
|
329
|
+
additions += f.added;
|
|
330
|
+
deletions += f.deleted;
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
additions,
|
|
334
|
+
deletions,
|
|
335
|
+
fileCount: files.length,
|
|
336
|
+
truncated: Math.max(0, files.length - MAX_FILES),
|
|
337
|
+
files: files.slice(0, MAX_FILES),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
255
341
|
/**
|
|
256
342
|
* @param {string} wt the worktree directory
|
|
257
343
|
* @param {string} baseRef the project's base ref (e.g. `origin/main`)
|
|
258
|
-
* @returns {null | {branch:string, path:string, ahead:number,
|
|
259
|
-
*
|
|
344
|
+
* @returns {null | {branch:string, headSha?:string, path:string, ahead:number,
|
|
345
|
+
* behind:number, baseLabel:string,
|
|
346
|
+
* baseCommits:{sha:string, subject:string, author:string}[],
|
|
260
347
|
* dirty:boolean, additions:number, deletions:number, fileCount:number,
|
|
261
348
|
* truncated:number,
|
|
262
349
|
* files:{path:string, added:number, deleted:number, binary?:boolean}[],
|
|
350
|
+
* committed?:{additions:number, deletions:number, fileCount:number},
|
|
351
|
+
* uncommitted?:{additions:number, deletions:number, fileCount:number,
|
|
352
|
+
* truncated:number,
|
|
353
|
+
* files:{path:string, added:number, deleted:number, binary?:boolean}[]},
|
|
263
354
|
* commits:{sha:string, subject:string, author:string, at:string,
|
|
264
355
|
* additions:number, deletions:number, taskIds:string[]}[]}}
|
|
356
|
+
*
|
|
357
|
+
* The top-level additions/deletions/fileCount/truncated/files are the WHOLE
|
|
358
|
+
* (merge-base against the working tree, untracked included) — what the tab
|
|
359
|
+
* rails read. `committed` (`<merge-base>..HEAD`) and `uncommitted` (HEAD
|
|
360
|
+
* against the working tree, untracked included, with its own rows) split it
|
|
361
|
+
* for the agent page, which agrees with the first and lists the second apart.
|
|
362
|
+
* `committed` is absent without a merge-base; either half is absent when its
|
|
363
|
+
* git read failed — unknown, never a fabricated zero.
|
|
265
364
|
*/
|
|
266
365
|
export function worktreeDiff(wt, baseRef) {
|
|
267
366
|
if (!wt || !existsSync(wt)) return null;
|
|
@@ -289,52 +388,24 @@ export function worktreeDiff(wt, baseRef) {
|
|
|
289
388
|
HEAD below, which still reports the uncommitted half honestly */
|
|
290
389
|
}
|
|
291
390
|
|
|
292
|
-
const files = [];
|
|
293
|
-
let additions = 0;
|
|
294
|
-
let deletions = 0;
|
|
295
|
-
const push = (path, added, deleted, binary = false) => {
|
|
296
|
-
if (!path) return;
|
|
297
|
-
// Clamped to the server's zod cap (file path ≤ 300 — see the caps at the
|
|
298
|
-
// return, below): one over-cap string would 400 the whole report batch,
|
|
299
|
-
// and a readout must degrade to a shorter label, never to silence.
|
|
300
|
-
path = path.slice(0, 300);
|
|
301
|
-
files.push(binary ? { path, added, deleted, binary } : { path, added, deleted });
|
|
302
|
-
additions += added;
|
|
303
|
-
deletions += deleted;
|
|
304
|
-
};
|
|
305
|
-
|
|
306
391
|
// Tracked: working tree vs base. `git diff <base>` (no --cached, no second
|
|
307
|
-
// ref) is exactly "everything this session did", committed or not.
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
// `--numstat -z` frames a normal change as one field, "added\tdeleted\tpath",
|
|
313
|
-
// but a RENAME as three: "added\tdeleted\t" (empty path), then the old path,
|
|
314
|
-
// then the new one. An empty path is therefore the rename marker and the
|
|
315
|
-
// next two fields belong to it — read line-wise, a rename would report a
|
|
316
|
-
// file literally named "old => new".
|
|
317
|
-
const fields = splitNul(gitRaw(['diff', '--numstat', '-z', base || 'HEAD', '--'], wt));
|
|
318
|
-
for (let i = 0; i < fields.length; i++) {
|
|
319
|
-
const [a, d, ...rest] = fields[i].split('\t');
|
|
320
|
-
let path = rest.join('\t');
|
|
321
|
-
if (!path) {
|
|
322
|
-
path = fields[i + 2] ?? fields[i + 1]; // the post-rename name is what exists now
|
|
323
|
-
i += 2;
|
|
324
|
-
if (!path) continue;
|
|
325
|
-
}
|
|
326
|
-
const binary = a === '-' || d === '-';
|
|
327
|
-
push(path, binary ? 0 : Number(a) || 0, binary ? 0 : Number(d) || 0, binary);
|
|
328
|
-
}
|
|
329
|
-
} catch {
|
|
330
|
-
/* report what we have */
|
|
331
|
-
}
|
|
392
|
+
// ref) is exactly "everything this session did", committed or not. Without a
|
|
393
|
+
// merge-base it is HEAD, which is the uncommitted half alone — kept as rows
|
|
394
|
+
// OR NULL, so that half can reuse this read below without mistaking a failed
|
|
395
|
+
// one for a clean tree.
|
|
396
|
+
const trackedRows = numstatOrNull([base || 'HEAD'], wt);
|
|
332
397
|
|
|
333
398
|
// Untracked, minus everything gitignored — new files are the most visible
|
|
334
399
|
// work a session does and they would otherwise show as nothing at all. `-z`
|
|
335
400
|
// here is not cosmetic: a C-quoted untracked name fails the statSync below,
|
|
336
401
|
// and the catch swallowed the row — a session's brand-new `café.txt` simply
|
|
337
|
-
// vanished from the rail.
|
|
402
|
+
// vanished from the rail. Scanned ONCE: these rows belong to the whole and to
|
|
403
|
+
// the uncommitted half alike, and the scan opens files.
|
|
404
|
+
const untracked = [];
|
|
405
|
+
const pushUntracked = (path, added, binary = false) => {
|
|
406
|
+
const row = fileRow(path, added, 0, binary);
|
|
407
|
+
if (row) untracked.push(row);
|
|
408
|
+
};
|
|
338
409
|
try {
|
|
339
410
|
const others = splitNul(gitRaw(['ls-files', '--others', '--exclude-standard', '-z'], wt));
|
|
340
411
|
for (const path of others.slice(0, MAX_UNTRACKED_SCAN)) {
|
|
@@ -343,12 +414,12 @@ export function worktreeDiff(wt, baseRef) {
|
|
|
343
414
|
const st = statSync(full);
|
|
344
415
|
if (!st.isFile()) continue;
|
|
345
416
|
if (st.size > MAX_COUNT_BYTES) {
|
|
346
|
-
|
|
417
|
+
pushUntracked(path, 0, true);
|
|
347
418
|
continue;
|
|
348
419
|
}
|
|
349
420
|
const lines = countLines(readFileSync(full));
|
|
350
|
-
if (lines === null)
|
|
351
|
-
else
|
|
421
|
+
if (lines === null) pushUntracked(path, 0, true);
|
|
422
|
+
else pushUntracked(path, lines);
|
|
352
423
|
} catch {
|
|
353
424
|
/* vanished between listing and reading — it wasn't there to report */
|
|
354
425
|
}
|
|
@@ -357,6 +428,37 @@ export function worktreeDiff(wt, baseRef) {
|
|
|
357
428
|
/* report what we have */
|
|
358
429
|
}
|
|
359
430
|
|
|
431
|
+
const whole = diffstat([...(trackedRows ?? []), ...untracked]);
|
|
432
|
+
|
|
433
|
+
// THE WHOLE, SPLIT IN TWO — see the header. Each half is its own git read
|
|
434
|
+
// and each degrades alone: a half git would not answer is OMITTED, because
|
|
435
|
+
// a zero there would claim a clean side nobody measured.
|
|
436
|
+
//
|
|
437
|
+
// Committed: `git diff <merge-base> HEAD`, only with a merge-base. Without
|
|
438
|
+
// one there is no "this branch's commits" to count, and the fallback above
|
|
439
|
+
// already diffed HEAD, which is the other half.
|
|
440
|
+
let committed = null;
|
|
441
|
+
if (base) {
|
|
442
|
+
const rows = numstatOrNull([base, 'HEAD'], wt);
|
|
443
|
+
if (rows) {
|
|
444
|
+
const { additions, deletions, fileCount } = diffstat(rows);
|
|
445
|
+
committed = { additions, deletions, fileCount };
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// Uncommitted: `git diff HEAD` plus the untracked rows. An UNBORN HEAD has
|
|
449
|
+
// no commit, so everything there is uncommitted and the half is the whole —
|
|
450
|
+
// `git diff HEAD` fails on it, and that failure must not drop the half
|
|
451
|
+
// (defensive today: the branch read above already returns null on an unborn
|
|
452
|
+
// HEAD). With no merge-base the whole's tracked read already WAS `git diff
|
|
453
|
+
// HEAD`, so it is reused rather than run twice.
|
|
454
|
+
let uncommitted = null;
|
|
455
|
+
if (!headSha) {
|
|
456
|
+
uncommitted = { ...whole };
|
|
457
|
+
} else {
|
|
458
|
+
const rows = base ? numstatOrNull(['HEAD'], wt) : trackedRows;
|
|
459
|
+
if (rows) uncommitted = diffstat([...rows, ...untracked]);
|
|
460
|
+
}
|
|
461
|
+
|
|
360
462
|
let ahead = 0;
|
|
361
463
|
try {
|
|
362
464
|
if (base) ahead = Number(git(['rev-list', '--count', `${base}..HEAD`], wt)) || 0;
|
|
@@ -403,11 +505,6 @@ export function worktreeDiff(wt, baseRef) {
|
|
|
403
505
|
/* leave at false */
|
|
404
506
|
}
|
|
405
507
|
|
|
406
|
-
// Biggest first: with a 20-row cap, the rows that survive should be the ones
|
|
407
|
-
// worth looking at. Ties break by path so the list doesn't shuffle per sweep.
|
|
408
|
-
files.sort(
|
|
409
|
-
(x, y) => y.added + y.deleted - (x.added + x.deleted) || (x.path < y.path ? -1 : 1)
|
|
410
|
-
);
|
|
411
508
|
// Every string here is clamped to the server's own zod caps (branch ≤ 200,
|
|
412
509
|
// baseLabel ≤ 120, subject ≤ 200, author ≤ 80, file path ≤ 300): the report
|
|
413
510
|
// rides in a BATCH, so a single over-cap string — a generated branch name, a
|
|
@@ -421,11 +518,13 @@ export function worktreeDiff(wt, baseRef) {
|
|
|
421
518
|
baseLabel: String(baseRef).replace(/^origin\//, '').slice(0, 120),
|
|
422
519
|
baseCommits,
|
|
423
520
|
dirty,
|
|
424
|
-
additions,
|
|
425
|
-
deletions,
|
|
426
|
-
fileCount:
|
|
427
|
-
truncated:
|
|
428
|
-
files: files
|
|
521
|
+
additions: whole.additions,
|
|
522
|
+
deletions: whole.deletions,
|
|
523
|
+
fileCount: whole.fileCount,
|
|
524
|
+
truncated: whole.truncated,
|
|
525
|
+
files: whole.files,
|
|
526
|
+
...(committed ? { committed } : {}),
|
|
527
|
+
...(uncommitted ? { uncommitted } : {}),
|
|
429
528
|
commits: branchCommits(wt, base),
|
|
430
529
|
};
|
|
431
530
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.125.
|
|
3
|
+
"version": "0.125.23",
|
|
4
4
|
"description": "Run your own coding CLIs as build agents for Flowviant \u2014 Claude Code, Codex or Antigravity, on your own credentials. Holds your sessions, keeps a worktree per tab, and ships branches on your word.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|