@ucsandman/legcli 0.8.0 → 0.9.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/CHANGELOG.md +45 -0
- package/NOTICE +8 -0
- package/README.md +601 -560
- package/bin/fake-agent.mjs +4 -4
- package/bin/leg.mjs +21 -12
- package/docs/DECISIONS.md +20 -2
- package/docs/ERRORS.md +71 -0
- package/docs/README.md +2 -0
- package/docs/REUSE.md +1 -1
- package/docs/VOCABULARY.md +21 -0
- package/docs/board-guide.md +13 -0
- package/docs/cli-contracts.md +22 -1
- package/docs/concepts.md +42 -3
- package/docs/configuration.md +22 -1
- package/docs/faq.md +19 -0
- package/docs/getting-started.md +272 -251
- package/docs/harness.md +319 -0
- package/fixtures/verified.json +1 -1
- package/package.json +7 -3
- package/scripts/build-docs-site.mjs +11 -4
- package/scripts/check-branding.mjs +118 -0
- package/scripts/check-claims.mjs +1 -1
- package/scripts/license-sign.mjs +1 -1
- package/scripts/limits-table.mjs +1 -1
- package/scripts/live-limits.mjs +1 -1
- package/scripts/npm-publish-gate.mjs +114 -0
- package/scripts/probe.mjs +4 -3
- package/scripts/seed-fake-cards.mjs +4 -3
- package/scripts/seed-floor-board.mjs +5 -4
- package/scripts/seed-wes-board.mjs +5 -4
- package/scripts/stripe-setup.mjs +1 -1
- package/scripts/sync-harness-engine.mjs +159 -0
- package/scripts/sync-leg-agents.mjs +127 -0
- package/src/accounts.mjs +1 -2
- package/src/adapters/codex.mjs +1 -1
- package/src/attach.mjs +75 -19
- package/src/auth.mjs +2 -2
- package/src/board/board.js +3 -3
- package/src/board/sessions.js +77 -3
- package/src/bundle.mjs +54 -8
- package/src/chain.mjs +1 -1
- package/src/contract.mjs +4 -3
- package/src/fsx.mjs +5 -2
- package/src/handoff.mjs +6 -6
- package/src/harness/cli.mjs +281 -0
- package/src/harness/fingerprint.mjs +68 -0
- package/src/harness/index.mjs +407 -0
- package/src/harness/registry.mjs +124 -0
- package/src/harness/vendor/agnostic-ai/LICENSE +21 -0
- package/src/harness/vendor/agnostic-ai/UPSTREAM.json +30 -0
- package/src/harness/vendor/agnostic-ai/core/safety/guards.json +96 -0
- package/src/harness/vendor/agnostic-ai/core/templates/targets.json +252 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/README.md +199 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/apply.cjs +247 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/bundle.cjs +243 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/capture.cjs +119 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/common.cjs +375 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/index.cjs +55 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/sources/claude.cjs +330 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/sources/codex.cjs +314 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/status.cjs +171 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/targets/agy.cjs +113 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/targets/claude.cjs +158 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/targets/codex.cjs +832 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/targets/cursor.cjs +87 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/targets/gemini.cjs +128 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/targets/generic.cjs +424 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/toml.cjs +149 -0
- package/src/harness/vendor/agnostic-ai/engine/hooks/shim.cjs +431 -0
- package/src/hook.mjs +49 -49
- package/src/land.mjs +7 -35
- package/src/launcher.mjs +38 -26
- package/src/ledger.mjs +6 -6
- package/src/license.mjs +10 -9
- package/src/live-capture.mjs +1 -1
- package/src/mergequeue.mjs +5 -5
- package/src/orchestrator.mjs +28 -4
- package/src/preferences.mjs +37 -3
- package/src/redact.mjs +1 -1
- package/src/resume.mjs +17 -15
- package/src/runner.mjs +2 -2
- package/src/scheduler.mjs +1 -1
- package/src/server.mjs +38 -10
- package/src/session-detail.mjs +15 -1
- package/src/sessions.mjs +6 -3
- package/src/share.mjs +2 -2
- package/src/stations/agent.mjs +1 -1
- package/src/sync/dashclaw.mjs +4 -4
- package/src/synthesis.mjs +165 -0
- package/src/taps/agy.mjs +2 -2
- package/src/taps/claude-usage.mjs +1 -1
- package/src/taps/claude.mjs +170 -170
- package/src/taps/codex.mjs +286 -286
- package/src/taps/grok.mjs +2 -2
- package/src/trust.mjs +205 -36
- package/src/usage.mjs +5 -1
- package/src/worktree.mjs +5 -4
- package/fixtures/live/agy/attempt-1-scratch-workspace.out.log +0 -1
- package/fixtures/live/agy/err.log +0 -0
- package/fixtures/live/agy/out.log +0 -1
- package/fixtures/live/agy/supervisor.log +0 -2
- package/fixtures/live/claude/err.log +0 -0
- package/fixtures/live/claude/out.log +0 -1
- package/fixtures/live/claude/supervisor.log +0 -2
- package/fixtures/live/codex/err.log +0 -1
- package/fixtures/live/codex/out.log +0 -8
- package/fixtures/live/codex/supervisor.log +0 -2
- package/fixtures/live/grok/err.log +0 -32
- package/fixtures/live/grok/out.log +0 -7
- package/fixtures/live/grok/supervisor.log +0 -2
package/src/trust.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// trust — records the folder-trust answer each agent CLI asks for on its first
|
|
2
|
-
// run in a directory, before
|
|
2
|
+
// run in a directory, before Leg spawns that agent.
|
|
3
3
|
//
|
|
4
4
|
// Why this exists: the handoff is the product. When claude hits its 5-hour
|
|
5
|
-
// limit at 3am,
|
|
5
|
+
// limit at 3am, Leg writes the bundle and starts codex in the same terminal
|
|
6
6
|
// with nobody there. If the incoming agent has never run in that folder it
|
|
7
7
|
// stops on a full-screen "Is this a project you trust?" prompt and waits for a
|
|
8
8
|
// keypress that is not coming, and the handoff the user paid for silently
|
|
@@ -10,24 +10,25 @@
|
|
|
10
10
|
//
|
|
11
11
|
// Each CLI already stores that answer in a file, so the fix is to write the
|
|
12
12
|
// same answer the user would have clicked, for the repository they already
|
|
13
|
-
// chose by typing `
|
|
13
|
+
// chose by typing `leg claude` in it:
|
|
14
14
|
//
|
|
15
15
|
// claude ~/.claude.json projects["<repo>"].hasTrustDialogAccepted
|
|
16
16
|
// (or $CLAUDE_CONFIG_DIR/.claude.json)
|
|
17
17
|
// codex ~/.codex/config.toml [projects."<repo>"] trust_level = "trusted"
|
|
18
|
-
// agy ~/.gemini/
|
|
18
|
+
// agy ~/.gemini/antigravity-cli/settings.json trustedWorkspaces: ["<repo>", "<worktree>"]
|
|
19
|
+
// (~/.gemini/config/projects/default-cli-project.json defense in depth)
|
|
19
20
|
//
|
|
20
21
|
// For claude this is the documented remedy: its own permissions docs say to
|
|
21
22
|
// "set projects[<path>].hasTrustDialogAccepted to true in ~/.claude.json, where
|
|
22
23
|
// <path> is the repository root", and its error text prints the same sentence.
|
|
23
24
|
//
|
|
24
|
-
// Three rules this module holds to, because it writes files
|
|
25
|
+
// Three rules this module holds to, because it writes files Leg does not own:
|
|
25
26
|
// 1. Never create a config file that is not already there. A missing file
|
|
26
27
|
// means that CLI has never run here, so its own first-run flow (login,
|
|
27
28
|
// onboarding) is about to happen with the user present anyway. Skip.
|
|
28
29
|
// 2. Never rewrite a file to say what it already says. No write, no risk.
|
|
29
30
|
// 3. Never lose what is already in the file. Read, add, write atomically,
|
|
30
|
-
// under the same cross-process lock the rest of
|
|
31
|
+
// under the same cross-process lock the rest of Leg uses.
|
|
31
32
|
//
|
|
32
33
|
// BATON_TRUST=never turns all of it off; the prompts come back.
|
|
33
34
|
import { existsSync, readFileSync, writeFileSync, renameSync, unlinkSync } from 'node:fs'
|
|
@@ -37,7 +38,7 @@ import { realPath, withFileLock, writeJsonAtomic } from './fsx.mjs'
|
|
|
37
38
|
|
|
38
39
|
// The repository root is the unit of trust: claude walks up from the working
|
|
39
40
|
// directory to the repo root looking for the flag, so one record covers every
|
|
40
|
-
// worktree
|
|
41
|
+
// worktree Leg creates under <repo>/.baton-worktrees/ as well.
|
|
41
42
|
export function repoRootOf(dir) {
|
|
42
43
|
let at = realPath(dir)
|
|
43
44
|
const stop = parse(at).root
|
|
@@ -90,7 +91,7 @@ export function claudeProjectKey(repo) {
|
|
|
90
91
|
|
|
91
92
|
// The canonical key, plus the spellings Claude Code used in older versions.
|
|
92
93
|
// Older spellings are updated when they are already in the file and never
|
|
93
|
-
// created, so
|
|
94
|
+
// created, so Leg corrects a stale entry without littering the config.
|
|
94
95
|
export function projectKeys(repo, existing = {}) {
|
|
95
96
|
const key = claudeProjectKey(repo)
|
|
96
97
|
const out = [key]
|
|
@@ -100,7 +101,7 @@ export function projectKeys(repo, existing = {}) {
|
|
|
100
101
|
// The older spelling is the same path written with Windows separators, so it
|
|
101
102
|
// is recognised by its spelling rather than by asking the host to resolve it:
|
|
102
103
|
// matching on the exact string only worked on a host where a backslash is a
|
|
103
|
-
// separator, and the entry
|
|
104
|
+
// separator, and the entry Leg was meant to correct was left stale
|
|
104
105
|
// anywhere else. Nothing is created here — an entry is only ever brought up
|
|
105
106
|
// to date when it is already in the file.
|
|
106
107
|
for (const k of Object.keys(existing)) {
|
|
@@ -113,7 +114,7 @@ export function projectKeys(repo, existing = {}) {
|
|
|
113
114
|
// is what triggers the second prompt ("Allow external CLAUDE.md file imports?").
|
|
114
115
|
// Claude Code reads a CLAUDE.md from every ancestor directory of the working
|
|
115
116
|
// directory, so an import in C:\Projects\CLAUDE.md is external to a session in
|
|
116
|
-
// C:\Projects\some-repo. Returns the resolved paths, so
|
|
117
|
+
// C:\Projects\some-repo. Returns the resolved paths, so Leg can print exactly
|
|
117
118
|
// what it approved instead of approving something invisible.
|
|
118
119
|
export function externalImports(cwd) {
|
|
119
120
|
const start = realPath(cwd)
|
|
@@ -200,7 +201,7 @@ export function ensureClaudeTrust(repo, { env = process.env, cwd = repo } = {})
|
|
|
200
201
|
wrote = [...new Set(changed)]
|
|
201
202
|
})
|
|
202
203
|
if (wrote === null) return { agent: 'claude', file, wrote: [], imports, skipped: 'claude config is not readable json' }
|
|
203
|
-
if (wrote === 'declined') return { agent: 'claude', file, root, wrote: [], imports, skipped: 'you answered no for this folder;
|
|
204
|
+
if (wrote === 'declined') return { agent: 'claude', file, root, wrote: [], imports, skipped: 'you answered no for this folder; Leg leaves that answer alone' }
|
|
204
205
|
return { agent: 'claude', file, root, wrote, imports, skipped: null }
|
|
205
206
|
}
|
|
206
207
|
|
|
@@ -225,7 +226,7 @@ export function ensureCodexTrust(repo, { env = process.env } = {}) {
|
|
|
225
226
|
if (codexTrustedPaths(text).some((p) => samePath(p, root))) return
|
|
226
227
|
const block = `\n[projects."${tomlPath(root)}"]\ntrust_level = "trusted"\n`
|
|
227
228
|
// Append only: a TOML table added at the end cannot change the meaning of
|
|
228
|
-
// a table above it, and
|
|
229
|
+
// a table above it, and Leg never reformats a file it did not write.
|
|
229
230
|
writeTextAtomic(file, text.endsWith('\n') ? text + block : text + '\n' + block)
|
|
230
231
|
wrote = ['trust_level']
|
|
231
232
|
})
|
|
@@ -246,7 +247,12 @@ export function codexTrustedPaths(text) {
|
|
|
246
247
|
}
|
|
247
248
|
|
|
248
249
|
function samePath(a, b) {
|
|
249
|
-
|
|
250
|
+
if (typeof a !== 'string' || typeof b !== 'string') return false
|
|
251
|
+
const normA = a.replace(/\\/g, '/')
|
|
252
|
+
const normB = b.replace(/\\/g, '/')
|
|
253
|
+
if (process.platform === 'win32') return normA.toLowerCase() === normB.toLowerCase()
|
|
254
|
+
if (DRIVE_ABS.test(a) && DRIVE_ABS.test(b)) return normA.toLowerCase() === normB.toLowerCase()
|
|
255
|
+
return normA === normB
|
|
250
256
|
}
|
|
251
257
|
|
|
252
258
|
function writeTextAtomic(file, text) {
|
|
@@ -255,7 +261,7 @@ function writeTextAtomic(file, text) {
|
|
|
255
261
|
for (let i = 0; i < 20; i++) {
|
|
256
262
|
try { renameSync(tmp, file); return } catch (err) {
|
|
257
263
|
// Windows holds a brief lock on a file another process has open; the
|
|
258
|
-
// same retry the rest of
|
|
264
|
+
// same retry the rest of Leg uses rather than lose the config.
|
|
259
265
|
if (!['EPERM', 'EBUSY', 'EACCES', 'EEXIST'].includes(err.code)) { try { unlinkSync(tmp) } catch {} throw err }
|
|
260
266
|
const until = Date.now() + 25
|
|
261
267
|
while (Date.now() < until) { /* spin */ }
|
|
@@ -267,31 +273,194 @@ function writeTextAtomic(file, text) {
|
|
|
267
273
|
|
|
268
274
|
// ---- agy ----
|
|
269
275
|
|
|
276
|
+
export function agySettingsFile(env = process.env) {
|
|
277
|
+
if (env.ANTIGRAVITY_APP_DATA_DIR) {
|
|
278
|
+
const p = resolve(env.ANTIGRAVITY_APP_DATA_DIR)
|
|
279
|
+
return p.endsWith('settings.json') ? p : join(p, 'settings.json')
|
|
280
|
+
}
|
|
281
|
+
const base = env.GEMINI_CONFIG_DIR ? resolve(env.GEMINI_CONFIG_DIR) : join(homedir(), '.gemini')
|
|
282
|
+
if (base.endsWith('settings.json')) return base
|
|
283
|
+
if (base.endsWith('antigravity-cli')) return join(base, 'settings.json')
|
|
284
|
+
return join(base, 'antigravity-cli', 'settings.json')
|
|
285
|
+
}
|
|
286
|
+
|
|
270
287
|
export function agyTrustFile(env = process.env) {
|
|
271
|
-
const
|
|
272
|
-
|
|
288
|
+
const base = env.GEMINI_CONFIG_DIR ? resolve(env.GEMINI_CONFIG_DIR) : join(homedir(), '.gemini')
|
|
289
|
+
if (base.endsWith('default-cli-project.json')) return base
|
|
290
|
+
if (base.endsWith('projects')) return join(base, 'default-cli-project.json')
|
|
291
|
+
if (base.endsWith('config')) return join(base, 'projects', 'default-cli-project.json')
|
|
292
|
+
return join(base, 'config', 'projects', 'default-cli-project.json')
|
|
273
293
|
}
|
|
274
294
|
|
|
275
|
-
export function
|
|
276
|
-
const
|
|
295
|
+
export function agyTrustedFoldersFile(env = process.env) {
|
|
296
|
+
const base = env.GEMINI_CONFIG_DIR ? resolve(env.GEMINI_CONFIG_DIR) : join(homedir(), '.gemini')
|
|
297
|
+
if (base.endsWith('trustedFolders.json')) return base
|
|
298
|
+
return join(base, 'trustedFolders.json')
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function normalizeWorkspacePath(raw) {
|
|
302
|
+
const s = String(raw)
|
|
303
|
+
let p
|
|
304
|
+
if (DRIVE_ABS.test(s)) {
|
|
305
|
+
p = process.platform === 'win32' ? realPath(s) : s
|
|
306
|
+
p = p.replace(/\//g, '\\')
|
|
307
|
+
if (/^[a-z]:/i.test(p)) p = p[0].toUpperCase() + p.slice(1)
|
|
308
|
+
return p.length > 3 ? p.replace(/\\+$/, '') : p
|
|
309
|
+
}
|
|
310
|
+
p = realPath(s)
|
|
311
|
+
if (process.platform === 'win32') {
|
|
312
|
+
p = p.replace(/\//g, '\\')
|
|
313
|
+
if (/^[a-z]:/i.test(p)) p = p[0].toUpperCase() + p.slice(1)
|
|
314
|
+
return p.length > 3 ? p.replace(/\\+$/, '') : p
|
|
315
|
+
}
|
|
316
|
+
p = p.replace(/\\/g, '/')
|
|
317
|
+
return p.length > 1 ? p.replace(/\/+$/, '') : p
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function agyFolderUri(repo) {
|
|
277
321
|
const root = repoRootOf(repo)
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
322
|
+
let p = root.replace(/\\/g, '/')
|
|
323
|
+
if (!p.startsWith('/')) p = '/' + p
|
|
324
|
+
return `file://${p}`
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function agyUriMatches(uri, root) {
|
|
328
|
+
if (typeof uri !== 'string') return false
|
|
329
|
+
let p = uri.replace(/^file:\/\//, '').replace(/%3A/gi, ':')
|
|
330
|
+
if (/^\/[A-Za-z]:/.test(p)) p = p.slice(1)
|
|
331
|
+
return samePath(p, root)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function ensureAgyTrust(repo, { env = process.env, cwd = repo } = {}) {
|
|
335
|
+
const settingsFile = agySettingsFile(env)
|
|
336
|
+
const projectFile = agyTrustFile(env)
|
|
337
|
+
const trustedFoldersFile = agyTrustedFoldersFile(env)
|
|
338
|
+
|
|
339
|
+
const settingsDir = dirname(settingsFile)
|
|
340
|
+
const projectDir = dirname(projectFile)
|
|
341
|
+
|
|
342
|
+
if (!existsSync(settingsDir) && !existsSync(projectDir) && !existsSync(settingsFile) && !existsSync(projectFile)) {
|
|
343
|
+
return { agent: 'agy', file: settingsFile, wrote: [], imports: [], skipped: 'no agy config yet' }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const root = normalizeWorkspacePath(repoRootOf(repo))
|
|
347
|
+
const workDir = normalizeWorkspacePath(cwd || repo)
|
|
348
|
+
const targets = samePath(root, workDir) ? [root] : [root, workDir]
|
|
349
|
+
|
|
350
|
+
// Rule 4: refusal preservation. If user answered "no" in any existing config, respect it.
|
|
351
|
+
if (existsSync(projectFile)) {
|
|
352
|
+
const project = readJson(projectFile)
|
|
353
|
+
if (project?.projectResources?.resources && Array.isArray(project.projectResources.resources)) {
|
|
354
|
+
const declined = project.projectResources.resources.some((res) => {
|
|
355
|
+
const uri = res.gitFolder?.folderUri ?? res.folderUri
|
|
356
|
+
return targets.some((t) => agyUriMatches(uri, t)) && res.gitFolder?.allowWrite === false
|
|
357
|
+
})
|
|
358
|
+
if (declined) {
|
|
359
|
+
return { agent: 'agy', file: settingsFile, root, wrote: [], imports: [], skipped: 'you answered no for this folder; Leg leaves that answer alone' }
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (existsSync(trustedFoldersFile)) {
|
|
364
|
+
const tf = readJson(trustedFoldersFile)
|
|
365
|
+
if (tf && typeof tf === 'object') {
|
|
366
|
+
const declined = targets.some((t) => tf[t] === 'DENY' || tf[t] === 'DONT_TRUST' || tf[t] === false)
|
|
367
|
+
if (declined) {
|
|
368
|
+
return { agent: 'agy', file: settingsFile, root, wrote: [], imports: [], skipped: 'you answered no for this folder; Leg leaves that answer alone' }
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
281
373
|
let wrote = []
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
374
|
+
|
|
375
|
+
// 1. ~/.gemini/antigravity-cli/settings.json -> trustedWorkspaces
|
|
376
|
+
// agy checks this file on startup: workspaceTrusted iterates settings.TrustedWorkspaces
|
|
377
|
+
// and does an exact string match (runtime.memequal) against Store.workspacePath.
|
|
378
|
+
const hasSettings = existsSync(settingsFile) || existsSync(settingsDir)
|
|
379
|
+
if (hasSettings) {
|
|
380
|
+
withFileLock(`${settingsFile}.baton-lock`, () => {
|
|
381
|
+
const settings = existsSync(settingsFile) ? readJson(settingsFile) : {}
|
|
382
|
+
if (!settings || typeof settings !== 'object') { wrote = null; return }
|
|
383
|
+
const list = Array.isArray(settings.trustedWorkspaces) ? [...settings.trustedWorkspaces] : []
|
|
384
|
+
let changed = false
|
|
385
|
+
for (const target of targets) {
|
|
386
|
+
const idx = list.findIndex((item) => typeof item === 'string' && samePath(item, target))
|
|
387
|
+
if (idx === -1) {
|
|
388
|
+
list.push(target)
|
|
389
|
+
changed = true
|
|
390
|
+
} else if (list[idx] !== target) {
|
|
391
|
+
// Fix casing / separator normalization so exact memequal matches in Go
|
|
392
|
+
list[idx] = target
|
|
393
|
+
changed = true
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (changed) {
|
|
397
|
+
settings.trustedWorkspaces = list
|
|
398
|
+
writeJsonAtomic(settingsFile, settings)
|
|
399
|
+
wrote.push('trustedWorkspaces')
|
|
400
|
+
}
|
|
401
|
+
})
|
|
402
|
+
if (wrote === null) {
|
|
403
|
+
return { agent: 'agy', file: settingsFile, wrote: [], imports: [], skipped: 'agy settings file is not readable json' }
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// 2. ~/.gemini/config/projects/default-cli-project.json (Gemini project resources)
|
|
408
|
+
const hasProject = existsSync(projectFile) || existsSync(projectDir)
|
|
409
|
+
if (hasProject) {
|
|
410
|
+
withFileLock(`${projectFile}.baton-lock`, () => {
|
|
411
|
+
const project = existsSync(projectFile)
|
|
412
|
+
? readJson(projectFile)
|
|
413
|
+
: { id: 'default-cli-project', name: 'CLI Project', projectResources: { resources: [] } }
|
|
414
|
+
if (!project || typeof project !== 'object') return
|
|
415
|
+
if (!project.projectResources || typeof project.projectResources !== 'object') {
|
|
416
|
+
project.projectResources = {}
|
|
417
|
+
}
|
|
418
|
+
if (!Array.isArray(project.projectResources.resources)) {
|
|
419
|
+
project.projectResources.resources = []
|
|
420
|
+
}
|
|
421
|
+
let changed = false
|
|
422
|
+
for (const target of targets) {
|
|
423
|
+
const alreadyTrusted = project.projectResources.resources.some((res) => {
|
|
424
|
+
const uri = res.gitFolder?.folderUri ?? res.folderUri
|
|
425
|
+
return agyUriMatches(uri, target)
|
|
426
|
+
})
|
|
427
|
+
if (!alreadyTrusted) {
|
|
428
|
+
project.projectResources.resources.push({
|
|
429
|
+
gitFolder: {
|
|
430
|
+
folderUri: agyFolderUri(target),
|
|
431
|
+
defaultBranch: 'main',
|
|
432
|
+
allowWrite: true,
|
|
433
|
+
},
|
|
434
|
+
})
|
|
435
|
+
changed = true
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (changed) {
|
|
439
|
+
writeJsonAtomic(projectFile, project)
|
|
440
|
+
wrote.push('gitFolder')
|
|
441
|
+
}
|
|
442
|
+
})
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// 3. ~/.gemini/trustedFolders.json (legacy / extension support)
|
|
446
|
+
if (existsSync(trustedFoldersFile)) {
|
|
447
|
+
try {
|
|
448
|
+
withFileLock(`${trustedFoldersFile}.baton-lock`, () => {
|
|
449
|
+
const tf = readJson(trustedFoldersFile) || {}
|
|
450
|
+
let changed = false
|
|
451
|
+
for (const target of targets) {
|
|
452
|
+
if (tf[target] !== 'TRUST_FOLDER') {
|
|
453
|
+
tf[target] = 'TRUST_FOLDER'
|
|
454
|
+
changed = true
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (changed) writeJsonAtomic(trustedFoldersFile, tf)
|
|
458
|
+
})
|
|
459
|
+
} catch {}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const primaryFile = hasSettings ? settingsFile : projectFile
|
|
463
|
+
return { agent: 'agy', file: primaryFile, root, wrote, imports: [], skipped: null }
|
|
295
464
|
}
|
|
296
465
|
|
|
297
466
|
// ---- the one entry point ----
|
|
@@ -306,13 +475,13 @@ export function ensureTrust(agent, repo, { env = process.env, cwd = repo } = {})
|
|
|
306
475
|
try {
|
|
307
476
|
return fn(repo, { env, cwd })
|
|
308
477
|
} catch (err) {
|
|
309
|
-
// A trust record
|
|
478
|
+
// A trust record Leg could not write is a prompt the user will answer
|
|
310
479
|
// themselves. It is never a reason to fail the session.
|
|
311
480
|
return { agent, wrote: [], imports: [], skipped: `could not write the trust record: ${err.message}` }
|
|
312
481
|
}
|
|
313
482
|
}
|
|
314
483
|
|
|
315
|
-
// The line
|
|
484
|
+
// The line Leg prints. Null when there is nothing worth saying.
|
|
316
485
|
export function trustLine(result) {
|
|
317
486
|
if (!result || !result.wrote?.length) return null
|
|
318
487
|
const what = result.agent === 'claude' && result.imports?.length
|
package/src/usage.mjs
CHANGED
|
@@ -158,10 +158,14 @@ export function candidates({ agent, account = 'default', accounts, order = AGENT
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
// → { next: {agent, account} | null, out: [{agent, account, resets_at}] sorted by reset }
|
|
161
|
-
|
|
161
|
+
// `exclude` names (agent, account) pairs this choice must skip: a destination
|
|
162
|
+
// the strict harness policy refused is neither available nor out, it is off
|
|
163
|
+
// the list for this hand-off.
|
|
164
|
+
export function chooseNext({ agent, account, accounts, installed, order = AGENTS, nowS = Math.floor(Date.now() / 1000), exclude = [] }) {
|
|
162
165
|
const out = []
|
|
163
166
|
for (const c of candidates({ agent, account, accounts, order })) {
|
|
164
167
|
if (installed && installed[c.agent] === false) continue
|
|
168
|
+
if (exclude.some((x) => x.agent === c.agent && x.account === c.account)) continue
|
|
165
169
|
const u = readUsage(c.agent, c.account)
|
|
166
170
|
if (isAvailable(u, nowS)) return { next: c, out }
|
|
167
171
|
out.push({ ...c, resets_at: u.limited_until, reason: u.limited_reason })
|
package/src/worktree.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
// worktree — one git worktree per card under <repo>/.
|
|
2
|
-
// on branch
|
|
1
|
+
// worktree — one git worktree per card under <repo>/.leg-worktrees/<cardId>,
|
|
2
|
+
// on branch leg/<cardId> (legacy .baton-worktrees/ and baton/<id> still
|
|
3
|
+
// resolved). Every git call runs with MSYS_NO_PATHCONV=1 (see
|
|
3
4
|
// git-snapshot.mjs) so Git Bash on Windows never rewrites an absolute path
|
|
4
|
-
// argument. Never pushes; never deletes outside
|
|
5
|
+
// argument. Never pushes; never deletes outside those worktree dirs.
|
|
5
6
|
import { execFileSync } from 'node:child_process'
|
|
6
7
|
import { existsSync, statSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
7
8
|
import { join, resolve, sep, isAbsolute } from 'node:path'
|
|
@@ -85,7 +86,7 @@ export function list(repo) {
|
|
|
85
86
|
return parseWorktreeList(out)
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
// Never committed by a landing:
|
|
89
|
+
// Never committed by a landing: Leg's own directories, and the local state a
|
|
89
90
|
// DashClaw hook writes into every directory an agent runs in.
|
|
90
91
|
export function ensureExcludeEntries(repo) {
|
|
91
92
|
// a linked worktree or submodule has `.git` as a FILE, so the shared info dir
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"conversation_id":"f7ff607d-9831-4f28-83d2-f50fb07ad8a1","status":"SUCCESS","response":"Done. Both files created in `~\\.gemini\\antigravity-cli\\scratch\\`:\n\n- [hello-agy.txt](file:///~/.gemini/antigravity-cli/scratch/hello-agy.txt) — contains `hi`\n- [.baton/DONE](file:///~/.gemini/antigravity-cli/scratch/.baton/DONE) — contains `done`\n","duration_seconds":17.670068,"num_turns":1,"usage":{"input_tokens":39133,"output_tokens":542,"thinking_tokens":0,"cache_read_tokens":37387,"total_tokens":39675}}
|
|
File without changes
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"conversation_id":"e36f7b41-8a0f-4666-a690-b49062d53de0","status":"SUCCESS","response":"Done. Created [hello-agy.txt](file:///~/AppData/Local/Temp/claude/C--Projects/49952fac-5ff4-486c-8395-9973cebdfdcb/scratchpad/toy-probe/hello-agy.txt) and [.baton/DONE](file:///~/AppData/Local/Temp/claude/C--Projects/49952fac-5ff4-486c-8395-9973cebdfdcb/scratchpad/toy-probe/.baton/DONE).\n","duration_seconds":36.3828431,"num_turns":1,"usage":{"input_tokens":39475,"output_tokens":600,"thinking_tokens":0,"cache_read_tokens":37477,"total_tokens":40075}}
|
|
File without changes
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"duration_api_ms":7804,"stop_reason":"end_turn","session_id":"d9e19d78-1502-4f88-8ded-81c5bbbda8c2","total_cost_usd":1.0015022500000001,"usage":{"input_tokens":34,"cache_creation_input_tokens":48486,"cache_read_input_tokens":47217,"output_tokens":372,"output_tokens_details":{"thinking_tokens":0},"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":48486,"ephemeral_5m_input_tokens":0},"inference_geo":"not_available","iterations":[{"input_tokens":32,"output_tokens":45,"cache_read_input_tokens":47217,"cache_creation_input_tokens":1269,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":1269},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-haiku-4-5-20251001":{"inputTokens":948,"outputTokens":18,"cacheReadInputTokens":0,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.0010379999999999999,"contextWindow":200000,"maxOutputTokens":32000,"thinkingTokens":0,"canonicalModel":"claude-haiku-4-5","provider":"firstParty","costBasis":"list"},"claude-fable-5-1":{"inputTokens":34,"outputTokens":372,"cacheReadInputTokens":47217,"cacheCreationInputTokens":48486,"webSearchRequests":0,"costUSD":1.00046425,"contextWindow":1000000,"maxOutputTokens":64000,"thinkingTokens":0,"canonicalModel":"claude-fable-5-1","provider":"firstParty","costBasis":"list"}},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","fast_mode_disabled_reason":"sdk_opt_in_required","subagent_stats":{"spawned":0,"requested":{"background":0,"foreground":0,"unset":0},"started_in_background":0,"max_depth":0,"spawned_by_subagents":0,"completed":0,"failed":0,"killed":{"parent":0,"user":0,"system":0},"refused":{"depth_limit":0,"concurrency_limit":0,"budget":0},"by_type":{}},"is_error":false,"num_turns":3,"subtype":"success","api_error_status":null,"result":"Done. Both files exist:\n\n- `hello-claude.txt` contains `hi`\n- `.baton/DONE` contains `done`","ttft_ms":2791,"type":"result","duration_ms":11899,"uuid":"d70f3d4c-5a7f-4675-a734-d05edf37b1ed","ttft_stream_ms":1846,"time_to_request_ms":338,"first_content_frame_ms":1846,"queued_turn_count":0,"result_index":0}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Reading additional input from stdin...
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
{"type":"thread.started","thread_id":"01a08de2-7611-7ff1-b0da-1e6e2cb7ca90"}
|
|
2
|
-
{"type":"turn.started"}
|
|
3
|
-
{"type":"item.completed","item":{"id":"item_0","type":"error","message":"Skill descriptions were shortened to fit the skills context budget. Codex can still see every skill, but some descriptions are shorter. Disable unused skills or plugins to leave more room for the rest."}}
|
|
4
|
-
{"type":"item.completed","item":{"id":"item_1","type":"agent_message","text":"`New-Item -ItemType File -Path hello-codex.txt -Value hi -Force` and `Set-Content` via `New-Item` in `.baton` (creating it only if missing), then writing `.baton/DONE` as `done`; I’m applying it now."}}
|
|
5
|
-
{"type":"item.started","item":{"id":"item_2","type":"command_execution","command":"\"~\\\\AppData\\\\Local\\\\Microsoft\\\\WindowsApps\\\\pwsh.exe\" -Command '$ErrorActionPreference = '\"'Stop'; Set-Content -Path .\\\\hello-codex.txt -NoNewline -Value 'hi'; if (-not (Test-Path .\\\\.baton)) { New-Item -ItemType Directory -Path .\\\\.baton | Out-Null }; Set-Content -Path .\\\\.baton\\\\DONE -NoNewline -Value 'done'\"","aggregated_output":"","exit_code":null,"status":"in_progress"}}
|
|
6
|
-
{"type":"item.completed","item":{"id":"item_2","type":"command_execution","command":"\"~\\\\AppData\\\\Local\\\\Microsoft\\\\WindowsApps\\\\pwsh.exe\" -Command '$ErrorActionPreference = '\"'Stop'; Set-Content -Path .\\\\hello-codex.txt -NoNewline -Value 'hi'; if (-not (Test-Path .\\\\.baton)) { New-Item -ItemType Directory -Path .\\\\.baton | Out-Null }; Set-Content -Path .\\\\.baton\\\\DONE -NoNewline -Value 'done'\"","aggregated_output":"","exit_code":0,"status":"completed"}}
|
|
7
|
-
{"type":"item.completed","item":{"id":"item_3","type":"agent_message","text":"Done. \n`hello-codex.txt` now contains exactly `hi`, and `.baton/DONE` contains exactly `done`."}}
|
|
8
|
-
{"type":"turn.completed","usage":{"input_tokens":58585,"cached_input_tokens":10880,"cache_write_input_tokens":0,"output_tokens":853,"reasoning_output_tokens":650}}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
To sign in, open this URL in your browser:
|
|
3
|
-
|
|
4
|
-
https://accounts.x.ai/oauth2/device?user_code=S3S6-5YEF
|
|
5
|
-
|
|
6
|
-
Confirm this code in your browser:
|
|
7
|
-
|
|
8
|
-
S3S6-5YEF
|
|
9
|
-
|
|
10
|
-
[90mOnly continue with a code you requested. Don't share it with anyone.[0m
|
|
11
|
-
|
|
12
|
-
Waiting for authorization...
|
|
13
|
-
[2m2026-09-11T00:37:22.879885Z[0m [31mERROR[0m Skipping MCP tool with invalid name [3mtool_name[0m[2m=[0mmole__mole.aggregate [3mserver[0m[2m=[0mmole [3mreason[0m[2m=[0mtool name 'mole__mole.aggregate' is invalid — must match ^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$ (start with letter/underscore, max 64 chars)
|
|
14
|
-
[2m2026-09-11T00:37:22.879980Z[0m [31mERROR[0m Skipping MCP tool with invalid name [3mtool_name[0m[2m=[0mmole__mole.citations [3mserver[0m[2m=[0mmole [3mreason[0m[2m=[0mtool name 'mole__mole.citations' is invalid — must match ^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$ (start with letter/underscore, max 64 chars)
|
|
15
|
-
[2m2026-09-11T00:37:22.880166Z[0m [31mERROR[0m Skipping MCP tool with invalid name [3mtool_name[0m[2m=[0mmole__mole.claim_add [3mserver[0m[2m=[0mmole [3mreason[0m[2m=[0mtool name 'mole__mole.claim_add' is invalid — must match ^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$ (start with letter/underscore, max 64 chars)
|
|
16
|
-
[2m2026-09-11T00:37:22.880194Z[0m [31mERROR[0m Skipping MCP tool with invalid name [3mtool_name[0m[2m=[0mmole__mole.claims_list [3mserver[0m[2m=[0mmole [3mreason[0m[2m=[0mtool name 'mole__mole.claims_list' is invalid — must match ^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$ (start with letter/underscore, max 64 chars)
|
|
17
|
-
[2m2026-09-11T00:37:22.880215Z[0m [31mERROR[0m Skipping MCP tool with invalid name [3mtool_name[0m[2m=[0mmole__mole.connect_list [3mserver[0m[2m=[0mmole [3mreason[0m[2m=[0mtool name 'mole__mole.connect_list' is invalid — must match ^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$ (start with letter/underscore, max 64 chars)
|
|
18
|
-
[2m2026-09-11T00:37:22.880237Z[0m [31mERROR[0m Skipping MCP tool with invalid name [3mtool_name[0m[2m=[0mmole__mole.dataset [3mserver[0m[2m=[0mmole [3mreason[0m[2m=[0mtool name 'mole__mole.dataset' is invalid — must match ^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$ (start with letter/underscore, max 64 chars)
|
|
19
|
-
[2m2026-09-11T00:37:22.880264Z[0m [31mERROR[0m Skipping MCP tool with invalid name [3mtool_name[0m[2m=[0mmole__mole.edge_add [3mserver[0m[2m=[0mmole [3mreason[0m[2m=[0mtool name 'mole__mole.edge_add' is invalid — must match ^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$ (start with letter/underscore, max 64 chars)
|
|
20
|
-
[2m2026-09-11T00:37:22.880280Z[0m [31mERROR[0m Skipping MCP tool with invalid name [3mtool_name[0m[2m=[0mmole__mole.fetch [3mserver[0m[2m=[0mmole [3mreason[0m[2m=[0mtool name 'mole__mole.fetch' is invalid — must match ^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$ (start with letter/underscore, max 64 chars)
|
|
21
|
-
[2m2026-09-11T00:37:22.880298Z[0m [31mERROR[0m Skipping MCP tool with invalid name [3mtool_name[0m[2m=[0mmole__mole.pairs_candidates [3mserver[0m[2m=[0mmole [3mreason[0m[2m=[0mtool name 'mole__mole.pairs_candidates' is invalid — must match ^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$ (start with letter/underscore, max 64 chars)
|
|
22
|
-
[2m2026-09-11T00:37:22.880319Z[0m [31mERROR[0m Skipping MCP tool with invalid name [3mtool_name[0m[2m=[0mmole__mole.rows_add [3mserver[0m[2m=[0mmole [3mreason[0m[2m=[0mtool name 'mole__mole.rows_add' is invalid — must match ^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$ (start with letter/underscore, max 64 chars)
|
|
23
|
-
[2m2026-09-11T00:37:22.880339Z[0m [31mERROR[0m Skipping MCP tool with invalid name [3mtool_name[0m[2m=[0mmole__mole.search [3mserver[0m[2m=[0mmole [3mreason[0m[2m=[0mtool name 'mole__mole.search' is invalid — must match ^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$ (start with letter/underscore, max 64 chars)
|
|
24
|
-
[2m2026-09-11T00:37:22.880354Z[0m [31mERROR[0m Skipping MCP tool with invalid name [3mtool_name[0m[2m=[0mmole__mole.session_close [3mserver[0m[2m=[0mmole [3mreason[0m[2m=[0mtool name 'mole__mole.session_close' is invalid — must match ^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$ (start with letter/underscore, max 64 chars)
|
|
25
|
-
[2m2026-09-11T00:37:22.880412Z[0m [31mERROR[0m Skipping MCP tool with invalid name [3mtool_name[0m[2m=[0mmole__mole.session_open [3mserver[0m[2m=[0mmole [3mreason[0m[2m=[0mtool name 'mole__mole.session_open' is invalid — must match ^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$ (start with letter/underscore, max 64 chars)
|
|
26
|
-
[2m2026-09-11T00:37:22.880437Z[0m [31mERROR[0m Skipping MCP tool with invalid name [3mtool_name[0m[2m=[0mmole__mole.verify_quote [3mserver[0m[2m=[0mmole [3mreason[0m[2m=[0mtool name 'mole__mole.verify_quote' is invalid — must match ^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$ (start with letter/underscore, max 64 chars)
|
|
27
|
-
[2m2026-09-11T00:37:22.880460Z[0m [31mERROR[0m Skipping MCP tool with invalid name [3mtool_name[0m[2m=[0mmole__research.ask [3mserver[0m[2m=[0mmole [3mreason[0m[2m=[0mtool name 'mole__research.ask' is invalid — must match ^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$ (start with letter/underscore, max 64 chars)
|
|
28
|
-
[2m2026-09-11T00:37:22.880482Z[0m [31mERROR[0m Skipping MCP tool with invalid name [3mtool_name[0m[2m=[0mmole__research.cancel [3mserver[0m[2m=[0mmole [3mreason[0m[2m=[0mtool name 'mole__research.cancel' is invalid — must match ^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$ (start with letter/underscore, max 64 chars)
|
|
29
|
-
[2m2026-09-11T00:37:22.880502Z[0m [31mERROR[0m Skipping MCP tool with invalid name [3mtool_name[0m[2m=[0mmole__research.report [3mserver[0m[2m=[0mmole [3mreason[0m[2m=[0mtool name 'mole__research.report' is invalid — must match ^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$ (start with letter/underscore, max 64 chars)
|
|
30
|
-
[2m2026-09-11T00:37:22.880519Z[0m [31mERROR[0m Skipping MCP tool with invalid name [3mtool_name[0m[2m=[0mmole__research.result [3mserver[0m[2m=[0mmole [3mreason[0m[2m=[0mtool name 'mole__research.result' is invalid — must match ^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$ (start with letter/underscore, max 64 chars)
|
|
31
|
-
[2m2026-09-11T00:37:22.880541Z[0m [31mERROR[0m Skipping MCP tool with invalid name [3mtool_name[0m[2m=[0mmole__research.sessions.list [3mserver[0m[2m=[0mmole [3mreason[0m[2m=[0mtool name 'mole__research.sessions.list' is invalid — must match ^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$ (start with letter/underscore, max 64 chars)
|
|
32
|
-
[2m2026-09-11T00:37:22.880557Z[0m [31mERROR[0m Skipping MCP tool with invalid name [3mtool_name[0m[2m=[0mmole__research.status [3mserver[0m[2m=[0mmole [3mreason[0m[2m=[0mtool name 'mole__research.status' is invalid — must match ^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$ (start with letter/underscore, max 64 chars)
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"text": "Creating `hello-grok.txt` and `.baton/DONE`.",
|
|
3
|
-
"stopReason": "Cancelled",
|
|
4
|
-
"sessionId": "01a08de5-c64c-7a31-ac58-1c11c2dd7332",
|
|
5
|
-
"requestId": "fb815c5a-2e96-47c4-8bcc-30669f38e844",
|
|
6
|
-
"thought": "The user wants me to:\n\n1. Create a file named hello-grok.txt in the current directory containing exactly the word hi"
|
|
7
|
-
}
|