@ucsandman/legcli 0.8.0 → 0.10.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 +121 -0
- package/NOTICE +8 -0
- package/README.md +639 -560
- package/bin/fake-agent.mjs +4 -4
- package/bin/leg.mjs +43 -12
- package/docs/DECISIONS.md +20 -2
- package/docs/ERRORS.md +205 -0
- package/docs/README.md +5 -1
- package/docs/REUSE.md +1 -1
- package/docs/VOCABULARY.md +22 -0
- package/docs/board-guide.md +33 -1
- package/docs/cli-contracts.md +36 -1
- package/docs/concepts.md +42 -3
- package/docs/configuration.md +23 -1
- package/docs/faq.md +19 -0
- package/docs/getting-started.md +272 -251
- package/docs/harness.md +319 -0
- package/docs/history.md +172 -0
- package/docs/runtime-tap.md +156 -0
- package/fixtures/verified.json +1 -1
- package/package.json +7 -3
- package/scripts/build-docs-site.mjs +18 -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 +6 -4
- package/src/adapters/codex.mjs +1 -1
- package/src/attach.mjs +125 -23
- package/src/auth.mjs +2 -2
- package/src/board/board.css +23 -1
- package/src/board/board.js +17 -5
- package/src/board/history.js +377 -0
- package/src/board/index.html +33 -0
- package/src/board/sessions.js +95 -7
- 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/history/cli.mjs +159 -0
- package/src/history/common.mjs +119 -0
- package/src/history/index.mjs +429 -0
- package/src/history/providers/agy.mjs +91 -0
- package/src/history/providers/claude.mjs +161 -0
- package/src/history/providers/codex.mjs +133 -0
- package/src/history/providers/copilot.mjs +94 -0
- package/src/history/providers/grok.mjs +138 -0
- package/src/history/worktrees.mjs +116 -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 +24 -6
- package/src/resume.mjs +17 -15
- package/src/runner.mjs +2 -2
- package/src/scheduler.mjs +1 -1
- package/src/server.mjs +224 -18
- package/src/session-detail.mjs +15 -1
- package/src/sessions.mjs +15 -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 +177 -170
- package/src/taps/codex.mjs +286 -286
- package/src/taps/grok.mjs +2 -2
- package/src/taps/mod.mjs +340 -0
- package/src/trust.mjs +205 -36
- package/src/usage.mjs +5 -1
- package/src/worktree.mjs +6 -5
- 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/session-detail.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { scrub } from './redact.mjs'
|
|
|
11
11
|
import { transcriptTail as claudeTail } from './taps/claude.mjs'
|
|
12
12
|
import { transcriptTail as codexTail } from './taps/codex.mjs'
|
|
13
13
|
import { resumeVerdict, verdictForBoard } from './resume.mjs'
|
|
14
|
+
import { readHistory } from './harness/index.mjs'
|
|
14
15
|
|
|
15
16
|
export const MESSAGE_LIMIT = 8
|
|
16
17
|
export const DIFF_MAX_LINES = 400
|
|
@@ -41,7 +42,7 @@ export function sessionMessages(session, limit = MESSAGE_LIMIT) {
|
|
|
41
42
|
if (!path) return []
|
|
42
43
|
const tail = session.agent === 'claude' ? claudeTail(path, limit)
|
|
43
44
|
: session.agent === 'codex' ? codexTail(path, limit)
|
|
44
|
-
: [] // agy keeps no transcript
|
|
45
|
+
: [] // agy keeps no transcript Leg can read
|
|
45
46
|
return tail.map((m) => ({ role: m.role === 'user' ? 'user' : 'assistant', text: scrub(m.text), ts: m.ts ?? null }))
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -116,6 +117,18 @@ function resumeFor(session) {
|
|
|
116
117
|
try { return verdictForBoard(resumeVerdict(root)) } catch { return null }
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
// The harness the leg now running was given: what the session recorded when
|
|
121
|
+
// the leg started, plus every capture, sync and decision the harness trail
|
|
122
|
+
// holds for this session. Paths in the attention list are local; a guest never
|
|
123
|
+
// reaches the drawer.
|
|
124
|
+
function harnessFor(session) {
|
|
125
|
+
const h = session.harness ?? null
|
|
126
|
+
let history = []
|
|
127
|
+
try { history = readHistory(200).filter((r) => r.session_id === session.session_id).slice(-20) } catch { /* the drawer renders without the trail */ }
|
|
128
|
+
if (!h && !history.length) return null
|
|
129
|
+
return { ...(h ?? {}), history }
|
|
130
|
+
}
|
|
131
|
+
|
|
119
132
|
export function sessionDetail(session) {
|
|
120
133
|
return {
|
|
121
134
|
session_id: session.session_id,
|
|
@@ -124,6 +137,7 @@ export function sessionDetail(session) {
|
|
|
124
137
|
events: readEvents(session.session_id).slice(-EVENT_LIMIT),
|
|
125
138
|
bundle: session.bundle ?? null,
|
|
126
139
|
resume: resumeFor(session),
|
|
140
|
+
harness: harnessFor(session),
|
|
127
141
|
ts: new Date().toISOString(),
|
|
128
142
|
}
|
|
129
143
|
}
|
package/src/sessions.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// sessions — the store behind `
|
|
1
|
+
// sessions — the store behind `leg claude|codex|agy`. One directory per
|
|
2
2
|
// interactive terminal session under $BATON_HOME/sessions/<id>/:
|
|
3
3
|
// session.json the live record the board renders (atomic writes)
|
|
4
4
|
// events.jsonl timeline (started, turn, warning, limit, handoff, ended)
|
|
@@ -44,7 +44,7 @@ export function listSessions() {
|
|
|
44
44
|
|
|
45
45
|
export function isActive(s) { return ACTIVE.includes(s?.status) }
|
|
46
46
|
|
|
47
|
-
// The checkout this session's files live in: its own worktree when
|
|
47
|
+
// The checkout this session's files live in: its own worktree when Leg gave
|
|
48
48
|
// it one (repo stays the main checkout, for grouping and landing), else the repo.
|
|
49
49
|
export function workRoot(s) { return s?.worktree?.path ?? s?.repo ?? s?.cwd ?? null }
|
|
50
50
|
|
|
@@ -58,10 +58,13 @@ export function createSession({ id, agent, account = 'default', cwd, repo = null
|
|
|
58
58
|
task: null, turns: 0,
|
|
59
59
|
files_touched: [], files_dirty: [], head: null, head_at_start: null,
|
|
60
60
|
limits: null, limit: null, warning: null,
|
|
61
|
-
bundle: null, handoff: null, chain,
|
|
61
|
+
bundle: null, handoff: null, chain, checkpoints: [],
|
|
62
62
|
handoff_order: normalizeHandoffOrder(handoffOrder), installed,
|
|
63
63
|
runtime_capabilities: [...new Set(runtimeCapabilities)],
|
|
64
64
|
lineage: { from: null, to: null },
|
|
65
|
+
// the portable-harness outcome for the leg now running (src/harness/index.mjs);
|
|
66
|
+
// null until the feature is enabled and a leg has been prepared
|
|
67
|
+
harness: null,
|
|
65
68
|
exit_code: null,
|
|
66
69
|
}
|
|
67
70
|
mkdirSync(sessionDir(id), { recursive: true })
|
|
@@ -82,6 +85,15 @@ export function updateSession(id, patch, { event } = {}) {
|
|
|
82
85
|
const delta = typeof patch === 'function' ? patch(cur) : patch
|
|
83
86
|
const next = { ...cur, ...delta, updated_at: now() }
|
|
84
87
|
if (delta.limits && cur.limits) next.limits = { ...cur.limits, ...delta.limits }
|
|
88
|
+
// every agent conversation this session has been: a hand-off overwrites
|
|
89
|
+
// agent_session_id with the next agent's, and history (src/history) still
|
|
90
|
+
// needs to know the earlier legs were this session's too
|
|
91
|
+
if (delta.agent_session_id && delta.agent_session_id !== cur.agent_session_id) {
|
|
92
|
+
const seen = cur.agent_sessions ?? []
|
|
93
|
+
if (!seen.some((x) => x.agent === next.agent && x.agent_session_id === delta.agent_session_id)) {
|
|
94
|
+
next.agent_sessions = [...seen, { agent: next.agent, agent_session_id: delta.agent_session_id, transcript_path: delta.transcript_path ?? null, at: now() }].slice(-24)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
85
97
|
writeJsonAtomic(join(sessionDir(id), 'session.json'), next)
|
|
86
98
|
if (event) appendEvent(id, event)
|
|
87
99
|
return next
|
package/src/share.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// share — optional multiplayer for the board, off until `
|
|
1
|
+
// share — optional multiplayer for the board, off until `leg share on`
|
|
2
2
|
// writes $BATON_HOME/share.json. With it on, the board binds the Tailscale or
|
|
3
3
|
// LAN address, every human has a name and their own token (kept as a sha256
|
|
4
|
-
// hash: a token is printed once and `
|
|
4
|
+
// hash: a token is printed once and `leg share rotate` issues a new one),
|
|
5
5
|
// and every session belongs to the human whose terminal started it
|
|
6
6
|
// (`BATON_PERSON`, else the owner). With it off nothing changes: loopback is
|
|
7
7
|
// open and `BATON_TOKEN` is the only token.
|
package/src/stations/agent.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Station kind: agent. One chain leg runs here (or, after `
|
|
1
|
+
// Station kind: agent. One chain leg runs here (or, after `leg down` or a
|
|
2
2
|
// crashed server, the orchestrator re-attaches to the run it left behind),
|
|
3
3
|
// the run is settled, and its verdict is applied to the card. `ops` are the
|
|
4
4
|
// orchestrator's helpers; this module never imports the orchestrator.
|
package/src/sync/dashclaw.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// dashclaw sync — records every ledger event as a DashClaw action over native
|
|
2
|
-
// http/https (LESSONS 07-12: no global fetch). Off unless
|
|
2
|
+
// http/https (LESSONS 07-12: no global fetch). Off unless LEG_SYNC_DASHCLAW=1
|
|
3
3
|
// and DASHCLAW_URL + DASHCLAW_API_KEY are set. Field names come from
|
|
4
4
|
// DashClaw's validator (app/lib/validate.js ACTION_RECORD_SCHEMA): agent_id,
|
|
5
5
|
// action_type and declared_goal are required; status is one of running,
|
|
@@ -26,11 +26,11 @@ const STATUS_BY_TYPE = {
|
|
|
26
26
|
|
|
27
27
|
// One ledger event → one action record (the request body).
|
|
28
28
|
export function actionForEvent(ev, card = null) {
|
|
29
|
-
const actor = ev.actor ?? { type: '
|
|
30
|
-
const agent = actor.type === 'agent' ? `
|
|
29
|
+
const actor = ev.actor ?? { type: 'leg' }
|
|
30
|
+
const agent = actor.type === 'agent' ? `leg/${actor.adapter}` : actor.type === 'human' ? `leg/human:${actor.id}` : 'leg'
|
|
31
31
|
const body = {
|
|
32
32
|
agent_id: agent,
|
|
33
|
-
action_type: `
|
|
33
|
+
action_type: `leg_${ev.type}`,
|
|
34
34
|
declared_goal: String(ev.summary ?? ev.type).slice(0, 2000),
|
|
35
35
|
status: STATUS_BY_TYPE[ev.type] ?? 'completed',
|
|
36
36
|
reversible: true,
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// synthesis — the agent-maintained judgment record included in the handoff bundle.
|
|
2
|
+
// Spec: Leg Handoff Synthesis Layer — build spec v1
|
|
3
|
+
// File: .leg/SYNTHESIS-<session-id>.md
|
|
4
|
+
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
|
|
7
|
+
export const MAX_SYNTHESIS_BYTES = 4096
|
|
8
|
+
export const TRUNCATED_MARKER = '[synthesis truncated]'
|
|
9
|
+
export const INVALID_HEADER_PREFIX = '[synthesis header invalid, rendering body as-is]'
|
|
10
|
+
export const SYNTHESIS_POINTER_PARAGRAPH = "Read the Synthesis section first if present. Treat 'Ruled out' as settled: do not retry a ruled-out approach unless you have new evidence it was wrong. Start from the top-ranked Next step unless the repo state contradicts it."
|
|
11
|
+
|
|
12
|
+
export const SYNTHESIS_SECTIONS = [
|
|
13
|
+
'## Ruled out',
|
|
14
|
+
'## Decisions',
|
|
15
|
+
'## Next steps',
|
|
16
|
+
'## Open questions',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
// The synthesis file path for a session in its checkout/worktree root
|
|
20
|
+
export function synthesisFile(cwd, id) {
|
|
21
|
+
if (!cwd || !id) return null
|
|
22
|
+
const leg = join(cwd, '.leg', `SYNTHESIS-${id}.md`)
|
|
23
|
+
const baton = join(cwd, '.baton', `SYNTHESIS-${id}.md`)
|
|
24
|
+
if (existsSync(leg)) return leg
|
|
25
|
+
if (existsSync(baton)) return baton
|
|
26
|
+
if (existsSync(join(cwd, '.baton')) && !existsSync(join(cwd, '.leg'))) return baton
|
|
27
|
+
return leg
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Safely read the synthesis file; returns null if absent, empty, or unreadable
|
|
31
|
+
export function readSynthesis(cwd, id) {
|
|
32
|
+
if (!cwd || !id) return null
|
|
33
|
+
const file = synthesisFile(cwd, id)
|
|
34
|
+
try {
|
|
35
|
+
if (!existsSync(file)) return null
|
|
36
|
+
const text = readFileSync(file, 'utf8')
|
|
37
|
+
if (!text || !text.trim()) return null
|
|
38
|
+
return text
|
|
39
|
+
} catch {
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The standing directive for Leg per-session instructions
|
|
45
|
+
export function synthesisDirective(sessionId = '<session-id>') {
|
|
46
|
+
return `Maintain .leg/SYNTHESIS-${sessionId}.md using the schema in section 4. Update it whenever you rule out an approach, make a consequential decision, or change direction. Keep each section to 5 bullets max, one line per bullet.`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Validates the 2-line header per schema v1:
|
|
50
|
+
// synthesis_version: 1
|
|
51
|
+
// session: <session-id> updated: <ISO-8601 UTC>
|
|
52
|
+
export function validateSynthesisHeader(text, { sessionId = null } = {}) {
|
|
53
|
+
if (typeof text !== 'string') return { valid: false, reason: 'content is not a string' }
|
|
54
|
+
const lines = text.split(/\r?\n/)
|
|
55
|
+
if (lines.length < 2) return { valid: false, reason: 'fewer than 2 lines' }
|
|
56
|
+
|
|
57
|
+
const l0 = lines[0].trim()
|
|
58
|
+
if (!/^synthesis_version:\s*1$/.test(l0)) {
|
|
59
|
+
return { valid: false, reason: 'missing or invalid synthesis_version: 1' }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const l1 = lines[1].trim()
|
|
63
|
+
const m = /^session:\s*(\S+)\s+updated:\s*(\S+)$/.exec(l1)
|
|
64
|
+
if (!m) {
|
|
65
|
+
return { valid: false, reason: 'missing or invalid session/updated header line' }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const [, sessId, updated] = m
|
|
69
|
+
if (sessionId && sessId !== sessionId) {
|
|
70
|
+
return { valid: false, reason: `session mismatch: expected ${sessionId}, got ${sessId}` }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const parsed = Date.parse(updated)
|
|
74
|
+
if (Number.isNaN(parsed) || !/(?:Z|[+-]00:?00)$/i.test(updated)) {
|
|
75
|
+
return { valid: false, reason: 'updated timestamp is not valid ISO-8601 UTC' }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { valid: true, version: 1, session: sessId, updated }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Validates the full schema v1: header + optional sections in fixed order with max 5 bullets each
|
|
82
|
+
export function validateSynthesis(text, { sessionId = null } = {}) {
|
|
83
|
+
const header = validateSynthesisHeader(text, { sessionId })
|
|
84
|
+
const errors = []
|
|
85
|
+
if (!header.valid) {
|
|
86
|
+
errors.push(header.reason)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (typeof text !== 'string') {
|
|
90
|
+
return { valid: false, headerValid: false, errors: ['content is not a string'] }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const lines = text.split(/\r?\n/)
|
|
94
|
+
let lastSectionIdx = -1
|
|
95
|
+
let currentSection = null
|
|
96
|
+
let currentBullets = 0
|
|
97
|
+
|
|
98
|
+
for (let i = 2; i < lines.length; i++) {
|
|
99
|
+
const line = lines[i].trim()
|
|
100
|
+
if (line.startsWith('## ')) {
|
|
101
|
+
const idx = SYNTHESIS_SECTIONS.indexOf(line)
|
|
102
|
+
if (idx === -1) {
|
|
103
|
+
errors.push(`unknown section: ${line}`)
|
|
104
|
+
} else if (idx <= lastSectionIdx) {
|
|
105
|
+
errors.push(`section out of order: ${line}`)
|
|
106
|
+
} else {
|
|
107
|
+
lastSectionIdx = idx
|
|
108
|
+
}
|
|
109
|
+
currentSection = line
|
|
110
|
+
currentBullets = 0
|
|
111
|
+
} else if (line.startsWith('- ')) {
|
|
112
|
+
currentBullets++
|
|
113
|
+
if (currentBullets > 5) {
|
|
114
|
+
errors.push(`section ${currentSection || 'unknown'} exceeds 5 bullets`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
valid: errors.length === 0,
|
|
121
|
+
headerValid: header.valid,
|
|
122
|
+
errors,
|
|
123
|
+
session: header.session ?? null,
|
|
124
|
+
updated: header.updated ?? null,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Inlines synthesis into the ## Synthesis section with size cap (4 KB) and malformed header handling
|
|
129
|
+
export function formatSynthesisSection(rawText, { sessionId = null } = {}) {
|
|
130
|
+
if (!rawText || !rawText.trim()) return ''
|
|
131
|
+
|
|
132
|
+
let content = rawText
|
|
133
|
+
const buf = Buffer.from(content, 'utf8')
|
|
134
|
+
if (buf.length > MAX_SYNTHESIS_BYTES) {
|
|
135
|
+
const sliced = buf.subarray(0, MAX_SYNTHESIS_BYTES).toString('utf8')
|
|
136
|
+
const sep = sliced.endsWith('\n') ? '' : '\n'
|
|
137
|
+
content = `${sliced}${sep}${TRUNCATED_MARKER}`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const header = validateSynthesisHeader(content, { sessionId })
|
|
141
|
+
if (!header.valid) {
|
|
142
|
+
content = `${INVALID_HEADER_PREFIX}\n${content}`
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return `## Synthesis\n\n${content.trim()}`
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// True if .leg/SYNTHESIS-<session-id>.md exists, is non-empty, and was modified within the last 3 checkpoints
|
|
149
|
+
export function hasRecentSynthesis(session) {
|
|
150
|
+
if (!session) return false
|
|
151
|
+
const root = session.worktree?.path ?? session.repo ?? session.cwd ?? null
|
|
152
|
+
if (!root || !session.session_id) return false
|
|
153
|
+
const file = synthesisFile(root, session.session_id)
|
|
154
|
+
try {
|
|
155
|
+
if (!existsSync(file)) return false
|
|
156
|
+
const st = statSync(file)
|
|
157
|
+
if (!st.isFile() || st.size === 0) return false
|
|
158
|
+
const checkpoints = session.checkpoints ?? []
|
|
159
|
+
if (checkpoints.length < 3) return true
|
|
160
|
+
const threshold = new Date(checkpoints[checkpoints.length - 3]).getTime()
|
|
161
|
+
return st.mtimeMs >= threshold
|
|
162
|
+
} catch {
|
|
163
|
+
return false
|
|
164
|
+
}
|
|
165
|
+
}
|
package/src/taps/agy.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// agy tap — Antigravity CLI 1.2.0 is a closed Go binary with no hooks and no
|
|
2
|
-
// usage percentage on any surface
|
|
2
|
+
// usage percentage on any surface Leg can read (its own status line fetches
|
|
3
3
|
// a quota summary from the backend; the value is not written anywhere). What
|
|
4
4
|
// it does give:
|
|
5
|
-
// --log-file <path> one log per
|
|
5
|
+
// --log-file <path> one log per Leg session; the wall shows up as
|
|
6
6
|
// RESOURCE_EXHAUSTED / "it resets in <d>" / "out of quota"
|
|
7
7
|
// (strings present in agy.exe; docs-only until hit live)
|
|
8
8
|
// ~/.gemini/antigravity-cli/history.jsonl
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Why not the status line: Claude Code 2.1.268 renders its built-in status
|
|
4
4
|
// line and does not run a custom `statusLine` command passed via --settings
|
|
5
5
|
// or a project settings file (verified 2026-09-11 with an `echo` command at
|
|
6
|
-
// both levels; hooks from the same --settings file do run). So
|
|
6
|
+
// both levels; hooks from the same --settings file do run). So Leg asks the
|
|
7
7
|
// usage endpoint directly with the OAuth token Claude Code stored at login.
|
|
8
8
|
// The token is read by this process only, sent only to api.anthropic.com,
|
|
9
9
|
// and never written anywhere (the ledger scrubs bearer tokens regardless).
|