flowviant 0.97.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.
Files changed (45) hide show
  1. package/bin/cli.mjs +13 -3
  2. package/bin/lib/agentPlan.mjs +159 -37
  3. package/bin/lib/artifacts.mjs +136 -26
  4. package/bin/lib/authReject.mjs +32 -0
  5. package/bin/lib/authproxy.mjs +52 -16
  6. package/bin/lib/childEnv.mjs +33 -1
  7. package/bin/lib/claude.mjs +166 -71
  8. package/bin/lib/config.mjs +24 -1
  9. package/bin/lib/credentials.mjs +27 -4
  10. package/bin/lib/deploy.mjs +364 -30
  11. package/bin/lib/env.mjs +130 -13
  12. package/bin/lib/fleet.mjs +99 -28
  13. package/bin/lib/git.mjs +131 -3
  14. package/bin/lib/hooks/readGuard.mjs +24 -0
  15. package/bin/lib/instance.mjs +115 -14
  16. package/bin/lib/knowledge.mjs +180 -60
  17. package/bin/lib/landed.mjs +38 -30
  18. package/bin/lib/listeners.mjs +211 -0
  19. package/bin/lib/localSessions.mjs +29 -4
  20. package/bin/lib/login.mjs +2 -2
  21. package/bin/lib/machines.mjs +16 -7
  22. package/bin/lib/preview.mjs +95 -6
  23. package/bin/lib/printable.mjs +42 -0
  24. package/bin/lib/procRegistry.mjs +65 -0
  25. package/bin/lib/processes.mjs +20 -7
  26. package/bin/lib/projectTools.mjs +248 -0
  27. package/bin/lib/prompts.mjs +16 -11
  28. package/bin/lib/resources.mjs +46 -4
  29. package/bin/lib/runtimeOfferings.mjs +75 -0
  30. package/bin/lib/runtimes.mjs +71 -9
  31. package/bin/lib/shipMerge.mjs +8 -4
  32. package/bin/lib/trace.mjs +16 -6
  33. package/bin/lib/update.mjs +5 -0
  34. package/bin/lib/work.mjs +1192 -4584
  35. package/bin/lib/workAgentMerges.mjs +504 -0
  36. package/bin/lib/workAgentPlans.mjs +447 -0
  37. package/bin/lib/workAgentReview.mjs +702 -0
  38. package/bin/lib/workAgentTurns.mjs +1148 -0
  39. package/bin/lib/workDiffs.mjs +107 -0
  40. package/bin/lib/workPreviews.mjs +351 -0
  41. package/bin/lib/workProcesses.mjs +236 -0
  42. package/bin/lib/workPullRequests.mjs +313 -0
  43. package/bin/lib/workShip.mjs +385 -0
  44. package/bin/lib/worktreeDiff.mjs +99 -43
  45. package/package.json +2 -2
@@ -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 text =
346
- (item.item_type ?? item.type) === 'agent_message'
347
- ? `${item.text ?? item.message ?? ''}\n`
348
- : '';
349
- return { activity, text };
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.toString();
1529
+ buf += d;
1468
1530
  let nl;
1469
1531
  while (!settled && (nl = buf.indexOf('\n')) >= 0) {
1470
1532
  const line = buf.slice(0, nl);
@@ -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
- git(['push', 'origin', `HEAD:${baseBranchName(baseRef())}`], tmpDir);
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
- git(['fetch', 'origin', '--quiet'], repoRoot);
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
- git(['fetch', 'origin', '--quiet'], repoRoot);
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: the server rebases an
42
- * unfamiliar one onto its current mark, so a second attempt appends AFTER the
43
- * first instead of being deleted by it, and a retry inside one run still trims
44
- * exactly as before.
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
- queue.splice(0, entries.length);
167
- base += entries.length;
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
 
@@ -100,6 +100,11 @@ function reexec(teardown, { viaNpx = false, target = null } = {}) {
100
100
  env: {
101
101
  ...process.env,
102
102
  FLOWVIANT_REEXEC: '1',
103
+ // WHICH DAEMON THE SUCCESSOR IS REPLACING. Under npx the successor's
104
+ // parent is `npm exec`, not us, so the instance lock's ppid check read us
105
+ // as a rival and the new daemon SIGTERMed this proxy. instance.mjs adopts
106
+ // the lock from this pid when it is really an ancestor of the successor.
107
+ FLOWVIANT_REEXEC_FROM: String(process.pid),
103
108
  // WHAT WE RESTARTED IN ORDER TO BECOME. The successor compares its own
104
109
  // VERSION against this: if it came back still short, the update did not
105
110
  // take (a registry serving a stale `latest`, an npx cache that refused to