cawdev-cli 0.9.0 → 1.0.0-beta
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 +4 -171
- package/lib/run-plugin.mjs +40 -0
- package/lib/usage-report.mjs +27 -0
- package/lib/version.mjs +48 -0
- package/package.json +1 -1
- package/runner/README.md +56 -0
- package/runner/attach.mjs +442 -1
- package/runner/banner.mjs +11 -1
- package/runner/bootstrap.mjs +70 -35
- package/runner/cawdev.mjs +215 -19
- package/runner/configure.mjs +412 -0
- package/runner/paths.mjs +254 -0
- package/runner/runner.mjs +829 -131
- package/runner/token-store.mjs +14 -2
package/runner/runner.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { spawn } from 'node:child_process';
|
|
|
15
15
|
import {
|
|
16
16
|
access, copyFile, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile,
|
|
17
17
|
} from 'node:fs/promises';
|
|
18
|
-
import { tmpdir } from 'node:os';
|
|
18
|
+
import { homedir, tmpdir } from 'node:os';
|
|
19
19
|
|
|
20
20
|
import { join, resolve } from 'node:path';
|
|
21
21
|
import { describeTurn, totalsOf } from '../lib/usage.mjs';
|
|
@@ -27,7 +27,9 @@ import { codeMapOf } from '../lib/code-map.mjs';
|
|
|
27
27
|
import { usageLimitOf } from '../lib/usage-limit.mjs';
|
|
28
28
|
import { TranscriptBatch } from '../lib/transcript-batch.mjs';
|
|
29
29
|
import { parseUsage } from '../lib/usage-report.mjs';
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
ANTIGRAVITY_EXPERTS_PLUGIN, qualified, writeAntigravityExpertsPlugin, writeRunPlugin,
|
|
32
|
+
} from '../lib/run-plugin.mjs';
|
|
31
33
|
import { AI_CONFIG, harnessPrompt, readRepoConfig } from '../lib/harness-prompt.mjs';
|
|
32
34
|
import { loadToken, storedUrls } from './token-store.mjs';
|
|
33
35
|
import {
|
|
@@ -35,6 +37,7 @@ import {
|
|
|
35
37
|
} from '../lib/stage-tools.mjs';
|
|
36
38
|
import { capabilityIn, describeCall } from '../lib/tool-line.mjs';
|
|
37
39
|
import { findSecret } from '../lib/secrets.mjs';
|
|
40
|
+
import { cliVersion, isBelow } from '../lib/version.mjs';
|
|
38
41
|
|
|
39
42
|
// --- configuration -----------------------------------------------------------
|
|
40
43
|
|
|
@@ -72,7 +75,7 @@ const DEFAULTS = {
|
|
|
72
75
|
* cure. A default that is simply the thing this daemon exists to run costs
|
|
73
76
|
* nothing and cannot be missing.
|
|
74
77
|
*/
|
|
75
|
-
|
|
78
|
+
agentCommands: ['claude'],
|
|
76
79
|
/**
|
|
77
80
|
* Verified against Claude Code 2.1.247.
|
|
78
81
|
*
|
|
@@ -337,15 +340,22 @@ const DEFAULTS = {
|
|
|
337
340
|
usageSeconds: 600,
|
|
338
341
|
};
|
|
339
342
|
|
|
340
|
-
|
|
343
|
+
/** The `--config` path this process was started with, or the default. */
|
|
344
|
+
function configPathFromArgv() {
|
|
341
345
|
const index = process.argv.indexOf('--config');
|
|
342
|
-
|
|
346
|
+
return index === -1 ? 'runner.config.json' : process.argv[index + 1];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function readConfig() {
|
|
350
|
+
const path = configPathFromArgv();
|
|
343
351
|
|
|
344
352
|
let file = {};
|
|
353
|
+
let fromFile = false;
|
|
345
354
|
try {
|
|
346
355
|
file = JSON.parse(await readFile(path, 'utf8'));
|
|
356
|
+
fromFile = true;
|
|
347
357
|
} catch (failure) {
|
|
348
|
-
if (
|
|
358
|
+
if (process.argv.includes('--config')) {
|
|
349
359
|
throw new Error(`Could not read ${path}: ${failure.message}`);
|
|
350
360
|
}
|
|
351
361
|
// No config file is fine when everything comes from the environment.
|
|
@@ -369,7 +379,7 @@ async function readConfig() {
|
|
|
369
379
|
*/
|
|
370
380
|
token: process.env.CAWDEV_TOKEN ?? file.token ?? (await loadToken(url)),
|
|
371
381
|
name: process.env.CAWDEV_RUNNER_NAME ?? file.name ?? DEFAULTS.name,
|
|
372
|
-
agentCommand: process.env.CAWDEV_AGENT_COMMAND
|
|
382
|
+
agentCommands: file.agentCommands ?? (file.agentCommand ? [file.agentCommand] : null) ?? (process.env.CAWDEV_AGENT_COMMAND ? process.env.CAWDEV_AGENT_COMMAND.split(',').map(s => s.trim()) : null) ?? DEFAULTS.agentCommands,
|
|
373
383
|
// Which projects this runner serves, and where their working copies are.
|
|
374
384
|
projects: normaliseProjects(file.projects ?? {}),
|
|
375
385
|
/**
|
|
@@ -391,6 +401,14 @@ async function readConfig() {
|
|
|
391
401
|
idleSeconds: file.idleSeconds ?? DEFAULTS.idleSeconds,
|
|
392
402
|
usageSeconds: file.usageSeconds ?? DEFAULTS.usageSeconds,
|
|
393
403
|
sessionExitSeconds: file.sessionExitSeconds ?? DEFAULTS.sessionExitSeconds,
|
|
404
|
+
/**
|
|
405
|
+
* The FILE this daemon booted from, absolute, or null when everything
|
|
406
|
+
* came from the environment — R288. Published on the control socket so
|
|
407
|
+
* the attached terminal's `c` key edits the config this process is
|
|
408
|
+
* actually running on and not one it guessed at. Absolute because the
|
|
409
|
+
* daemon's cwd is not the terminal's: `cawdev` starts it detached.
|
|
410
|
+
*/
|
|
411
|
+
configPath: fromFile ? resolve(path) : null,
|
|
394
412
|
};
|
|
395
413
|
|
|
396
414
|
if (!config.token) {
|
|
@@ -783,50 +801,59 @@ async function readUsage(config) {
|
|
|
783
801
|
if (!config.usageSeconds || !config.runnerId) {
|
|
784
802
|
return;
|
|
785
803
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
804
|
+
|
|
805
|
+
const allWindows = [];
|
|
806
|
+
|
|
807
|
+
for (const agentCommand of config.agentCommands) {
|
|
808
|
+
const said = await new Promise((resolve) => {
|
|
809
|
+
let out = '';
|
|
810
|
+
const child = spawn(agentCommand, ['-p', '/usage', '--output-format', 'text'], {
|
|
811
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
812
|
+
});
|
|
813
|
+
// Bounded, because this runs on a timer for the life of the daemon: a CLI
|
|
814
|
+
// that hangs here must not accumulate a process per tick.
|
|
815
|
+
const giveUp = setTimeout(() => {
|
|
816
|
+
try {
|
|
817
|
+
child.kill('SIGTERM');
|
|
818
|
+
} catch { /* already gone */ }
|
|
819
|
+
resolve('');
|
|
820
|
+
}, 60_000);
|
|
821
|
+
giveUp.unref?.();
|
|
822
|
+
child.stdout.on('data', (chunk) => (out += chunk));
|
|
823
|
+
child.stderr.on('data', (chunk) => (out += chunk));
|
|
824
|
+
child.on('error', () => {
|
|
825
|
+
clearTimeout(giveUp);
|
|
826
|
+
resolve('');
|
|
827
|
+
});
|
|
828
|
+
child.on('close', () => {
|
|
829
|
+
clearTimeout(giveUp);
|
|
830
|
+
resolve(out);
|
|
831
|
+
});
|
|
809
832
|
});
|
|
810
|
-
});
|
|
811
833
|
|
|
812
|
-
|
|
813
|
-
|
|
834
|
+
const windows = parseUsage(said);
|
|
835
|
+
for (const each of windows) {
|
|
836
|
+
allWindows.push({
|
|
837
|
+
provider: agentCommand.endsWith('agy') ? 'agy' : 'claude',
|
|
838
|
+
window: each.kind,
|
|
839
|
+
model: each.model,
|
|
840
|
+
percentUsed: each.percent,
|
|
841
|
+
// The counts stay null. The CLI gives a percentage and no totals, and
|
|
842
|
+
// inventing `used: 25, limit: 100` would put units on a page that the
|
|
843
|
+
// provider never stated — R20's rule, one level down.
|
|
844
|
+
used: null,
|
|
845
|
+
limit: null,
|
|
846
|
+
resetsAt: each.resetsAt ? each.resetsAt.toISOString() : null,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (!allWindows.length) {
|
|
814
852
|
return;
|
|
815
853
|
}
|
|
816
854
|
await api(config, `/api/runners/${config.runnerId}/limits`, {
|
|
817
855
|
method: 'POST',
|
|
818
|
-
body:
|
|
819
|
-
provider: 'claude',
|
|
820
|
-
window: each.kind,
|
|
821
|
-
model: each.model,
|
|
822
|
-
percentUsed: each.percent,
|
|
823
|
-
// The counts stay null. The CLI gives a percentage and no totals, and
|
|
824
|
-
// inventing `used: 25, limit: 100` would put units on a page that the
|
|
825
|
-
// provider never stated — R20's rule, one level down.
|
|
826
|
-
used: null,
|
|
827
|
-
limit: null,
|
|
828
|
-
resetsAt: each.resetsAt ? each.resetsAt.toISOString() : null,
|
|
829
|
-
})),
|
|
856
|
+
body: allWindows,
|
|
830
857
|
}).catch((failure) => log(` could not report usage: ${failure.message}`));
|
|
831
858
|
}
|
|
832
859
|
|
|
@@ -5888,6 +5915,40 @@ async function spawnAgent(config, run, runToken, cwd, baseCommit, workspace, res
|
|
|
5888
5915
|
// narrowed by what the laptop already allows, and this is the laptop being
|
|
5889
5916
|
// told — by its own operator, through a page it opted in to — to allow more.
|
|
5890
5917
|
const granted = await machineRules(config);
|
|
5918
|
+
|
|
5919
|
+
// R147. Which of the run's experts THIS process may reach — moved ahead of
|
|
5920
|
+
// the agy branch below (R279) so BOTH spawn paths narrow the same way: a
|
|
5921
|
+
// read-only stage is handed only the experts that can change nothing.
|
|
5922
|
+
// `readOnlyExpert` judges by the frontmatter the plugin is written from, so
|
|
5923
|
+
// the plugin loaded, the delegation tool allowed, and the list the session
|
|
5924
|
+
// is told about all agree. Everything else, and every process with no
|
|
5925
|
+
// lifecycle, gets them all.
|
|
5926
|
+
const readOnlyStage = Boolean(stage) && ['PLAN', 'VERIFY', 'MEMORY'].includes(stage.stage);
|
|
5927
|
+
const stageExperts = readOnlyStage ? expertAgents.filter(readOnlyExpert) : expertAgents;
|
|
5928
|
+
// R161. From `stageExperts` and NEVER from `expertAgents` — see the note
|
|
5929
|
+
// above; an expert a read-only stage cannot reach must not be required here.
|
|
5930
|
+
const cannotDelegate = readOnlyStage && expertAgents.length && !stageExperts.length
|
|
5931
|
+
? [`The ${stage.stage} stage holds nothing that can change anything, and none of this `
|
|
5932
|
+
+ "project's experts is read-only, so none can be reached from it. They are reached "
|
|
5933
|
+
+ 'from IMPLEMENT and TEST.']
|
|
5934
|
+
: undefined;
|
|
5935
|
+
|
|
5936
|
+
// Which CLI answers this run — R276's follow-on made real. A staged run
|
|
5937
|
+
// carries the resolved choice on its stage (`stage.agent`, from
|
|
5938
|
+
// `WorkflowService.forRun`); anything else carries it on the run itself
|
|
5939
|
+
// now (`run.agent`, from `RunService.agentFor`) — both fall back to Claude
|
|
5940
|
+
// Code, exactly as every run did before either existed. Antigravity's
|
|
5941
|
+
// shape differs enough (no --mcp-config, no --allowedTools, no --resume —
|
|
5942
|
+
// checked against the real CLI by hand) that it is not this function's
|
|
5943
|
+
// Claude-specific machinery with a different binary name; it is
|
|
5944
|
+
// `spawnAntigravity`, entirely.
|
|
5945
|
+
if ((stage?.agent ?? run.agent) === 'agy') {
|
|
5946
|
+
const agyCmd = config.agentCommands.find((each) => each.endsWith('agy')) ?? 'agy';
|
|
5947
|
+
return spawnAntigravity(config, run, runToken, cwd, baseCommit, resume, projectServers,
|
|
5948
|
+
skills, stageExperts, cannotDelegate, instincts, briefing, plan, lifecycle, stage, carried,
|
|
5949
|
+
agyCmd, granted);
|
|
5950
|
+
}
|
|
5951
|
+
|
|
5891
5952
|
const ceiling = [
|
|
5892
5953
|
...(config.grantable ?? []),
|
|
5893
5954
|
...(config.projects[run.projectSlug]?.grantable ?? []),
|
|
@@ -5992,13 +6053,8 @@ async function spawnAgent(config, run, runToken, cwd, baseCommit, workspace, res
|
|
|
5992
6053
|
};
|
|
5993
6054
|
await writeFile(mcpConfigPath, JSON.stringify({ mcpServers }, null, 2));
|
|
5994
6055
|
|
|
5995
|
-
// R147.
|
|
5996
|
-
//
|
|
5997
|
-
// judges by the frontmatter the plugin below is written from — so the plugin
|
|
5998
|
-
// it loads, the `Agent` it is allowed and the list it is told about all
|
|
5999
|
-
// agree. Everything else, and every process with no lifecycle, gets them all.
|
|
6000
|
-
const readOnlyStage = Boolean(stage) && ['PLAN', 'VERIFY', 'MEMORY'].includes(stage.stage);
|
|
6001
|
-
const stageExperts = readOnlyStage ? expertAgents.filter(readOnlyExpert) : expertAgents;
|
|
6056
|
+
// R147/R279. `readOnlyStage` and `stageExperts` are computed once, ahead of
|
|
6057
|
+
// the agy branch above, so both spawn paths narrow the same way.
|
|
6002
6058
|
|
|
6003
6059
|
// R161. What this project said had to be used, by the name a transcript line
|
|
6004
6060
|
// carries.
|
|
@@ -6151,11 +6207,7 @@ async function spawnAgent(config, run, runToken, cwd, baseCommit, workspace, res
|
|
|
6151
6207
|
description: each.description ?? each.name ?? each.key,
|
|
6152
6208
|
required: each.mode === 'REQUIRED',
|
|
6153
6209
|
})),
|
|
6154
|
-
cannotDelegate
|
|
6155
|
-
? [`The ${stage.stage} stage holds nothing that can change anything, and none of this `
|
|
6156
|
-
+ "project's experts is read-only, so none can be reached from it. They are reached "
|
|
6157
|
-
+ 'from IMPLEMENT and TEST.']
|
|
6158
|
-
: undefined,
|
|
6210
|
+
cannotDelegate,
|
|
6159
6211
|
// R124. Null on everything but an implementation phase whose card has been
|
|
6160
6212
|
// planned, which the PLATFORM decides — the runner does not work out which
|
|
6161
6213
|
// runs deserve a plan, it carries the one it was handed.
|
|
@@ -6196,6 +6248,14 @@ async function spawnAgent(config, run, runToken, cwd, baseCommit, workspace, res
|
|
|
6196
6248
|
writesItsOwnCard: writesItsOwnCard(run) })]
|
|
6197
6249
|
: null;
|
|
6198
6250
|
|
|
6251
|
+
// Reaching here at all means Claude Code answers this run — the branch at
|
|
6252
|
+
// the top of this function sent Antigravity to spawnAntigravity instead.
|
|
6253
|
+
// Matched against what this machine actually lists (`config.agentCommands`)
|
|
6254
|
+
// by the same suffix rule `readUsage` uses the other way around, so a
|
|
6255
|
+
// machine whose list is agy-first still gets Claude Code's own command.
|
|
6256
|
+
const agentCmd = config.agentCommands.find((each) => !each.endsWith('agy'))
|
|
6257
|
+
?? config.agentCommands[0] ?? 'claude';
|
|
6258
|
+
|
|
6199
6259
|
const args = [
|
|
6200
6260
|
'--mcp-config',
|
|
6201
6261
|
mcpConfigPath,
|
|
@@ -6210,10 +6270,10 @@ async function spawnAgent(config, run, runToken, cwd, baseCommit, workspace, res
|
|
|
6210
6270
|
skills)]
|
|
6211
6271
|
: argsForProfile(agentArgs, run.profile, run, expertAgents, skills))),
|
|
6212
6272
|
];
|
|
6213
|
-
log(` spawning: ${
|
|
6273
|
+
log(` spawning: ${agentCmd} ${args.join(' ')} (prompt on stdin)`);
|
|
6214
6274
|
|
|
6215
6275
|
return new Promise((resolvePromise) => {
|
|
6216
|
-
const child = spawn(
|
|
6276
|
+
const child = spawn(agentCmd, args, {
|
|
6217
6277
|
cwd,
|
|
6218
6278
|
// Its own process group, so cancelling can take down the whole tree
|
|
6219
6279
|
// rather than leaving orphaned children behind.
|
|
@@ -6751,6 +6811,556 @@ async function spawnAgent(config, run, runToken, cwd, baseCommit, workspace, res
|
|
|
6751
6811
|
});
|
|
6752
6812
|
}
|
|
6753
6813
|
|
|
6814
|
+
// --- Antigravity ('agy') ------------------------------------------------------
|
|
6815
|
+
|
|
6816
|
+
// Serializes agy's global registry mutations — MCP servers AND, since R279,
|
|
6817
|
+
// installed plugins. Claude Code's --mcp-config and --plugin-dir are each a
|
|
6818
|
+
// file or directory this daemon writes and hands to one process; agy has no
|
|
6819
|
+
// per-invocation equivalent for either, only two machine-wide registries
|
|
6820
|
+
// (`agy mcp add/remove` and `agy plugin install/uninstall`, both under
|
|
6821
|
+
// ~/.gemini) shared by every agy call on this machine. Both are
|
|
6822
|
+
// re-registered before each spawn — the MCP token is fresh per run, and a
|
|
6823
|
+
// different project may have turned different experts on — and ONE lock
|
|
6824
|
+
// serializes both, because they are the same kind of hazard: two concurrent
|
|
6825
|
+
// agy spawns racing on the same machine-wide state, v1's answer to a problem
|
|
6826
|
+
// Claude Code's own design does not have.
|
|
6827
|
+
let agyRegistryLock = Promise.resolve();
|
|
6828
|
+
function withAgyRegistryLock(work) {
|
|
6829
|
+
const next = agyRegistryLock.then(work, work);
|
|
6830
|
+
agyRegistryLock = next.then(() => {}, () => {});
|
|
6831
|
+
return next;
|
|
6832
|
+
}
|
|
6833
|
+
|
|
6834
|
+
/** One `agy` subcommand, run to completion. Not a session — just its exit and output. */
|
|
6835
|
+
function runAgyCli(agyCmd, args) {
|
|
6836
|
+
return new Promise((resolve) => {
|
|
6837
|
+
let out = '';
|
|
6838
|
+
const child = spawn(agyCmd, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
6839
|
+
child.stdout.on('data', (chunk) => (out += chunk));
|
|
6840
|
+
child.stderr.on('data', (chunk) => (out += chunk));
|
|
6841
|
+
child.on('error', () => resolve({ code: -1, out }));
|
|
6842
|
+
child.on('close', (code) => resolve({ code, out }));
|
|
6843
|
+
});
|
|
6844
|
+
}
|
|
6845
|
+
|
|
6846
|
+
/**
|
|
6847
|
+
* What Antigravity currently has registered, read rather than asked for:
|
|
6848
|
+
* `agy mcp list` has no machine-readable form, and the registry is one
|
|
6849
|
+
* small JSON file at a fixed, undocumented path. Read-only, and tolerant of
|
|
6850
|
+
* that path changing under a future CLI version — an empty list here costs
|
|
6851
|
+
* a stale entry left behind, never a wrong removal.
|
|
6852
|
+
*/
|
|
6853
|
+
async function currentAntigravityMcpNames() {
|
|
6854
|
+
try {
|
|
6855
|
+
const raw = await readFile(join(homedir(), '.gemini', 'config', 'mcp_config.json'), 'utf8');
|
|
6856
|
+
return Object.keys(JSON.parse(raw)?.mcpServers ?? {});
|
|
6857
|
+
} catch {
|
|
6858
|
+
return [];
|
|
6859
|
+
}
|
|
6860
|
+
}
|
|
6861
|
+
|
|
6862
|
+
/**
|
|
6863
|
+
* Registers cawdev's own MCP server, this checkout's own (its `.mcp.json`),
|
|
6864
|
+
* and R76's registry entries this run was handed — R278, the same three
|
|
6865
|
+
* Claude Code's `--mcp-config` carries. R105's skills are NOT here: those
|
|
6866
|
+
* are not servers, see `inlinedSkillsFor`. Experts are NOT here either, but
|
|
6867
|
+
* for the opposite reason R278 first claimed — that card said agy "has
|
|
6868
|
+
* nothing shaped like `--plugin-dir`, only its own, unrelated subagent
|
|
6869
|
+
* system", and that was wrong, unverified when it was written. Checked by
|
|
6870
|
+
* hand for R279: `agy plugin install <directory>` reads the very same
|
|
6871
|
+
* `agents/*.md` shape, and `invoke_subagent` really does reach one — see
|
|
6872
|
+
* `registerAntigravityExperts`, which does for experts what this does for
|
|
6873
|
+
* servers, under the same lock.
|
|
6874
|
+
*
|
|
6875
|
+
* Every name but `cawdev` itself is prefixed `cawdev-server-` — on two
|
|
6876
|
+
* sides of the same reason: this must never touch an MCP server Dali
|
|
6877
|
+
* registered with agy himself for something unrelated, and a stale entry
|
|
6878
|
+
* an EARLIER run left behind (a different project, a different registry)
|
|
6879
|
+
* has to be findable by that same prefix so it can be removed before the
|
|
6880
|
+
* new set goes in. Confirmed by hand that this is safe to do at all: a
|
|
6881
|
+
* registered server inherits the daemon's own environment (`PATH`, `HOME`,
|
|
6882
|
+
* ...) same as any spawned child, so `--env` only has to carry what a
|
|
6883
|
+
* server adds on top of that — not `...process.env` the way the
|
|
6884
|
+
* file-based `--mcp-config` write does.
|
|
6885
|
+
*/
|
|
6886
|
+
function registerAntigravityMcp(agyCmd, {
|
|
6887
|
+
url, token, projectSlug, cwd, serverPath, repoServers, attachedServers,
|
|
6888
|
+
}) {
|
|
6889
|
+
return withAgyRegistryLock(async () => {
|
|
6890
|
+
const wanted = new Map();
|
|
6891
|
+
wanted.set('cawdev', {
|
|
6892
|
+
command: process.execPath,
|
|
6893
|
+
args: [serverPath],
|
|
6894
|
+
env: {
|
|
6895
|
+
CAWDEV_URL: url, CAWDEV_TOKEN: token, CAWDEV_PROJECT: projectSlug, CAWDEV_WORKSPACE: cwd,
|
|
6896
|
+
},
|
|
6897
|
+
});
|
|
6898
|
+
// This checkout's own `.mcp.json`.
|
|
6899
|
+
for (const [name, def] of Object.entries(repoServers ?? {})) {
|
|
6900
|
+
wanted.set(`cawdev-server-${name}`, {
|
|
6901
|
+
command: def.command, args: def.args ?? [], env: def.env ?? {},
|
|
6902
|
+
});
|
|
6903
|
+
}
|
|
6904
|
+
// R76's registry, via `resolveSkills` — `skill` here is what that
|
|
6905
|
+
// function calls its own parameter, from a rename that came after it
|
|
6906
|
+
// was written; the shape is `RunMcpServerView`, a server, not a SKILL.md.
|
|
6907
|
+
for (const { skill, local } of attachedServers ?? []) {
|
|
6908
|
+
wanted.set(`cawdev-server-${skill.key}`, {
|
|
6909
|
+
command: skill.command, args: skill.args, env: local.env ?? {},
|
|
6910
|
+
});
|
|
6911
|
+
}
|
|
6912
|
+
|
|
6913
|
+
const stale = (await currentAntigravityMcpNames()).filter((name) => (
|
|
6914
|
+
name === 'cawdev' || name.startsWith('cawdev-server-')
|
|
6915
|
+
) && !wanted.has(name));
|
|
6916
|
+
for (const name of stale) {
|
|
6917
|
+
await runAgyCli(agyCmd, ['mcp', 'remove', name]);
|
|
6918
|
+
}
|
|
6919
|
+
|
|
6920
|
+
let ok = true;
|
|
6921
|
+
for (const [name, def] of wanted) {
|
|
6922
|
+
// Removed first — agy mcp add UPDATES an existing name in place, but
|
|
6923
|
+
// a run's token changes every time and "updates" is exactly the kind
|
|
6924
|
+
// of word worth not trusting without having watched it happen.
|
|
6925
|
+
await runAgyCli(agyCmd, ['mcp', 'remove', name]);
|
|
6926
|
+
// Flags before <name> or agy silently absorbs them into the spawned
|
|
6927
|
+
// command's own argv instead of its env — confirmed by hand: the same
|
|
6928
|
+
// --env flags placed after the name landed in mcp_config.json's
|
|
6929
|
+
// `args`, never in an `env` object. `agy mcp add --help`'s own note
|
|
6930
|
+
// ("Flags must come before <name>") says this; it just doesn't reject
|
|
6931
|
+
// the wrong order, it mis-files it.
|
|
6932
|
+
const envArgs = Object.entries(def.env).flatMap(([key, value]) => ['--env', `${key}=${value}`]);
|
|
6933
|
+
const added = await runAgyCli(agyCmd,
|
|
6934
|
+
['mcp', 'add', ...envArgs, name, def.command, ...def.args]);
|
|
6935
|
+
if (added.code !== 0) {
|
|
6936
|
+
ok = false;
|
|
6937
|
+
}
|
|
6938
|
+
}
|
|
6939
|
+
return ok;
|
|
6940
|
+
});
|
|
6941
|
+
}
|
|
6942
|
+
|
|
6943
|
+
/**
|
|
6944
|
+
* What Antigravity currently has installed, read the same way
|
|
6945
|
+
* `currentAntigravityMcpNames` reads its registrations: `agy plugin list`
|
|
6946
|
+
* has no machine-readable form either, and `import_manifest.json` is the
|
|
6947
|
+
* file it renders from (confirmed by hand: installing and listing a probe
|
|
6948
|
+
* plugin round-tripped through exactly this file). Tolerant of the same
|
|
6949
|
+
* failure for the same reason — an empty list here costs a stale plugin
|
|
6950
|
+
* left behind, never a wrong uninstall.
|
|
6951
|
+
*/
|
|
6952
|
+
async function currentAntigravityPluginNames() {
|
|
6953
|
+
try {
|
|
6954
|
+
const raw = await readFile(join(homedir(), '.gemini', 'config', 'import_manifest.json'), 'utf8');
|
|
6955
|
+
const imports = JSON.parse(raw)?.imports;
|
|
6956
|
+
return Array.isArray(imports) ? imports.map((each) => each.name).filter(Boolean) : [];
|
|
6957
|
+
} catch {
|
|
6958
|
+
return [];
|
|
6959
|
+
}
|
|
6960
|
+
}
|
|
6961
|
+
|
|
6962
|
+
/**
|
|
6963
|
+
* Installs this run's experts as an Antigravity plugin — R279, doing for
|
|
6964
|
+
* `agents/*.md` what `registerAntigravityMcp` does for servers, and under
|
|
6965
|
+
* the SAME lock: `agy plugin install` is machine-wide state exactly like
|
|
6966
|
+
* `agy mcp add` is, one project's experts at a time on this machine.
|
|
6967
|
+
*
|
|
6968
|
+
* <p>Always removes whatever `ANTIGRAVITY_EXPERTS_PLUGIN` currently holds
|
|
6969
|
+
* first — never trusting `agy plugin install` to update a name in place,
|
|
6970
|
+
* the same caution `registerAntigravityMcp` already takes with `agy mcp
|
|
6971
|
+
* add`, unverified here and not worth finding out the hard way — then
|
|
6972
|
+
* installs fresh only when this run was handed experts at all. Takes
|
|
6973
|
+
* `stageExperts`, never raw `expertAgents`: R147's read-only-stage
|
|
6974
|
+
* narrowing has to hold for Antigravity exactly as it does for Claude Code,
|
|
6975
|
+
* and the caller is where that narrowing already happened.
|
|
6976
|
+
*
|
|
6977
|
+
* <p>Failing to install is NOT failed the way a failed MCP registration is
|
|
6978
|
+
* — an expert is optional delegation, not plumbing a report/ask_user call
|
|
6979
|
+
* depends on, so the caller logs it and carries on without one rather than
|
|
6980
|
+
* failing the run.
|
|
6981
|
+
*/
|
|
6982
|
+
function registerAntigravityExperts(agyCmd, { expertAgents }) {
|
|
6983
|
+
return withAgyRegistryLock(async () => {
|
|
6984
|
+
const stale = (await currentAntigravityPluginNames())
|
|
6985
|
+
.filter((name) => name === ANTIGRAVITY_EXPERTS_PLUGIN);
|
|
6986
|
+
for (const name of stale) {
|
|
6987
|
+
await runAgyCli(agyCmd, ['plugin', 'uninstall', name]);
|
|
6988
|
+
}
|
|
6989
|
+
if (!expertAgents.length) {
|
|
6990
|
+
return true;
|
|
6991
|
+
}
|
|
6992
|
+
const pluginDir = await mkdtemp(join(tmpdir(), 'cawdev-agy-experts-'));
|
|
6993
|
+
try {
|
|
6994
|
+
const root = await writeAntigravityExpertsPlugin(pluginDir, expertAgents);
|
|
6995
|
+
const installed = await runAgyCli(agyCmd, ['plugin', 'install', root]);
|
|
6996
|
+
return installed.code === 0;
|
|
6997
|
+
} finally {
|
|
6998
|
+
await rm(pluginDir, { recursive: true, force: true });
|
|
6999
|
+
}
|
|
7000
|
+
});
|
|
7001
|
+
}
|
|
7002
|
+
|
|
7003
|
+
/**
|
|
7004
|
+
* The prompt's half of R279 — telling the session which experts it has and
|
|
7005
|
+
* how to reach one, the way `harnessPrompt`'s own experts section does for
|
|
7006
|
+
* Claude Code. NOT folded into `harnessPrompt` itself: that function's
|
|
7007
|
+
* wording ("delegate with the `Agent` tool, `subagent_type` exactly as
|
|
7008
|
+
* written") names a Claude Code tool, and agy's is `invoke_subagent` — the
|
|
7009
|
+
* same split `inlinedSkillsFor` already makes for skills, for the same
|
|
7010
|
+
* reason.
|
|
7011
|
+
*
|
|
7012
|
+
* <p>`experts` here must already be `stageExperts` — narrowed for a
|
|
7013
|
+
* read-only stage — and, when nothing was installed for this run
|
|
7014
|
+
* (`registerAntigravityExperts` failed, or there was nothing to install),
|
|
7015
|
+
* an empty list: advertising an expert the plugin step could not actually
|
|
7016
|
+
* install would tell the session it can reach something it cannot.
|
|
7017
|
+
*/
|
|
7018
|
+
function expertsBlockFor(experts, cannotDelegate) {
|
|
7019
|
+
const list = Array.isArray(experts) ? experts : [];
|
|
7020
|
+
if (!list.length) {
|
|
7021
|
+
return cannotDelegate?.length ? `\n\n## Experts\n\n${cannotDelegate.join(' ')}` : '';
|
|
7022
|
+
}
|
|
7023
|
+
const must = list.filter((each) => each.mode === 'REQUIRED');
|
|
7024
|
+
const may = list.filter((each) => each.mode !== 'REQUIRED');
|
|
7025
|
+
const entry = (each) => `- **${each.key}** — ${each.description ?? each.name ?? each.key}`;
|
|
7026
|
+
const section = (heading, rows) => rows.length
|
|
7027
|
+
? `## ${heading}\n\n${rows.map(entry).join('\n')}`
|
|
7028
|
+
: '';
|
|
7029
|
+
return '\n\n' + [
|
|
7030
|
+
section('Experts you MUST use — delegate with `invoke_subagent`, naming it exactly as '
|
|
7031
|
+
+ 'written below, before this stage is finished', must),
|
|
7032
|
+
section("Experts — delegate with `invoke_subagent` whenever one's description fits the "
|
|
7033
|
+
+ 'step you are on', may),
|
|
7034
|
+
].filter(Boolean).join('\n\n');
|
|
7035
|
+
}
|
|
7036
|
+
|
|
7037
|
+
/**
|
|
7038
|
+
* R105's skills, whole, for a CLI with no `Skill` tool to fetch one lazily.
|
|
7039
|
+
*
|
|
7040
|
+
* <p>Claude Code sees a name and a description in its harness prompt and
|
|
7041
|
+
* loads the `body` — the actual SKILL.md instructions — only if the model
|
|
7042
|
+
* invokes it, through a plugin directory this agent has no equivalent way
|
|
7043
|
+
* to be handed. There is nothing to invoke here, so the body is INLINED
|
|
7044
|
+
* up front instead: every skill this run was handed, whether or not the
|
|
7045
|
+
* model ends up needing it. Larger prompt, no lazy loading — the honest
|
|
7046
|
+
* trade for a CLI with no mechanism to defer it.
|
|
7047
|
+
*/
|
|
7048
|
+
function inlinedSkillsFor(skills) {
|
|
7049
|
+
const list = Array.isArray(skills) ? skills : [];
|
|
7050
|
+
if (!list.length) {
|
|
7051
|
+
return '';
|
|
7052
|
+
}
|
|
7053
|
+
const must = list.filter((each) => each.mode === 'REQUIRED');
|
|
7054
|
+
const may = list.filter((each) => each.mode !== 'REQUIRED');
|
|
7055
|
+
const entry = (each) => `### ${each.name ?? each.key}\n\n${each.description ?? ''}\n\n${each.body ?? ''}`
|
|
7056
|
+
.trim();
|
|
7057
|
+
const section = (heading, rows) => rows.length
|
|
7058
|
+
? `## ${heading}\n\n${rows.map(entry).join('\n\n')}`
|
|
7059
|
+
: '';
|
|
7060
|
+
return '\n\n' + [
|
|
7061
|
+
section('Skills you MUST use — this project requires each of these; use one before this '
|
|
7062
|
+
+ 'stage is finished', must),
|
|
7063
|
+
section('Skills — use one when its description fits the work', may),
|
|
7064
|
+
].filter(Boolean).join('\n\n');
|
|
7065
|
+
}
|
|
7066
|
+
|
|
7067
|
+
/**
|
|
7068
|
+
* Antigravity's shape — one-shot per turn, not a long-lived process.
|
|
7069
|
+
*
|
|
7070
|
+
* Checked against the real CLI by hand rather than guessed at: no
|
|
7071
|
+
* `--mcp-config` (`flags provided but not defined`), no `--allowedTools` (no
|
|
7072
|
+
* per-tool allow-list exists at all), no `--resume <id>` (`--conversation
|
|
7073
|
+
* <id>` instead). `--mode plan` denies MCP tool calls outright — including
|
|
7074
|
+
* calls to cawdev's own `report`/`ask_user` — so even a read-only run needs
|
|
7075
|
+
* `--dangerously-skip-permissions` just to talk to the platform. There is no
|
|
7076
|
+
* narrower option on this CLI today.
|
|
7077
|
+
*
|
|
7078
|
+
* So: this refuses to run at all unless the machine has granted "everything"
|
|
7079
|
+
* — R126, the same switch Claude Code's `bypassPermissions` already stands
|
|
7080
|
+
* behind — and every call that does run gets `--dangerously-skip-permissions`
|
|
7081
|
+
* unconditionally, coding or not. No per-project tool-rule narrowing, and no
|
|
7082
|
+
* live mid-run prompting (R22's "keep stdin open" assumes a process that
|
|
7083
|
+
* stays up between turns; agy's `-p` exits after one). A STAGE is already
|
|
7084
|
+
* one process per R112, so stages fit this shape without changing it — a
|
|
7085
|
+
* follow-up typed into a RUNNING session does not reach it yet, and that gap
|
|
7086
|
+
* is not silently papered over: `sendPrompt` still only writes to a live
|
|
7087
|
+
* child's stdin, which this never opens.
|
|
7088
|
+
*
|
|
7089
|
+
* R278: this checkout's own MCP servers and R76's registry ARE reached now,
|
|
7090
|
+
* the way `registerAntigravityMcp` describes, and R105's skills are
|
|
7091
|
+
* inlined whole (`inlinedSkillsFor`) rather than reached at all — there is
|
|
7092
|
+
* nothing on this CLI shaped like Claude Code's `Skill` tool to fetch one
|
|
7093
|
+
* lazily.
|
|
7094
|
+
*
|
|
7095
|
+
* R279: expert agents ARE reached too, now — R278's claim that agy "has
|
|
7096
|
+
* nothing shaped like `--plugin-dir`" was wrong, and unverified when it was
|
|
7097
|
+
* written. `agy plugin install <directory>` reads the same `agents/*.md`
|
|
7098
|
+
* shape Claude Code's plugin does; `registerAntigravityExperts` installs
|
|
7099
|
+
* this run's `stageExperts` under it, and `invoke_subagent` is the real
|
|
7100
|
+
* tool a session delegates through — confirmed end-to-end by hand, a probe
|
|
7101
|
+
* agent installed, invoked by name, and its answer returned correctly.
|
|
7102
|
+
*/
|
|
7103
|
+
async function spawnAntigravity(config, run, runToken, cwd, baseCommit, resume, projectServers,
|
|
7104
|
+
skills, stageExperts, cannotDelegate, instincts, briefing, plan, lifecycle, stage, carried,
|
|
7105
|
+
agyCmd, granted) {
|
|
7106
|
+
if (!granted.everything) {
|
|
7107
|
+
const summary = 'This machine has not allowed everything, and Antigravity has no narrower '
|
|
7108
|
+
+ 'permission mode — no per-tool allow-list exists on it, and even reading needs it. Turn '
|
|
7109
|
+
+ 'on "allow everything" for this machine, or use Claude Code for this run.';
|
|
7110
|
+
log(` refusing to spawn Antigravity: ${summary}`);
|
|
7111
|
+
await finish(config, run, 'FAILED', summary);
|
|
7112
|
+
return stage ? { code: -1, signal: null, text: summary, turnEnded: false } : undefined;
|
|
7113
|
+
}
|
|
7114
|
+
|
|
7115
|
+
// Created here rather than down with the spawn — the notes below (what
|
|
7116
|
+
// attached, what a project asked for that this machine refused, what an
|
|
7117
|
+
// index build said) belong on the run the moment they are known, same as
|
|
7118
|
+
// Claude Code's path: a person watching should not have to wait for the
|
|
7119
|
+
// one-shot call to finish before finding out something never loaded.
|
|
7120
|
+
const transcript = new Transcript(config, run);
|
|
7121
|
+
|
|
7122
|
+
// Two DIFFERENT things cawdev calls capabilities, reached two different
|
|
7123
|
+
// ways even for Claude Code — R278 gives Antigravity both, each the way
|
|
7124
|
+
// its own shape allows:
|
|
7125
|
+
//
|
|
7126
|
+
// `projectServers` (R76's registry, `claimed.mcpServers` — the parameter
|
|
7127
|
+
// is misleadingly named after an old rename, not this checkout's own
|
|
7128
|
+
// `.mcp.json`) and `repoServers` (that actual file, read fresh) are both
|
|
7129
|
+
// real MCP SERVERS — a command line cawdev already knows how to run.
|
|
7130
|
+
// `resolveSkills` (despite its name — R76 called these "skills" before
|
|
7131
|
+
// R105 split the word) turns the registry half into the same shape.
|
|
7132
|
+
// Both go through `registerAntigravityMcp`, the same way cawdev's own
|
|
7133
|
+
// server does.
|
|
7134
|
+
//
|
|
7135
|
+
// `skills` (R105, `claimed.skills`) is NOT a server at all — it is a
|
|
7136
|
+
// SKILL.md's `body`, markdown Claude Code loads lazily through its own
|
|
7137
|
+
// `Skill` tool when the model invokes it by name, bundled into a plugin
|
|
7138
|
+
// directory this agent has no equivalent way to be handed
|
|
7139
|
+
// (`--plugin-dir`, same gap experts have). There is nothing to invoke
|
|
7140
|
+
// here to fetch it later, so it is not offered as something reachable —
|
|
7141
|
+
// it is INLINED below, in full, up front.
|
|
7142
|
+
const serverPath = config.mcpServerPath ?? new URL('../mcp/server.mjs', import.meta.url).pathname;
|
|
7143
|
+
const repoServers = await projectMcpServers(cwd);
|
|
7144
|
+
const { attached: attachedServers, notes: serverNotes } =
|
|
7145
|
+
await resolveSkills(config, run, cwd, baseCommit, projectServers);
|
|
7146
|
+
if (attachedServers.length) {
|
|
7147
|
+
log(` loading ${attachedServers.length} MCP server(s)`);
|
|
7148
|
+
}
|
|
7149
|
+
for (const line of serverNotes) {
|
|
7150
|
+
log(` ${line}`);
|
|
7151
|
+
transcript.push({ kind: 'SYSTEM', body: line });
|
|
7152
|
+
}
|
|
7153
|
+
const registered = await registerAntigravityMcp(agyCmd, {
|
|
7154
|
+
url: config.url, token: runToken, projectSlug: run.projectSlug, cwd, serverPath,
|
|
7155
|
+
repoServers, attachedServers,
|
|
7156
|
+
});
|
|
7157
|
+
if (!registered) {
|
|
7158
|
+
const summary = 'Could not register this session\'s MCP servers with Antigravity (`agy mcp '
|
|
7159
|
+
+ 'add` failed) — without cawdev\'s own this session cannot report, ask a question, or '
|
|
7160
|
+
+ 'read the roadmap.';
|
|
7161
|
+
log(` ${summary}`);
|
|
7162
|
+
await finish(config, run, 'FAILED', summary);
|
|
7163
|
+
return stage ? { code: -1, signal: null, text: summary, turnEnded: false } : undefined;
|
|
7164
|
+
}
|
|
7165
|
+
|
|
7166
|
+
// R279. Unlike the MCP registration above, a failed expert install does
|
|
7167
|
+
// NOT fail the run — an expert is optional delegation, not the plumbing a
|
|
7168
|
+
// report/ask_user call depends on. `usableExperts` drops to empty so the
|
|
7169
|
+
// prompt below never advertises an expert that was not actually
|
|
7170
|
+
// installed; `stageExperts` itself already carries R147's read-only-stage
|
|
7171
|
+
// narrowing from the caller.
|
|
7172
|
+
if (stageExperts.length) {
|
|
7173
|
+
log(` loading ${stageExperts.length} expert(s)`);
|
|
7174
|
+
}
|
|
7175
|
+
const expertsRegistered = await registerAntigravityExperts(agyCmd, { expertAgents: stageExperts });
|
|
7176
|
+
if (!expertsRegistered && stageExperts.length) {
|
|
7177
|
+
const note = 'Could not install this project\'s experts into Antigravity (`agy plugin '
|
|
7178
|
+
+ 'install` failed) — continuing without delegation.';
|
|
7179
|
+
log(` ${note}`);
|
|
7180
|
+
transcript.push({ kind: 'SYSTEM', body: note });
|
|
7181
|
+
}
|
|
7182
|
+
const usableExperts = expertsRegistered ? stageExperts : [];
|
|
7183
|
+
|
|
7184
|
+
// Same assembly as Claude Code's — promptFor, harnessPrompt, stagePrompt
|
|
7185
|
+
// are all agent-agnostic. Both experts and skills are kept OUT of
|
|
7186
|
+
// harnessPrompt's own `experts`/`skills` params and handled entirely
|
|
7187
|
+
// below instead — its wording ("the `Agent` tool", "the `Skill` tool")
|
|
7188
|
+
// names tools this CLI does not have. `expertsBlockFor` and
|
|
7189
|
+
// `inlinedSkillsFor` are agy's own wording for the same two ideas.
|
|
7190
|
+
const harness = harnessPrompt({
|
|
7191
|
+
instincts, briefing, experts: [], skills: [], plan, lifecycle,
|
|
7192
|
+
repoConfig: await readRepoConfig(cwd),
|
|
7193
|
+
});
|
|
7194
|
+
const skillsBlock = inlinedSkillsFor(skills);
|
|
7195
|
+
const expertsBlock = expertsBlockFor(usableExperts, cannotDelegate);
|
|
7196
|
+
const resuming = Boolean(resume?.agentSessionId);
|
|
7197
|
+
const promptText = resuming
|
|
7198
|
+
? resume.prompt
|
|
7199
|
+
: (promptFor(run) + harness + skillsBlock + expertsBlock
|
|
7200
|
+
+ (stage ? stagePrompt(stage, carried) : ''));
|
|
7201
|
+
|
|
7202
|
+
const args = ['-p', promptText, '--output-format', 'stream-json', '--dangerously-skip-permissions'];
|
|
7203
|
+
if (run.model) {
|
|
7204
|
+
args.push('--model', run.model);
|
|
7205
|
+
}
|
|
7206
|
+
if (resuming) {
|
|
7207
|
+
args.push('--conversation', resume.agentSessionId);
|
|
7208
|
+
}
|
|
7209
|
+
|
|
7210
|
+
log(` spawning: ${agyCmd} -p <prompt omitted, ${promptText.length} chars> --output-format `
|
|
7211
|
+
+ `stream-json --dangerously-skip-permissions${resuming ? ` --conversation ${resume.agentSessionId}` : ''}`);
|
|
7212
|
+
|
|
7213
|
+
// The same live view Claude Code gets — none of this existed before, and a
|
|
7214
|
+
// run that writes nothing here is a run the console and the CLI's attach
|
|
7215
|
+
// both draw as a black box until the one-shot call finally returns.
|
|
7216
|
+
// Confirmed by hand against the real CLI: `--output-format stream-json`
|
|
7217
|
+
// gives `init` (once, carries the conversation id), `step_update` (an
|
|
7218
|
+
// `agent_response` step's `text_delta` is an INCREMENTAL chunk to append,
|
|
7219
|
+
// not the text so far; a `tool` step is ACTIVE with the call and DONE with
|
|
7220
|
+
// `tool_info.output`), and one final `result` — the same shape
|
|
7221
|
+
// `--output-format json` returns whole, just arriving a piece at a time.
|
|
7222
|
+
|
|
7223
|
+
return new Promise((resolvePromise) => {
|
|
7224
|
+
let result = null;
|
|
7225
|
+
let err = '';
|
|
7226
|
+
let buffer = '';
|
|
7227
|
+
const child = spawn(agyCmd, args, {
|
|
7228
|
+
cwd,
|
|
7229
|
+
// Its own process group — see the same choice on the Claude Code path.
|
|
7230
|
+
detached: true,
|
|
7231
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
7232
|
+
});
|
|
7233
|
+
child.cawdevProjectSlug = run.projectSlug;
|
|
7234
|
+
// The existing cancellation sweep (`reapCancelled`) reads this map by
|
|
7235
|
+
// run id and SIGTERMs whatever is in it — true for any child, so a
|
|
7236
|
+
// cancelled Antigravity run stops exactly the way a cancelled Claude
|
|
7237
|
+
// Code one does, with no code of its own.
|
|
7238
|
+
running.set(run.id, child);
|
|
7239
|
+
|
|
7240
|
+
// stream-json arrives in chunks that split mid-line, same as Claude
|
|
7241
|
+
// Code's — buffer until a full line rather than assuming one chunk is
|
|
7242
|
+
// one event.
|
|
7243
|
+
child.stdout.on('data', (chunk) => {
|
|
7244
|
+
buffer += chunk;
|
|
7245
|
+
let newline;
|
|
7246
|
+
while ((newline = buffer.indexOf('\n')) !== -1) {
|
|
7247
|
+
const line = buffer.slice(0, newline);
|
|
7248
|
+
buffer = buffer.slice(newline + 1);
|
|
7249
|
+
if (!line.trim()) continue;
|
|
7250
|
+
let event;
|
|
7251
|
+
try {
|
|
7252
|
+
event = JSON.parse(line);
|
|
7253
|
+
} catch {
|
|
7254
|
+
// A stray non-protocol line on stdout — shown rather than dropped,
|
|
7255
|
+
// the same call `linesOf` makes for Claude Code's unparseable ones.
|
|
7256
|
+
transcript.push({ kind: 'SYSTEM', body: line.slice(0, 4000) });
|
|
7257
|
+
continue;
|
|
7258
|
+
}
|
|
7259
|
+
if (event.event === 'init') {
|
|
7260
|
+
if (event.conversation_id) {
|
|
7261
|
+
reportSessionId(config, run, event.conversation_id);
|
|
7262
|
+
}
|
|
7263
|
+
transcript.push({
|
|
7264
|
+
kind: 'SYSTEM',
|
|
7265
|
+
body: `session ${short(event.conversation_id)} started`
|
|
7266
|
+
+ (run.model ? ` on ${run.model}` : ''),
|
|
7267
|
+
});
|
|
7268
|
+
} else if (event.event === 'step_update') {
|
|
7269
|
+
const step = event.step_update ?? {};
|
|
7270
|
+
if (step.step_type === 'agent_response' && step.text_delta) {
|
|
7271
|
+
transcript.push({ kind: 'ASSISTANT', body: step.text_delta });
|
|
7272
|
+
} else if (step.step_type === 'tool' && step.state === 'ACTIVE') {
|
|
7273
|
+
transcript.push({
|
|
7274
|
+
kind: 'TOOL',
|
|
7275
|
+
body: `${step.tool_name}(${JSON.stringify(step.tool_info?.parameters ?? {})})`,
|
|
7276
|
+
});
|
|
7277
|
+
} else if (step.step_type === 'tool' && step.state === 'DONE') {
|
|
7278
|
+
transcript.push({
|
|
7279
|
+
kind: 'TOOL_RESULT',
|
|
7280
|
+
body: String(step.tool_info?.output ?? '(no output)').slice(0, 4000),
|
|
7281
|
+
});
|
|
7282
|
+
} else if (step.step_type === 'subagent' && step.state === 'ACTIVE') {
|
|
7283
|
+
// R279. Delegation is its OWN step_type, `subagent` — confirmed
|
|
7284
|
+
// by hand it is not `tool`, which is why this needed its own
|
|
7285
|
+
// branch rather than falling into the one above by accident.
|
|
7286
|
+
// No `tool_info` here, only `subagent_info.subagents[]`; the
|
|
7287
|
+
// sub-conversation's own transcript is a local file this
|
|
7288
|
+
// process never reads, so the parent's own narration (an
|
|
7289
|
+
// ordinary `agent_response`) is what carries the answer back to
|
|
7290
|
+
// this transcript, same as it already did in the run that
|
|
7291
|
+
// proved this end to end.
|
|
7292
|
+
const who = (step.subagent_info?.subagents ?? [])
|
|
7293
|
+
.map((each) => each.type_name).filter(Boolean).join(', ') || 'a subagent';
|
|
7294
|
+
const prompt = step.subagent_info?.subagents?.[0]?.initial_prompt ?? '';
|
|
7295
|
+
transcript.push({ kind: 'TOOL', body: `invoke_subagent(${who}): ${prompt}` });
|
|
7296
|
+
} else if (step.step_type === 'subagent' && step.state === 'DONE') {
|
|
7297
|
+
const who = (step.subagent_info?.subagents ?? [])
|
|
7298
|
+
.map((each) => each.type_name).filter(Boolean).join(', ') || 'a subagent';
|
|
7299
|
+
transcript.push({ kind: 'TOOL_RESULT', body: `${who} finished` });
|
|
7300
|
+
}
|
|
7301
|
+
// user_input steps carry no text of their own — the prompt is
|
|
7302
|
+
// already shown as "What it was asked", from run.openingPrompt.
|
|
7303
|
+
} else if (event.event === 'result') {
|
|
7304
|
+
result = event.result;
|
|
7305
|
+
if (result?.usage) {
|
|
7306
|
+
reportUsage(config, run, {
|
|
7307
|
+
tokensIn: Number(result.usage.input_tokens) || 0,
|
|
7308
|
+
tokensOut: Number(result.usage.output_tokens) || 0,
|
|
7309
|
+
});
|
|
7310
|
+
}
|
|
7311
|
+
}
|
|
7312
|
+
}
|
|
7313
|
+
});
|
|
7314
|
+
child.stderr.on('data', (chunk) => {
|
|
7315
|
+
const text = String(chunk).trim();
|
|
7316
|
+
if (text) {
|
|
7317
|
+
log(` agent stderr: ${text.slice(0, 400)}`);
|
|
7318
|
+
}
|
|
7319
|
+
err += chunk;
|
|
7320
|
+
});
|
|
7321
|
+
|
|
7322
|
+
child.on('error', async (failure) => {
|
|
7323
|
+
running.delete(run.id);
|
|
7324
|
+
const summary = `Could not spawn Antigravity: ${failure.message}`;
|
|
7325
|
+
await transcript.flush();
|
|
7326
|
+
await finish(config, run, 'FAILED', summary);
|
|
7327
|
+
resolvePromise(
|
|
7328
|
+
stage ? { code: -1, signal: null, text: summary, turnEnded: false } : undefined);
|
|
7329
|
+
});
|
|
7330
|
+
|
|
7331
|
+
child.on('close', async (code) => {
|
|
7332
|
+
running.delete(run.id);
|
|
7333
|
+
await transcript.flush();
|
|
7334
|
+
const text = result?.response
|
|
7335
|
+
|| (result?.denied_actions?.length
|
|
7336
|
+
? `Antigravity denied an action it needed and stopped: `
|
|
7337
|
+
+ result.denied_actions.map((each) => each.display_name ?? each.action).join(', ')
|
|
7338
|
+
: null)
|
|
7339
|
+
|| err.trim() || 'Antigravity produced no output.';
|
|
7340
|
+
// Whether the CLI ever gave back a parseable turn — this agent has no
|
|
7341
|
+
// separate "the turn ended" signal the way Claude Code's stream does;
|
|
7342
|
+
// getting the `result` event at all IS the turn ending, one way or
|
|
7343
|
+
// the other.
|
|
7344
|
+
const turnEnded = Boolean(result);
|
|
7345
|
+
|
|
7346
|
+
// The agent normally ends the run itself with report(done|blocked),
|
|
7347
|
+
// through the same cawdev MCP server Claude Code calls — this only
|
|
7348
|
+
// catches a call that produced nothing report could act on.
|
|
7349
|
+
const current = await api(config, `/api/projects/${run.projectSlug}/runs/${run.id}`)
|
|
7350
|
+
.catch(() => null);
|
|
7351
|
+
if (current?.live && !stage) {
|
|
7352
|
+
await finish(config, run, code === 0 && result?.status === 'SUCCESS' ? 'FINISHED' : 'FAILED',
|
|
7353
|
+
text);
|
|
7354
|
+
}
|
|
7355
|
+
// R112: not between stages — see the same line on the Claude Code path.
|
|
7356
|
+
if (!stage) {
|
|
7357
|
+
await settleActions(config, run, cwd);
|
|
7358
|
+
}
|
|
7359
|
+
resolvePromise(stage ? { code, signal: null, text, turnEnded } : undefined);
|
|
7360
|
+
});
|
|
7361
|
+
});
|
|
7362
|
+
}
|
|
7363
|
+
|
|
6754
7364
|
/**
|
|
6755
7365
|
* Tells the platform what the CLI calls this session — R69.
|
|
6756
7366
|
*
|
|
@@ -6853,7 +7463,7 @@ function capabilities(config) {
|
|
|
6853
7463
|
// can read this can say which side refused without waiting for a run to
|
|
6854
7464
|
// say it in a transcript.
|
|
6855
7465
|
skills: config.skills ?? [],
|
|
6856
|
-
|
|
7466
|
+
agents: config.agentCommands,
|
|
6857
7467
|
// R260. This daemon knows a claim can carry `baseBranch` — a sprint's
|
|
6858
7468
|
// branch to cut from, open the pull request against and merge into. It
|
|
6859
7469
|
// is the WHOLE of how the platform tells an old daemon from a new one:
|
|
@@ -6878,56 +7488,59 @@ function capabilities(config) {
|
|
|
6878
7488
|
* message from a CLI it does not own is worse than one that says so and runs.
|
|
6879
7489
|
*/
|
|
6880
7490
|
async function probePermissionPrompt(config) {
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
child
|
|
6886
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
6887
|
-
});
|
|
6888
|
-
} catch (failure) {
|
|
6889
|
-
return done(`could not be run: ${failure.message}`);
|
|
6890
|
-
}
|
|
6891
|
-
const give_up = setTimeout(() => {
|
|
7491
|
+
let allGood = true;
|
|
7492
|
+
for (const agentCommand of config.agentCommands) {
|
|
7493
|
+
const said = await new Promise((done) => {
|
|
7494
|
+
let text = '';
|
|
7495
|
+
let child;
|
|
6892
7496
|
try {
|
|
6893
|
-
child
|
|
6894
|
-
|
|
6895
|
-
|
|
7497
|
+
child = spawn(agentCommand, ['-p', '--permission-prompt-tool', 'mcp__cawdev__approve'], {
|
|
7498
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
7499
|
+
});
|
|
7500
|
+
} catch (failure) {
|
|
7501
|
+
return done(`could not be run: ${failure.message}`);
|
|
6896
7502
|
}
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
7503
|
+
const give_up = setTimeout(() => {
|
|
7504
|
+
try {
|
|
7505
|
+
child.kill('SIGKILL');
|
|
7506
|
+
} catch {
|
|
7507
|
+
// Already gone.
|
|
7508
|
+
}
|
|
7509
|
+
done(text);
|
|
7510
|
+
}, 10_000);
|
|
7511
|
+
give_up.unref?.();
|
|
7512
|
+
|
|
7513
|
+
child.stdout.on('data', (chunk) => (text += chunk));
|
|
7514
|
+
child.stderr.on('data', (chunk) => (text += chunk));
|
|
7515
|
+
child.on('error', (failure) => {
|
|
7516
|
+
clearTimeout(give_up);
|
|
7517
|
+
done(`could not be run: ${failure.message}`);
|
|
7518
|
+
});
|
|
7519
|
+
// `close`, not `exit`: this reads the CLI's own help to decide whether a
|
|
7520
|
+
// flag exists, and a truncated read would answer "no" for a flag that is
|
|
7521
|
+
// there. See git() above.
|
|
7522
|
+
child.on('close', () => {
|
|
7523
|
+
clearTimeout(give_up);
|
|
7524
|
+
done(text);
|
|
7525
|
+
});
|
|
7526
|
+
// Nothing to say: the missing prompt is what we want it to complain about.
|
|
7527
|
+
child.stdin.end();
|
|
6913
7528
|
});
|
|
6914
|
-
// Nothing to say: the missing prompt is what we want it to complain about.
|
|
6915
|
-
child.stdin.end();
|
|
6916
|
-
});
|
|
6917
7529
|
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
7530
|
+
if (/unknown option.*permission-prompt-tool/i.test(said)) {
|
|
7531
|
+
log(
|
|
7532
|
+
`WARNING: ${agentCommand} does not accept --permission-prompt-tool. Sessions that ` +
|
|
7533
|
+
`need a command nobody allowed in advance will be denied outright rather than asking ` +
|
|
7534
|
+
`you. See R51 and tools/runner/README.md.`,
|
|
7535
|
+
);
|
|
7536
|
+
allGood = false;
|
|
7537
|
+
}
|
|
7538
|
+
if (said.startsWith('could not be run:')) {
|
|
7539
|
+
log(`WARNING: ${agentCommand} ${said}`);
|
|
7540
|
+
allGood = false;
|
|
7541
|
+
}
|
|
6929
7542
|
}
|
|
6930
|
-
return
|
|
7543
|
+
return allGood;
|
|
6931
7544
|
}
|
|
6932
7545
|
|
|
6933
7546
|
async function main() {
|
|
@@ -6936,6 +7549,31 @@ async function main() {
|
|
|
6936
7549
|
quiet = attaching;
|
|
6937
7550
|
|
|
6938
7551
|
const config = await readConfig();
|
|
7552
|
+
const myVersion = await cliVersion();
|
|
7553
|
+
|
|
7554
|
+
// R283. Checked before registering — a daemon too old to be safe should not
|
|
7555
|
+
// get as far as claiming a run. `/api/health` is unauthenticated, so this
|
|
7556
|
+
// works with the very token `readConfig` just resolved, before it has been
|
|
7557
|
+
// proven good for anything else. Anything BUT "the platform said refuse"
|
|
7558
|
+
// fails open: a health check that cannot be reached is not evidence the
|
|
7559
|
+
// version is wrong, and refusing to boot over a network hiccup would be R2's
|
|
7560
|
+
// failure mode pointed at the wrong target.
|
|
7561
|
+
try {
|
|
7562
|
+
const health = await api(config, '/api/health');
|
|
7563
|
+
if (isBelow(myVersion, health?.minimumCliVersion)) {
|
|
7564
|
+
throw new Error(
|
|
7565
|
+
`This CLI is ${myVersion}; ${config.url} requires at least `
|
|
7566
|
+
+ `${health.minimumCliVersion}. Update it (npm i -g ./tools from an `
|
|
7567
|
+
+ 'updated checkout, or however this machine installs cawdev) and run '
|
|
7568
|
+
+ 'cawdev again.',
|
|
7569
|
+
);
|
|
7570
|
+
}
|
|
7571
|
+
} catch (failure) {
|
|
7572
|
+
if (failure.message.startsWith('This CLI is')) {
|
|
7573
|
+
throw failure;
|
|
7574
|
+
}
|
|
7575
|
+
log(`could not check the minimum CLI version (${failure.message}); continuing`);
|
|
7576
|
+
}
|
|
6939
7577
|
|
|
6940
7578
|
const runner = await api(config, '/api/runners', {
|
|
6941
7579
|
method: 'POST',
|
|
@@ -6948,6 +7586,8 @@ async function main() {
|
|
|
6948
7586
|
// would not be a ceiling.
|
|
6949
7587
|
acceptsConsoleRules: config.acceptsRulesFromConsole === true,
|
|
6950
7588
|
grantable: JSON.stringify(declaredCeiling(config)),
|
|
7589
|
+
// R283. Own version, on every registration and heartbeat — see cliVersion below.
|
|
7590
|
+
cliVersion: myVersion,
|
|
6951
7591
|
},
|
|
6952
7592
|
});
|
|
6953
7593
|
config.runnerId = runner.id;
|
|
@@ -6960,7 +7600,7 @@ async function main() {
|
|
|
6960
7600
|
// Skipped when attaching, where the UI takes the screen a moment later and
|
|
6961
7601
|
// a banner would only flash.
|
|
6962
7602
|
if (!quiet) {
|
|
6963
|
-
console.log(bannerLines(config, ink).join('\n'));
|
|
7603
|
+
console.log(bannerLines(config, ink, myVersion).join('\n'));
|
|
6964
7604
|
}
|
|
6965
7605
|
|
|
6966
7606
|
log(`registered as "${runner.name}" (${runner.id})`);
|
|
@@ -6990,36 +7630,90 @@ async function main() {
|
|
|
6990
7630
|
// than buried under a session that then fails for a reason it explains.
|
|
6991
7631
|
await probePermissionPrompt(config);
|
|
6992
7632
|
|
|
7633
|
+
// What this daemon SAYS it is, on every hello — R52's socket, R288's rule
|
|
7634
|
+
// that it describes the current state. One object, sent as-is by
|
|
7635
|
+
// `serveControl` and mutated by the SIGHUP handler below, so a terminal that
|
|
7636
|
+
// attaches after a toggle reads what is true now.
|
|
7637
|
+
const advertised = {
|
|
7638
|
+
id: config.runnerId,
|
|
7639
|
+
name: config.name,
|
|
7640
|
+
url: config.url,
|
|
7641
|
+
// R126. Whether this machine takes rules from the console — so the
|
|
7642
|
+
// terminal only offers `M` when pressing it would do something. A key
|
|
7643
|
+
// that takes an answer the platform then refuses reads as cawdev being
|
|
7644
|
+
// broken, which is R58's rule about `a` applied to this one.
|
|
7645
|
+
acceptsConsoleRules: config.acceptsRulesFromConsole === true,
|
|
7646
|
+
projects: Object.keys(config.projects),
|
|
7647
|
+
// How many checkouts each has — R62. The per-project half of R47's
|
|
7648
|
+
// gate, which the bar shows as `cawdev 1/2`. Added rather than
|
|
7649
|
+
// replacing `projects`: an older attach ignores it and still works,
|
|
7650
|
+
// and a newer one against an older daemon simply shows a count with
|
|
7651
|
+
// nothing to compare it against.
|
|
7652
|
+
workspaces: Object.fromEntries(
|
|
7653
|
+
Object.entries(config.projects).map(([slug, p]) => [slug, p.workspaces.length]),
|
|
7654
|
+
),
|
|
7655
|
+
// R288: which agents this machine spawns, and the file it booted from,
|
|
7656
|
+
// so the attached terminal's config screen can show the state of each
|
|
7657
|
+
// toggle and write to the right file. `configPath` is null for a daemon
|
|
7658
|
+
// run from the environment alone, and the screen says so rather than
|
|
7659
|
+
// inventing a file.
|
|
7660
|
+
agents: [...config.agentCommands],
|
|
7661
|
+
configPath: config.configPath,
|
|
7662
|
+
// R81. `cawdev` starts a daemon for you when it finds none, and
|
|
7663
|
+
// quitting the UI leaves it running — it is driving sessions. A
|
|
7664
|
+
// background process you did not know you started is the cost of that
|
|
7665
|
+
// choice, so the goodbye has to name it precisely enough to stop, and
|
|
7666
|
+
// this is the only place the number is known.
|
|
7667
|
+
pid: process.pid,
|
|
7668
|
+
};
|
|
7669
|
+
|
|
7670
|
+
// R283. `cawdev config agent enable/disable` and `cawdev config rules
|
|
7671
|
+
// on/off` write the file and, if a daemon is running on it, send this — NOT
|
|
7672
|
+
// a control-socket command. `control.mjs` is deliberately read-only ("no
|
|
7673
|
+
// command can" change anything, so a person acting through it never gets
|
|
7674
|
+
// more than their own HTTPS session already grants); SIGHUP is a unix
|
|
7675
|
+
// convention for "re-read your config", asked of a process this operator
|
|
7676
|
+
// already owns, so it does not open that door. Only the settings that are
|
|
7677
|
+
// genuinely safe to hot-swap: `agentCommands`, `grantable` and
|
|
7678
|
+
// `acceptsRulesFromConsole` are read fresh from `config` on every use
|
|
7679
|
+
// (spawn, or the next heartbeat), never captured once at boot — so mutating
|
|
7680
|
+
// them here is enough, with no further plumbing.
|
|
7681
|
+
process.on('SIGHUP', async () => {
|
|
7682
|
+
const path = configPathFromArgv();
|
|
7683
|
+
try {
|
|
7684
|
+
const file = JSON.parse(await readFile(path, 'utf8'));
|
|
7685
|
+
const before = {
|
|
7686
|
+
acceptsRulesFromConsole: config.acceptsRulesFromConsole,
|
|
7687
|
+
grantable: config.grantable,
|
|
7688
|
+
agentCommands: config.agentCommands,
|
|
7689
|
+
};
|
|
7690
|
+
config.acceptsRulesFromConsole =
|
|
7691
|
+
file.acceptsRulesFromConsole ?? DEFAULTS.acceptsRulesFromConsole;
|
|
7692
|
+
config.grantable = file.grantable ?? DEFAULTS.grantable;
|
|
7693
|
+
config.agentCommands = file.agentCommands
|
|
7694
|
+
?? (file.agentCommand ? [file.agentCommand] : null)
|
|
7695
|
+
?? DEFAULTS.agentCommands;
|
|
7696
|
+
// R288: the hello describes the current state, not the booted one — an
|
|
7697
|
+
// attach opened after a toggle would otherwise offer to turn on what is
|
|
7698
|
+
// already on. The same object `serveControl` sends, mutated in place.
|
|
7699
|
+
advertised.acceptsConsoleRules = config.acceptsRulesFromConsole === true;
|
|
7700
|
+
advertised.agents = [...config.agentCommands];
|
|
7701
|
+
const changed = Object.keys(before)
|
|
7702
|
+
.filter((key) => JSON.stringify(before[key]) !== JSON.stringify(config[key]));
|
|
7703
|
+
log(changed.length
|
|
7704
|
+
? `re-read ${path} on SIGHUP — changed: ${changed.join(', ')}`
|
|
7705
|
+
: `re-read ${path} on SIGHUP — nothing changed`);
|
|
7706
|
+
} catch (failure) {
|
|
7707
|
+
log(`SIGHUP re-read of ${path} failed: ${failure.message}`);
|
|
7708
|
+
}
|
|
7709
|
+
});
|
|
7710
|
+
|
|
6993
7711
|
// R52. Nothing here is load-bearing for running an agent, so a daemon that
|
|
6994
7712
|
// cannot open a socket says so and carries on: trading the ability to run
|
|
6995
7713
|
// work for the ability to watch it would be the wrong way round.
|
|
6996
7714
|
try {
|
|
6997
7715
|
control = await serveControl({
|
|
6998
|
-
runner:
|
|
6999
|
-
id: config.runnerId,
|
|
7000
|
-
name: config.name,
|
|
7001
|
-
url: config.url,
|
|
7002
|
-
// R126. Whether this machine takes rules from the console — so the
|
|
7003
|
-
// terminal only offers `M` when pressing it would do something. A key
|
|
7004
|
-
// that takes an answer the platform then refuses reads as cawdev being
|
|
7005
|
-
// broken, which is R58's rule about `a` applied to this one.
|
|
7006
|
-
acceptsConsoleRules: config.acceptsRulesFromConsole === true,
|
|
7007
|
-
projects: Object.keys(config.projects),
|
|
7008
|
-
// How many checkouts each has — R62. The per-project half of R47's
|
|
7009
|
-
// gate, which the bar shows as `cawdev 1/2`. Added rather than
|
|
7010
|
-
// replacing `projects`: an older attach ignores it and still works,
|
|
7011
|
-
// and a newer one against an older daemon simply shows a count with
|
|
7012
|
-
// nothing to compare it against.
|
|
7013
|
-
workspaces: Object.fromEntries(
|
|
7014
|
-
Object.entries(config.projects).map(([slug, p]) => [slug, p.workspaces.length]),
|
|
7015
|
-
),
|
|
7016
|
-
// R81. `cawdev` starts a daemon for you when it finds none, and
|
|
7017
|
-
// quitting the UI leaves it running — it is driving sessions. A
|
|
7018
|
-
// background process you did not know you started is the cost of that
|
|
7019
|
-
// choice, so the goodbye has to name it precisely enough to stop, and
|
|
7020
|
-
// this is the only place the number is known.
|
|
7021
|
-
pid: process.pid,
|
|
7022
|
-
},
|
|
7716
|
+
runner: advertised,
|
|
7023
7717
|
snapshot: snapshotRuns,
|
|
7024
7718
|
});
|
|
7025
7719
|
log(`watchable at ${control.path} — attach with: node runner.mjs attach`);
|
|
@@ -7099,6 +7793,10 @@ async function main() {
|
|
|
7099
7793
|
// without anybody having to revoke anything.
|
|
7100
7794
|
acceptsConsoleRules: config.acceptsRulesFromConsole === true,
|
|
7101
7795
|
grantable: JSON.stringify(declaredCeiling(config)),
|
|
7796
|
+
// R283. Sent on every beat, not only at registration, for
|
|
7797
|
+
// `acceptsConsoleRules`' own reason: a daemon reconfigured or
|
|
7798
|
+
// reinstalled reports what is true NOW.
|
|
7799
|
+
cliVersion: myVersion,
|
|
7102
7800
|
},
|
|
7103
7801
|
}).then((me) => {
|
|
7104
7802
|
// R73/R80. The heartbeat's answer is what the platform decided about
|