flowviant 0.115.0 → 0.117.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 +31 -12
- package/bin/cli.mjs +13 -17
- package/bin/lib/admission.mjs +7 -14
- package/bin/lib/agentInternalWait.mjs +83 -0
- package/bin/lib/agentPlan.mjs +56 -24
- package/bin/lib/agentPublish.mjs +12 -13
- package/bin/lib/agentTaskKinds.mjs +4 -7
- package/bin/lib/agySessions.mjs +3 -3
- package/bin/lib/artifactPreview.mjs +92 -18
- package/bin/lib/artifactReporter.mjs +16 -8
- package/bin/lib/artifacts.mjs +64 -60
- package/bin/lib/authproxy.mjs +2 -1
- package/bin/lib/childEnv.mjs +9 -4
- package/bin/lib/claude.mjs +10 -20
- package/bin/lib/claudePersonal.mjs +71 -6
- package/bin/lib/claudePosture.mjs +119 -42
- package/bin/lib/claudeSessions.mjs +7 -7
- package/bin/lib/claudeStream.mjs +7 -11
- package/bin/lib/cliLimit.mjs +2 -2
- package/bin/lib/codexHome.mjs +17 -0
- package/bin/lib/codexPersonal.mjs +44 -3
- package/bin/lib/codexRelay.mjs +21 -5
- package/bin/lib/codexResearch.mjs +180 -0
- package/bin/lib/config.mjs +37 -24
- package/bin/lib/conversationIds.mjs +51 -0
- package/bin/lib/credentials.mjs +2 -7
- package/bin/lib/daemonLocks.mjs +88 -0
- package/bin/lib/daemonState.mjs +5 -5
- package/bin/lib/deploy.mjs +6 -9
- package/bin/lib/deployCheckout.mjs +26 -22
- package/bin/lib/deployRunner.mjs +54 -18
- package/bin/lib/deployWire.mjs +32 -23
- package/bin/lib/desktopContract.mjs +3 -5
- package/bin/lib/env.mjs +104 -35
- package/bin/lib/fleet.mjs +7 -9
- package/bin/lib/fleetPost.mjs +172 -25
- package/bin/lib/fleetReports.mjs +130 -170
- package/bin/lib/fleetStartup.mjs +6 -6
- package/bin/lib/fleetWire.mjs +42 -0
- package/bin/lib/git.mjs +111 -61
- package/bin/lib/gitMergeIdentity.mjs +2 -8
- package/bin/lib/grant.mjs +23 -7
- package/bin/lib/holder.mjs +108 -81
- package/bin/lib/hooks/readGuard.mjs +41 -7
- package/bin/lib/install.mjs +80 -33
- package/bin/lib/instance.mjs +36 -83
- package/bin/lib/intakeDraft.mjs +4 -1
- package/bin/lib/knowledgeFileSync.mjs +31 -0
- package/bin/lib/knowledgeFiles.mjs +28 -83
- package/bin/lib/landed.mjs +22 -29
- package/bin/lib/launchCommand.mjs +91 -0
- package/bin/lib/listeners.mjs +0 -5
- package/bin/lib/localSessions.mjs +52 -39
- package/bin/lib/login.mjs +22 -12
- package/bin/lib/loginPath.mjs +22 -17
- package/bin/lib/machineListing.mjs +6 -3
- package/bin/lib/mcp-cli.mjs +9 -10
- package/bin/lib/noWindowEnv.mjs +23 -3
- package/bin/lib/ownedDirs.mjs +99 -0
- package/bin/lib/pinnedCapture.mjs +383 -0
- package/bin/lib/prWorkflow.mjs +94 -17
- package/bin/lib/preflight.mjs +8 -25
- package/bin/lib/preview.mjs +19 -25
- package/bin/lib/previewGatePolicy.mjs +34 -9
- package/bin/lib/previewRegistry.mjs +4 -11
- package/bin/lib/printable.mjs +70 -7
- package/bin/lib/procRegistry.mjs +33 -13
- package/bin/lib/procScan.mjs +2 -3
- package/bin/lib/procStat.mjs +76 -0
- package/bin/lib/processGroup.mjs +48 -0
- package/bin/lib/processes.mjs +10 -24
- package/bin/lib/projectToolRuntimes.mjs +101 -16
- package/bin/lib/projectTools.mjs +50 -32
- package/bin/lib/prompts.mjs +41 -19
- package/bin/lib/repoPath.mjs +19 -0
- package/bin/lib/repoState.mjs +17 -19
- package/bin/lib/runTurn.mjs +268 -210
- package/bin/lib/runtimeAntigravity.mjs +16 -5
- package/bin/lib/runtimeCapabilities.mjs +140 -20
- package/bin/lib/runtimeClaude.mjs +55 -5
- package/bin/lib/runtimeCodex.mjs +245 -205
- package/bin/lib/runtimeDetection.mjs +2 -2
- package/bin/lib/runtimeEvents.mjs +16 -10
- package/bin/lib/runtimeLimits.mjs +5 -6
- package/bin/lib/runtimeOfferings.mjs +24 -11
- package/bin/lib/runtimes.mjs +10 -0
- package/bin/lib/shipSweep.mjs +5 -3
- package/bin/lib/shot.mjs +91 -17
- package/bin/lib/sleep.mjs +11 -0
- package/bin/lib/startProject.mjs +5 -5
- package/bin/lib/statePaths.mjs +40 -0
- package/bin/lib/tty.mjs +25 -15
- package/bin/lib/turnLock.mjs +7 -14
- package/bin/lib/turnProfile.mjs +1 -1
- package/bin/lib/ui.mjs +0 -3
- package/bin/lib/uninstall.mjs +30 -26
- package/bin/lib/update.mjs +79 -27
- package/bin/lib/uplinkScrub.mjs +296 -31
- package/bin/lib/vault.mjs +78 -10
- package/bin/lib/vaultDiff.mjs +5 -2
- package/bin/lib/views.mjs +37 -12
- package/bin/lib/wikiRunner.mjs +40 -61
- package/bin/lib/work.mjs +17 -7
- package/bin/lib/workAdoptCarry.mjs +2 -4
- package/bin/lib/workAgentCheck.mjs +104 -57
- package/bin/lib/workAgentMerges.mjs +62 -57
- package/bin/lib/workAgentPlans.mjs +99 -145
- package/bin/lib/workAgentPrecheck.mjs +71 -138
- package/bin/lib/workAgentPublish.mjs +6 -8
- package/bin/lib/workAgentTurnBegun.mjs +15 -11
- package/bin/lib/workAgentTurnExecution.mjs +46 -53
- package/bin/lib/workAgentTurnReports.mjs +9 -37
- package/bin/lib/workAgentTurns.mjs +4 -3
- package/bin/lib/workAttachments.mjs +66 -11
- package/bin/lib/workBrain.mjs +9 -4
- package/bin/lib/workCappedTurn.mjs +109 -0
- package/bin/lib/workChildren.mjs +5 -5
- package/bin/lib/workCommandAudit.mjs +11 -2
- package/bin/lib/workDiffs.mjs +52 -6
- package/bin/lib/workIntake.mjs +35 -60
- package/bin/lib/workPlaceReports.mjs +7 -4
- package/bin/lib/workPlaces.mjs +96 -30
- package/bin/lib/workPreviews.mjs +47 -112
- package/bin/lib/workProcesses.mjs +8 -35
- package/bin/lib/workPullRequests.mjs +37 -56
- package/bin/lib/workReportQueue.mjs +100 -148
- package/bin/lib/workRetire.mjs +2 -1
- package/bin/lib/workSessionRuntime.mjs +38 -37
- package/bin/lib/workSessionTurns.mjs +111 -189
- package/bin/lib/workShip.mjs +43 -23
- package/bin/lib/workTurnShape.mjs +163 -0
- package/npm-shrinkwrap.json +59 -0
- package/package.json +3 -2
- package/bin/lib/fleetJobs.mjs +0 -120
- package/bin/lib/patchRevert.mjs +0 -60
package/bin/lib/vault.mjs
CHANGED
|
@@ -27,16 +27,79 @@ import {
|
|
|
27
27
|
} from 'node:fs';
|
|
28
28
|
import { execFileSync } from 'node:child_process';
|
|
29
29
|
import { join, relative, sep } from 'node:path';
|
|
30
|
+
import { gitRaw } from './git.mjs';
|
|
30
31
|
import { planVaultSync } from './vaultDiff.mjs';
|
|
31
32
|
|
|
32
33
|
const SYNC_STATE = '.flowviant-sync.json';
|
|
33
34
|
|
|
34
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* EVERY GIT CALL IN THE VAULT, AND NONE OF THE VAULT'S OWN PROGRAMS
|
|
37
|
+
* (2026-10-10, the audit). The vault is a directory a cartographer turn writes:
|
|
38
|
+
* Codex's wiki profile can write all of it, `.git` included, and Claude's list
|
|
39
|
+
* denies its `.git` only as far as the CLI keeps its word. A `core.fsmonitor`
|
|
40
|
+
* planted in `<vault>/.git/config` ran on the daemon's own `git add -A` — the
|
|
41
|
+
* daemon, with the daemon's environment, outside every turn's fence — and a
|
|
42
|
+
* planted hook would run on its `git commit`. So the daemon's git here runs
|
|
43
|
+
* with the monitor off and hooks pointed at nothing, and commits `--no-verify`.
|
|
44
|
+
* Command-line `-c` outranks every config file.
|
|
45
|
+
*/
|
|
46
|
+
const VAULT_GIT_FENCE = Object.freeze(['-c', 'core.fsmonitor=false', '-c', 'core.hooksPath=/dev/null']);
|
|
47
|
+
export function vaultGit(args, dir) {
|
|
48
|
+
return execFileSync('git', [...VAULT_GIT_FENCE, ...args], { cwd: dir, stdio: 'ignore' });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* …AND NO PROGRAM THE VAULT'S CONFIG NAMES. `-c` can switch off the monitor
|
|
53
|
+
* and the hooks by name, but not a program the config invents a name for: a
|
|
54
|
+
* `[filter "x"] clean = <cmd>` beside a `.gitattributes` of `* filter=x` ran
|
|
55
|
+
* on the fenced `git add -A` all the same (measured, git 2.55), and so would a
|
|
56
|
+
* `gpg.program` under `commit.gpgSign`. Nothing a vault needs is in its config
|
|
57
|
+
* beyond what `git init` writes, so a config holding anything else was not the
|
|
58
|
+
* daemon's — and the local commit is skipped, in words, rather than run. The
|
|
59
|
+
* sync itself is unaffected: it reads files, never git. A config git cannot
|
|
60
|
+
* read is the same answer.
|
|
61
|
+
*/
|
|
62
|
+
const INIT_CONFIG_KEYS = new Set([
|
|
63
|
+
'core.repositoryformatversion',
|
|
64
|
+
'core.filemode',
|
|
65
|
+
'core.bare',
|
|
66
|
+
'core.logallrefupdates',
|
|
67
|
+
'core.ignorecase',
|
|
68
|
+
'core.precomposeunicode',
|
|
69
|
+
'core.symlinks',
|
|
70
|
+
'extensions.objectformat',
|
|
71
|
+
'extensions.refstorage',
|
|
72
|
+
]);
|
|
73
|
+
export function vaultConfigForeignKey(dir) {
|
|
74
|
+
let listed;
|
|
75
|
+
try {
|
|
76
|
+
// `--local` reads the config THIS repository resolves to — through a
|
|
77
|
+
// planted `commondir` or a `.git` file too. It does NOT follow includes
|
|
78
|
+
// (a file option turns them off), so an `include.path` is caught by its
|
|
79
|
+
// own key (measured, git 2.55). Reading config runs nothing.
|
|
80
|
+
listed = gitRaw([...VAULT_GIT_FENCE, 'config', '--local', '--list', '-z'], dir, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
81
|
+
} catch {
|
|
82
|
+
return '(unreadable)';
|
|
83
|
+
}
|
|
84
|
+
// `-z`: each entry is `key\nvalue\0`, so a value with a newline in it
|
|
85
|
+
// cannot pass for a key of its own.
|
|
86
|
+
for (const entry of listed.split('\0')) {
|
|
87
|
+
if (!entry) continue;
|
|
88
|
+
const key = entry.split('\n')[0].toLowerCase();
|
|
89
|
+
if (!INIT_CONFIG_KEYS.has(key)) return key;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Create the vault dir + its private git history (best-effort). With NO
|
|
95
|
+
* template (2026-10-10): a person's `init.templateDir` can carry a `config`
|
|
96
|
+
* of its own, which `git init` copies in (measured, git 2.55) — and which the
|
|
97
|
+
* check above would then refuse on every sync. A vault needs none of it. */
|
|
35
98
|
export function ensureVault(dir) {
|
|
36
99
|
mkdirSync(dir, { recursive: true });
|
|
37
100
|
if (!existsSync(join(dir, '.git'))) {
|
|
38
101
|
try {
|
|
39
|
-
|
|
102
|
+
vaultGit(['init', '-q', '--template='], dir);
|
|
40
103
|
} catch {
|
|
41
104
|
/* git unavailable — the vault still works, just unversioned */
|
|
42
105
|
}
|
|
@@ -68,14 +131,19 @@ function walkMd(dir, base = dir, out = [], errors = { count: 0 }) {
|
|
|
68
131
|
|
|
69
132
|
/** Best-effort local history commit — identity pinned so it works on machines
|
|
70
133
|
* with no global git config, and never touches the user's identity. */
|
|
71
|
-
function commitVault(dir, message) {
|
|
134
|
+
function commitVault(dir, message, warn) {
|
|
135
|
+
// No `.git` of its own (git was unavailable at init, or a turn removed it):
|
|
136
|
+
// unversioned — and never a `git add -A` that walks UP into whatever
|
|
137
|
+
// repository encloses the vault.
|
|
138
|
+
if (!existsSync(join(dir, '.git'))) return;
|
|
139
|
+
const foreign = vaultConfigForeignKey(dir);
|
|
140
|
+
if (foreign) {
|
|
141
|
+
warn(`wiki vault history not committed — its git config holds \`${foreign}\`, which Flowviant did not write (${dir})`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
72
144
|
try {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
'git',
|
|
76
|
-
['-c', 'user.name=flowviant', '-c', 'user.email=wiki@flowviant.local', 'commit', '-q', '-m', message],
|
|
77
|
-
{ cwd: dir, stdio: 'ignore' }
|
|
78
|
-
);
|
|
145
|
+
vaultGit(['add', '-A'], dir);
|
|
146
|
+
vaultGit(['-c', 'user.name=flowviant', '-c', 'user.email=wiki@flowviant.local', 'commit', '-q', '--no-verify', '-m', message], dir);
|
|
79
147
|
} catch {
|
|
80
148
|
/* nothing to commit / git unavailable — fine */
|
|
81
149
|
}
|
|
@@ -124,6 +192,6 @@ export async function syncVault({ dir, url, token, userAgent, finalize, grounded
|
|
|
124
192
|
}
|
|
125
193
|
|
|
126
194
|
writeFileSync(join(dir, SYNC_STATE), JSON.stringify(plan.state));
|
|
127
|
-
commitVault(dir, plan.finalized ? `sweep${groundedAtSha ? ` @ ${groundedAtSha.slice(0, 7)}` : ''}` : `update${groundedAtSha ? ` @ ${groundedAtSha.slice(0, 7)}` : ''}
|
|
195
|
+
commitVault(dir, plan.finalized ? `sweep${groundedAtSha ? ` @ ${groundedAtSha.slice(0, 7)}` : ''}` : `update${groundedAtSha ? ` @ ${groundedAtSha.slice(0, 7)}` : ''}`, warn);
|
|
128
196
|
return { pages: plan.pages, uploaded: plan.uploaded, deleted: plan.deleted };
|
|
129
197
|
}
|
package/bin/lib/vaultDiff.mjs
CHANGED
|
@@ -21,9 +21,12 @@ import { createHash } from 'node:crypto';
|
|
|
21
21
|
|
|
22
22
|
// Mirror the server contract (shared schema) — a page that violates it is
|
|
23
23
|
// skipped with a warning (and carried forward if previously synced), never
|
|
24
|
-
// allowed to 400 the whole request and wedge the sync.
|
|
24
|
+
// allowed to 400 the whole request and wedge the sync. The release parity gate
|
|
25
|
+
// (scripts/check-app-parity.mjs, 2026-10-10, audit F142) holds these to the
|
|
26
|
+
// shared WIKI_VAULT_* names, CHUNK_FILES within the per-request file cap, and
|
|
27
|
+
// isSafePath to isSafeVaultPath over a vector list.
|
|
25
28
|
export const MAX_FILE_BYTES = 262_144;
|
|
26
|
-
const MAX_PATH_CHARS = 300;
|
|
29
|
+
export const MAX_PATH_CHARS = 300;
|
|
27
30
|
export const MAX_FILES = 400;
|
|
28
31
|
export const CHUNK_FILES = 30;
|
|
29
32
|
export const CHUNK_BYTES = 700_000;
|
package/bin/lib/views.mjs
CHANGED
|
@@ -7,32 +7,37 @@
|
|
|
7
7
|
* status line for a server that did not answer is left out, never guessed.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
11
|
import { closeSync, existsSync, openSync, readSync, statSync } from 'node:fs';
|
|
12
|
-
import {
|
|
12
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
13
|
+
import { APP_URL, FLEET_URL, USER_AGENT, VERSION } from './config.mjs';
|
|
13
14
|
import { detectRepoRoot, listStoredProjects, projectLabel, resolveStoredCredential } from './credentials.mjs';
|
|
14
15
|
import { desktopStatusRemote, installChannel } from './desktopContract.mjs';
|
|
15
16
|
import { daemonLogPath } from './daemonState.mjs';
|
|
17
|
+
import { git as runGit } from './git.mjs';
|
|
16
18
|
import { daemonRunningFor } from './instance.mjs';
|
|
17
19
|
import { agoFrom } from './machineListing.mjs';
|
|
20
|
+
import { printable, printableText } from './printable.mjs';
|
|
18
21
|
import { detectRuntimes } from './runtimeDetection.mjs';
|
|
19
22
|
import { c } from './ui.mjs';
|
|
20
23
|
import { versionBelow } from './versionOrder.mjs';
|
|
21
24
|
|
|
22
|
-
const APP_URL = process.env.FLOWVIANT_APP_URL || 'https://app.flowviant.com';
|
|
23
|
-
|
|
24
25
|
// "Behind" in these views is versionBelow (versionOrder.mjs): an unreadable
|
|
25
26
|
// version is never marked older — a read-only view states nothing it cannot
|
|
26
27
|
// measure.
|
|
27
28
|
|
|
28
29
|
function agentLine(agent, now) {
|
|
29
|
-
|
|
30
|
+
// A member names an agent; this prints it to a terminal. Scrubbed like every
|
|
31
|
+
// other server string this daemon prints (2026-10-10, the audit's F151;
|
|
32
|
+
// see printable.mjs).
|
|
33
|
+
const name = printable(agent.name);
|
|
34
|
+
if (agent.asks) return `${name} · ${c.yellow('asks you a question')}`;
|
|
30
35
|
const since = agent.status === 'working' && !agent.parked && agent.since ? Date.parse(agent.since) : NaN;
|
|
31
36
|
const minutes = Number.isFinite(since) ? Math.max(0, Math.floor((now - since) / 60_000)) : null;
|
|
32
37
|
const took = minutes == null ? '' : ` ${minutes < 60 ? `${minutes}m` : `${Math.floor(minutes / 60)}h ${minutes % 60}m`}`;
|
|
33
38
|
const word = agent.parked ? 'parked' : agent.status;
|
|
34
39
|
const cards = `${agent.delivered} of ${agent.total} ${agent.total === 1 ? 'card' : 'cards'}${agent.delivered ? ' delivered' : ''}`;
|
|
35
|
-
return `${
|
|
40
|
+
return `${name} · ${word === 'working' ? c.blue(`working${took}`) : word} · ${cards}`;
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
/** `flowviant status`, rendered from the same document `status --json --remote` prints. */
|
|
@@ -51,7 +56,7 @@ export function renderStatus(status, { now = Date.now() } = {}) {
|
|
|
51
56
|
lines.push(label('here', `not running — run ${c.cyan('flowviant')} in that repo`));
|
|
52
57
|
} else if (project.running === true) {
|
|
53
58
|
const serves = role === 'serving' ? c.green('serving')
|
|
54
|
-
: role === 'inactive' ? `inactive${remote?.servingBoxName ? ` — ${remote.servingBoxName} serves this project` : ''}`
|
|
59
|
+
: role === 'inactive' ? `inactive${remote?.servingBoxName ? ` — ${printable(remote.servingBoxName)} serves this project` : ''}`
|
|
55
60
|
: project.holder === 'serving' ? c.green('serving') : null;
|
|
56
61
|
lines.push(label('here', serves ? `running · ${serves}` : 'running'));
|
|
57
62
|
}
|
|
@@ -61,7 +66,9 @@ export function renderStatus(status, { now = Date.now() } = {}) {
|
|
|
61
66
|
if (agents && agents.length > 0) {
|
|
62
67
|
agents.forEach((agent, i) => lines.push(label(i === 0 ? 'agents' : '', agentLine(agent, now))));
|
|
63
68
|
}
|
|
64
|
-
|
|
69
|
+
// The server scrubs these two (machineBoxes.ts, machinePressure.ts); the
|
|
70
|
+
// CLI keeps its belt, as printable.mjs explains.
|
|
71
|
+
if (remote?.pressure?.reason) lines.push(label('waiting', `Waiting for the machine — ${printable(remote.pressure.reason)}.`));
|
|
65
72
|
for (const rt of project.runtimes ?? []) {
|
|
66
73
|
if (rt.parkedByLimit && rt.message) lines.push(label('limit', `${rt.id} hit its limit: “${rt.message}”`));
|
|
67
74
|
}
|
|
@@ -94,19 +101,36 @@ export function resolveProjectForView(argv = process.argv, cwd = process.cwd())
|
|
|
94
101
|
};
|
|
95
102
|
}
|
|
96
103
|
|
|
97
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* The last `count` lines of a file, read from its end — PRINTABLE (2026-10-10,
|
|
106
|
+
* the audit's F133). A daemon before this release logged member-written card
|
|
107
|
+
* titles raw, so a replay could carry an OSC 52 into whichever terminal runs
|
|
108
|
+
* `flowviant logs`. `printableText` keeps the lines' shape; a log written on a
|
|
109
|
+
* TTY loses its colours with everything else.
|
|
110
|
+
*/
|
|
98
111
|
export function tailLines(path, count) {
|
|
99
112
|
const size = statSync(path).size;
|
|
100
113
|
const want = Math.min(size, 256 * 1024);
|
|
101
114
|
const buf = Buffer.alloc(want);
|
|
102
115
|
const fd = openSync(path, 'r');
|
|
103
116
|
try { readSync(fd, buf, 0, want, size - want); } finally { closeSync(fd); }
|
|
104
|
-
const lines = buf.toString('utf8').split('\n');
|
|
117
|
+
const lines = printableText(buf.toString('utf8')).split('\n');
|
|
105
118
|
if (lines.at(-1) === '') lines.pop();
|
|
106
119
|
if (want < size) lines.shift(); // the first line is cut mid-way
|
|
107
120
|
return { text: lines.slice(-count).join('\n'), size };
|
|
108
121
|
}
|
|
109
122
|
|
|
123
|
+
/**
|
|
124
|
+
* What `logs -f` prints of each new chunk: decoded as a STREAM, because a read
|
|
125
|
+
* can end inside a multi-byte character, and scrubbed like the tail. A sequence
|
|
126
|
+
* split across two reads loses its ESC in the first and its terminator in the
|
|
127
|
+
* second, so what is left of it is plain text.
|
|
128
|
+
*/
|
|
129
|
+
export function createLogFollower() {
|
|
130
|
+
const decoder = new StringDecoder('utf8');
|
|
131
|
+
return (buf) => printableText(decoder.write(buf));
|
|
132
|
+
}
|
|
133
|
+
|
|
110
134
|
export async function runLogs(argv = process.argv) {
|
|
111
135
|
const which = resolveProjectForView(argv);
|
|
112
136
|
if (!which.entry) { console.error(which.message); process.exit(1); }
|
|
@@ -125,6 +149,7 @@ export async function runLogs(argv = process.argv) {
|
|
|
125
149
|
// Poll rather than fs.watch: the daemon rotates the file (a rename), and a
|
|
126
150
|
// watch on the old inode would go quiet exactly then.
|
|
127
151
|
let offset = existsSync(path) ? statSync(path).size : 0;
|
|
152
|
+
const followed = createLogFollower();
|
|
128
153
|
for (;;) {
|
|
129
154
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
130
155
|
if (!existsSync(path)) continue;
|
|
@@ -134,7 +159,7 @@ export async function runLogs(argv = process.argv) {
|
|
|
134
159
|
const buf = Buffer.alloc(size - offset);
|
|
135
160
|
const fd = openSync(path, 'r');
|
|
136
161
|
try { readSync(fd, buf, 0, buf.length, offset); } finally { closeSync(fd); }
|
|
137
|
-
process.stdout.write(buf);
|
|
162
|
+
process.stdout.write(followed(buf));
|
|
138
163
|
offset = size;
|
|
139
164
|
}
|
|
140
165
|
}
|
|
@@ -173,7 +198,7 @@ export async function runDoctor({ cwd = process.cwd(), fetchImpl = fetch } = {})
|
|
|
173
198
|
const add = (state, text, fix) => { if (state === false) failed++; lines.push(check(state, text, fix)); };
|
|
174
199
|
|
|
175
200
|
let git = null;
|
|
176
|
-
try { git =
|
|
201
|
+
try { git = runGit(['--version'], undefined, { timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] }); } catch { /* absent */ }
|
|
177
202
|
add(Boolean(git), git ?? 'git is not installed', 'Install git; agents work in git worktrees.');
|
|
178
203
|
|
|
179
204
|
const installed = detectRuntimes().filter((rt) => rt.installed);
|
package/bin/lib/wikiRunner.mjs
CHANGED
|
@@ -21,24 +21,23 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { existsSync } from 'node:fs';
|
|
24
|
-
import { execFileSync } from 'node:child_process';
|
|
25
24
|
import { homedir } from 'node:os';
|
|
26
25
|
import { join } from 'node:path';
|
|
27
26
|
import { FLEET_URL, FLEET_TOKEN, USER_AGENT } from './config.mjs';
|
|
28
|
-
import { git, resetWorktree, originSlug, isSafePathSegment } from './git.mjs';
|
|
27
|
+
import { git, gitOrNull, gitRaw, resetWorktree, originSlug, isSafePathSegment, SHA1_ABBREV_RE } from './git.mjs';
|
|
29
28
|
import { c, note, ok, warn } from './ui.mjs';
|
|
30
29
|
import { sawSentinel } from './claude.mjs';
|
|
31
30
|
import { runTurn as defaultRunTurn } from './runTurn.mjs';
|
|
32
31
|
import { SYSTEM_WIKI, WIKI_KICKOFF, SYSTEM_REGROUND, REGROUND_KICKOFF } from './prompts.mjs';
|
|
33
32
|
import { ensureVault, syncVault } from './vault.mjs';
|
|
34
33
|
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
35
|
-
import {
|
|
34
|
+
import { runtimeLabel } from './runtimes.mjs';
|
|
36
35
|
import { pickRuntimeFor as defaultPickRuntimeFor } from './runtimeDetection.mjs';
|
|
37
|
-
import { recordMcpServers, recordSkills } from './runtimeCapabilities.mjs';
|
|
38
36
|
import { THINK_MARKER } from './runtimeEvents.mjs';
|
|
39
37
|
import { fleetEndpoint } from './fleetWire.mjs';
|
|
40
|
-
import { postToFleet } from './fleetPost.mjs';
|
|
38
|
+
import { fleetHeaders, postToFleet } from './fleetPost.mjs';
|
|
41
39
|
import { addUsage } from './turnUsage.mjs';
|
|
40
|
+
import { printable } from './printable.mjs';
|
|
42
41
|
|
|
43
42
|
export function createWikiRunner({
|
|
44
43
|
repoRoot,
|
|
@@ -126,11 +125,7 @@ export function createWikiRunner({
|
|
|
126
125
|
try {
|
|
127
126
|
await fetch(WIKI_PROGRESS_URL, {
|
|
128
127
|
method: 'POST',
|
|
129
|
-
headers:
|
|
130
|
-
Authorization: `Bearer ${FLEET_TOKEN}`,
|
|
131
|
-
'User-Agent': USER_AGENT,
|
|
132
|
-
'Content-Type': 'application/json',
|
|
133
|
-
},
|
|
128
|
+
headers: fleetHeaders(),
|
|
134
129
|
signal: AbortSignal.timeout(15_000),
|
|
135
130
|
body: JSON.stringify(safe),
|
|
136
131
|
});
|
|
@@ -148,22 +143,10 @@ export function createWikiRunner({
|
|
|
148
143
|
* sweep queued, for the rest of the day. Best-effort: an older server 404s
|
|
149
144
|
* once and the TTL is still the backstop it always was.
|
|
150
145
|
*/
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
headers: {
|
|
156
|
-
Authorization: `Bearer ${FLEET_TOKEN}`,
|
|
157
|
-
'User-Agent': USER_AGENT,
|
|
158
|
-
'Content-Type': 'application/json',
|
|
159
|
-
},
|
|
160
|
-
signal: AbortSignal.timeout(15_000),
|
|
161
|
-
body: '{}',
|
|
162
|
-
});
|
|
163
|
-
} catch {
|
|
164
|
-
/* best-effort — the request's own TTL still expires it */
|
|
165
|
-
}
|
|
166
|
-
};
|
|
146
|
+
// No longer a near copy of `postToFleet` (decisions.md's 2026-09-26 open
|
|
147
|
+
// item, closed audit 2026-10-10 F136): the same post, on its own 15 s
|
|
148
|
+
// ceiling. Never throws — the request's own TTL still expires it.
|
|
149
|
+
const postWikiAbandoned = () => postToFleet(WIKI_ABANDONED_URL, {}, { timeoutMs: 15_000 });
|
|
167
150
|
|
|
168
151
|
const enqueueSweep = (job) => {
|
|
169
152
|
if (!job || job.requestedAt === lastSweepAt) return;
|
|
@@ -187,7 +170,19 @@ export function createWikiRunner({
|
|
|
187
170
|
type: 'reground',
|
|
188
171
|
intentId,
|
|
189
172
|
prUrl,
|
|
190
|
-
|
|
173
|
+
// A CARD'S TITLE, which any member writes (and intake can seed from a
|
|
174
|
+
// stranger's ticket), printed by the drain's narration six times and put
|
|
175
|
+
// in the prompt. SANITIZED AT THE INTAKE like the pages below (2026-10-10,
|
|
176
|
+
// the audit's F097): an OSC 52 in a title rewrote the operator's
|
|
177
|
+
// clipboard the moment the card shipped. printable() takes the whole
|
|
178
|
+
// display-control class, newlines included, so a title is one line.
|
|
179
|
+
title:
|
|
180
|
+
(typeof title === 'string'
|
|
181
|
+
? printable(title)
|
|
182
|
+
.replace(/\s+/g, ' ')
|
|
183
|
+
.trim()
|
|
184
|
+
.slice(0, 300)
|
|
185
|
+
: '') || 'a delivered task',
|
|
191
186
|
// What the PLAN thought this would invalidate. A hint, not the truth —
|
|
192
187
|
// the turn still reads the real changed files; this catches pages whose
|
|
193
188
|
// frontmatter file list has drifted, or that document a concept rather
|
|
@@ -239,13 +234,11 @@ export function createWikiRunner({
|
|
|
239
234
|
if (!Array.isArray(shas) || shas.length === 0) return null;
|
|
240
235
|
const files = new Set();
|
|
241
236
|
for (const sha of shas.slice(0, 50)) {
|
|
242
|
-
|
|
237
|
+
// git.mjs's SHA-1 shape, either case — the shape this lookup has
|
|
238
|
+
// always taken (audit 2026-10-10, F749).
|
|
239
|
+
if (!SHA1_ABBREV_RE.test(String(sha).toLowerCase())) continue;
|
|
243
240
|
try {
|
|
244
|
-
const out =
|
|
245
|
-
'git',
|
|
246
|
-
['show', '--name-only', '--pretty=format:', String(sha)],
|
|
247
|
-
{ cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }
|
|
248
|
-
);
|
|
241
|
+
const out = gitRaw(['show', '--name-only', '--pretty=format:', String(sha)], repoRoot);
|
|
249
242
|
for (const line of out.split('\n')) {
|
|
250
243
|
const f = line.trim();
|
|
251
244
|
if (f) files.add(f);
|
|
@@ -399,12 +392,8 @@ export function createWikiRunner({
|
|
|
399
392
|
}
|
|
400
393
|
}
|
|
401
394
|
resetWorktree(wikiWt, getBaseRef());
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
sha = git(['rev-parse', 'HEAD'], wikiWt);
|
|
405
|
-
} catch {
|
|
406
|
-
/* detached/no HEAD — still writes the map, just ungrounded */
|
|
407
|
-
}
|
|
395
|
+
// No HEAD → '' — still writes the map, just ungrounded.
|
|
396
|
+
const sha = gitOrNull(['rev-parse', 'HEAD'], wikiWt) ?? '';
|
|
408
397
|
// Sync the vault after the turn regardless of the sentinel: a died
|
|
409
398
|
// sweep's partial pages still persist (merge, no prune) — only a
|
|
410
399
|
// COMPLETED sweep finalizes, so an interrupted one can't erase pages.
|
|
@@ -441,7 +430,7 @@ export function createWikiRunner({
|
|
|
441
430
|
warn('wiki generation skipped — no installed CLI can run a vault-scoped turn');
|
|
442
431
|
return;
|
|
443
432
|
}
|
|
444
|
-
const wikiLabel =
|
|
433
|
+
const wikiLabel = runtimeLabel(wikiRt);
|
|
445
434
|
if (task.type === 'sweep') {
|
|
446
435
|
sweepAttempts++;
|
|
447
436
|
note(`${c.cyan('wiki')} ${c.dim(`— regenerating: your ${wikiLabel} is reading the repo…`)}`);
|
|
@@ -454,19 +443,11 @@ export function createWikiRunner({
|
|
|
454
443
|
vaultDir,
|
|
455
444
|
runtime: wikiRt,
|
|
456
445
|
label: c.cyan('[wiki]'),
|
|
457
|
-
|
|
446
|
+
// No skills or connectors are harvested off a wiki turn: it is
|
|
447
|
+
// fenced (no settings source, strict MCP), so its init names less
|
|
448
|
+
// than the machine has (runTurn.mjs, `initHarvestFor`).
|
|
458
449
|
onActivity,
|
|
459
450
|
onUsage: onUsage(wikiRt),
|
|
460
|
-
// FREE SKILLS, off a turn that was running anyway. This stream is
|
|
461
|
-
// already parsed and its init event already carries the CLI's own
|
|
462
|
-
// resolved skill set — the same fact a tab turn teaches — so the
|
|
463
|
-
// only thing missing was the handler. The wiki turn runs in a
|
|
464
|
-
// detached worktree of THIS repo, so its `.claude/skills` and the
|
|
465
|
-
// machine's personal ones resolve identically to a tab's.
|
|
466
|
-
onInit: (i) => {
|
|
467
|
-
recordSkills(i.skills);
|
|
468
|
-
recordMcpServers(i.mcpServers);
|
|
469
|
-
},
|
|
470
451
|
onSpawn: (ch) => {
|
|
471
452
|
wikiChild = ch;
|
|
472
453
|
},
|
|
@@ -512,14 +493,8 @@ export function createWikiRunner({
|
|
|
512
493
|
vaultDir,
|
|
513
494
|
runtime: wikiRt,
|
|
514
495
|
label: c.cyan('[wiki]'),
|
|
515
|
-
streamJson: true,
|
|
516
496
|
onActivity,
|
|
517
497
|
onUsage: onUsage(wikiRt),
|
|
518
|
-
// Same free harvest as the sweep above.
|
|
519
|
-
onInit: (i) => {
|
|
520
|
-
recordSkills(i.skills);
|
|
521
|
-
recordMcpServers(i.mcpServers);
|
|
522
|
-
},
|
|
523
498
|
onSpawn: (ch) => {
|
|
524
499
|
wikiChild = ch;
|
|
525
500
|
},
|
|
@@ -579,10 +554,14 @@ export function createWikiRunner({
|
|
|
579
554
|
// Terminal frame so the app cover clears promptly (don't wait for the
|
|
580
555
|
// freshness window to lapse). force-sent past the throttle.
|
|
581
556
|
await postWikiProgress(frame({ done: true, ...(usage ? { usage } : {}) }), true);
|
|
582
|
-
// Safety net: the wiki turn is read-only on the repo by CONTRACT
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
//
|
|
557
|
+
// Safety net: the wiki turn is read-only on the repo by CONTRACT.
|
|
558
|
+
// Claude's list writes only the vault (claudePosture.mjs
|
|
559
|
+
// `wikiPermFor`, 2026-10-10) and Codex's kernel profile likewise,
|
|
560
|
+
// but Antigravity's flags cannot say "only the vault" — so discard
|
|
561
|
+
// anything a turn wrote to the worktree before it can leak into a
|
|
562
|
+
// later turn or a push. It reaches the worktree ONLY: a write
|
|
563
|
+
// anywhere else is the posture's to refuse, never this reset's to
|
|
564
|
+
// undo.
|
|
586
565
|
try {
|
|
587
566
|
resetWorktree(wikiWt, getBaseRef());
|
|
588
567
|
} catch {
|
package/bin/lib/work.mjs
CHANGED
|
@@ -40,11 +40,16 @@
|
|
|
40
40
|
* workSessionRuntime.mjs which CLI a tab speaks, which conversation resumes
|
|
41
41
|
* workAdoptCarry.mjs adoption's dirty carry
|
|
42
42
|
* workSessionTurns.mjs a tab's turn, start to settle
|
|
43
|
+
* workTurnShape.mjs what that turn is — plain, plan, capture or build —
|
|
44
|
+
* and its prompt pair (pure, 2026-10-10)
|
|
43
45
|
* workRetire.mjs held sessions, retirement, manual-place freshening
|
|
44
46
|
* workToolLog.mjs one turn's tool cards, folded and capped (pure)
|
|
45
47
|
* workCommandAudit.mjs one turn's `$ …` audit, batched to the server
|
|
46
48
|
* workBrain.mjs the model and effort a turn may put on argv (pure)
|
|
47
49
|
* workIntake.mjs one incoming ticket drafted into a card, read-only (2026-09-28)
|
|
50
|
+
* workCappedTurn.mjs one capped read-only consult turn: its child, its slot,
|
|
51
|
+
* its cap and its transcript — the planner, the pre-review
|
|
52
|
+
* and intake all run one (2026-10-10)
|
|
48
53
|
* turnLock.mjs whether a live CLI of ours holds a directory's turn
|
|
49
54
|
* (and, split earlier: workDiffs, workPreviews, workPullRequests, workShip,
|
|
50
55
|
* workAgentPlans, workAgentCheck, workAgentPrecheck, workAgentReview,
|
|
@@ -71,6 +76,7 @@ import { createWorkChildren } from './workChildren.mjs';
|
|
|
71
76
|
import { createWorkAgentPublish } from './workAgentPublish.mjs';
|
|
72
77
|
import { createWorkPlaceReports } from './workPlaceReports.mjs';
|
|
73
78
|
import { createWorkReportQueue } from './workReportQueue.mjs';
|
|
79
|
+
import { postBestEffort } from './fleetPost.mjs';
|
|
74
80
|
import { createWorkSessionTokens } from './workSessionTokens.mjs';
|
|
75
81
|
import { createWorkNarration } from './workNarration.mjs';
|
|
76
82
|
import { createWorkAttachments } from './workAttachments.mjs';
|
|
@@ -186,7 +192,7 @@ export function createWorkManager({
|
|
|
186
192
|
const gitNet = (args, ms) => gitNetIn(args, repoRoot, ms);
|
|
187
193
|
|
|
188
194
|
// ── THE SESSION'S OWN HALF — built first, because the lanes below are handed it.
|
|
189
|
-
const {
|
|
195
|
+
const { placeOf, notePlace, learnPlaces, placeDir, branchAt, placeWtFor, sessionMetaPath } =
|
|
190
196
|
createWorkPlaces({ repoRoot, baseDir, baseRef, onRepoChanged });
|
|
191
197
|
const { sessionGroups, noteSessionGroup, pruneSessionGroups, sessionProcesses } =
|
|
192
198
|
createWorkSessionGroups({ repoRoot });
|
|
@@ -211,7 +217,6 @@ export function createWorkManager({
|
|
|
211
217
|
const {
|
|
212
218
|
pendingWorkReports,
|
|
213
219
|
pendingShipReports,
|
|
214
|
-
postBestEffort,
|
|
215
220
|
REJECT_RETRY_MS,
|
|
216
221
|
settleWorkTurn,
|
|
217
222
|
settleShip,
|
|
@@ -241,7 +246,7 @@ export function createWorkManager({
|
|
|
241
246
|
inPlace,
|
|
242
247
|
workChildren,
|
|
243
248
|
workAttempts,
|
|
244
|
-
|
|
249
|
+
notePlace,
|
|
245
250
|
placeOf,
|
|
246
251
|
placeWtFor,
|
|
247
252
|
sessionMetaPath,
|
|
@@ -282,8 +287,9 @@ export function createWorkManager({
|
|
|
282
287
|
|
|
283
288
|
const { processPrJobs } = createWorkPullRequests({
|
|
284
289
|
placeOf,
|
|
290
|
+
placeDir,
|
|
291
|
+
branchAt,
|
|
285
292
|
repoRoot,
|
|
286
|
-
baseDir,
|
|
287
293
|
baseRef,
|
|
288
294
|
gitNet,
|
|
289
295
|
landed,
|
|
@@ -298,6 +304,8 @@ export function createWorkManager({
|
|
|
298
304
|
baseDir,
|
|
299
305
|
gitNet,
|
|
300
306
|
baseRef,
|
|
307
|
+
placeDir,
|
|
308
|
+
branchAt,
|
|
301
309
|
placeWtFor,
|
|
302
310
|
sessionMetaPath,
|
|
303
311
|
shipping,
|
|
@@ -457,9 +465,11 @@ export function createWorkManager({
|
|
|
457
465
|
processAgentPlanJobs,
|
|
458
466
|
processIntakeJobs,
|
|
459
467
|
processAgentTurnJobs,
|
|
460
|
-
// Only the
|
|
461
|
-
//
|
|
462
|
-
//
|
|
468
|
+
// Only the roster's stand-down calls this — a displacement, a removal or a
|
|
469
|
+
// commanded stop (holder.mjs, `standDownDisplaced`); see its comment.
|
|
470
|
+
// Exported rather than hooked into `shutdownWork` because the signal
|
|
471
|
+
// handlers cannot await, and a settle that is not awaited is a settle that
|
|
472
|
+
// did not happen.
|
|
463
473
|
settleAgentTurns,
|
|
464
474
|
processAgentMergeJobs,
|
|
465
475
|
freshenManualPlaces,
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* promise of its own (the source is never written, a problem becomes one line
|
|
7
7
|
* for the agent to say), reached once, from the session lane's adopt path.
|
|
8
8
|
*/
|
|
9
|
-
import { execFileSync } from 'node:child_process';
|
|
10
9
|
import { cpSync, lstatSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
11
10
|
import { dirname, join } from 'node:path';
|
|
12
11
|
import { git, gitRaw, splitNul } from './git.mjs';
|
|
@@ -31,9 +30,8 @@ export function createAdoptCarry({ sessionMetaPath }) {
|
|
|
31
30
|
// text file) must round-trip byte-exact or the apply corrupts what it
|
|
32
31
|
// carries. 64MB of headroom — a dirtier tree than that fails the read
|
|
33
32
|
// here and is SAID, below, rather than half-applied.
|
|
34
|
-
const patch =
|
|
35
|
-
|
|
36
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
33
|
+
const patch = gitRaw(['diff', 'HEAD', '--binary'], srcCwd, {
|
|
34
|
+
encoding: 'buffer',
|
|
37
35
|
maxBuffer: 64 * 1024 * 1024,
|
|
38
36
|
});
|
|
39
37
|
if (patch.length) {
|