@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
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
// harness — the portable-harness subsystem. Leg carries a session's task
|
|
2
|
+
// context between agents (src/bundle.mjs); this carries the agent's WORKING
|
|
3
|
+
// ENVIRONMENT: the global working agreement, identity, hooks, skills,
|
|
4
|
+
// subagents, slash commands, MCP servers and permissions, as far as the
|
|
5
|
+
// destination client can represent them, and says what it could not carry.
|
|
6
|
+
//
|
|
7
|
+
// The engine is the Agnostic AI port engine (MIT), vendored byte for byte
|
|
8
|
+
// under vendor/agnostic-ai and driven only through its library entry
|
|
9
|
+
// (engine/harness/index.cjs). Leg owns everything around it: consent
|
|
10
|
+
// (preferences.harness.enabled), policy (warn | sync | strict), the client
|
|
11
|
+
// registry (registry.mjs), where state lives ($LEG_HOME/harness), the
|
|
12
|
+
// fingerprint that keeps a hand-off fast (fingerprint.mjs), the evidence
|
|
13
|
+
// trail (history.jsonl) and the decision a hand-off makes (prepare below).
|
|
14
|
+
//
|
|
15
|
+
// What the engine promises and Leg keeps: the source client is never written;
|
|
16
|
+
// every generated file carries the ownership claim BRAND.mark; a file the user
|
|
17
|
+
// hand-edited is backed up and skipped, never overwritten, unless --force;
|
|
18
|
+
// managed regions leave everything outside them byte for byte; a credential
|
|
19
|
+
// never enters the bundle (it becomes ${NAME} and the user is told); every
|
|
20
|
+
// dropped item carries a reason. Account credentials are not a harness concern
|
|
21
|
+
// at all: which login runs is the account layer's decision (src/accounts.mjs).
|
|
22
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
23
|
+
import { join } from 'node:path'
|
|
24
|
+
import engine from './vendor/agnostic-ai/engine/harness/index.cjs'
|
|
25
|
+
import { BRAND, CLIENT_IDS, NO_ADAPTER, SOURCE_IDS, VENDOR, clientFor, detectSources, harnessHome, registry } from './registry.mjs'
|
|
26
|
+
import { sourceFingerprint, sourcePresent } from './fingerprint.mjs'
|
|
27
|
+
import { home } from '../store.mjs'
|
|
28
|
+
import { withFileLock, writeJsonAtomic } from '../fsx.mjs'
|
|
29
|
+
import { readPreferences, writePreferences, HARNESS_POLICIES } from '../preferences.mjs'
|
|
30
|
+
import { SECRET_PATTERNS } from '../redact.mjs'
|
|
31
|
+
|
|
32
|
+
export { BRAND, CLIENT_IDS, SOURCE_IDS, NO_ADAPTER, clientFor, detectSources, harnessHome, registry } from './registry.mjs'
|
|
33
|
+
export const COMPONENTS = engine.COMPONENTS
|
|
34
|
+
export const POLICIES = ['off', ...HARNESS_POLICIES]
|
|
35
|
+
// What a hand-off records about the destination's harness. One word each,
|
|
36
|
+
// so the board and the CLI print the same vocabulary.
|
|
37
|
+
export const STATES = ['off', 'same-client', 'source', 'synced', 'partial', 'stale', 'attention', 'unsupported', 'blocked', 'error']
|
|
38
|
+
|
|
39
|
+
// ---- engine configuration (once per process) ----
|
|
40
|
+
let configured = false
|
|
41
|
+
function guardsPatterns() {
|
|
42
|
+
try {
|
|
43
|
+
const g = JSON.parse(readFileSync(join(VENDOR, 'core', 'safety', 'guards.json'), 'utf8'))
|
|
44
|
+
return g?.guards?.secretScan?.sensitivePatterns ?? []
|
|
45
|
+
} catch { return [] }
|
|
46
|
+
}
|
|
47
|
+
export function configureEngine() {
|
|
48
|
+
if (configured) return engine
|
|
49
|
+
engine.configure({
|
|
50
|
+
brand: { ...BRAND },
|
|
51
|
+
// the engine's own shapes plus Leg's (src/redact.mjs): one list, fail closed
|
|
52
|
+
secretPatterns: [...guardsPatterns(), ...SECRET_PATTERNS.map(([, re]) => re)],
|
|
53
|
+
shimPath: join(VENDOR, 'engine', 'hooks', 'shim.cjs'),
|
|
54
|
+
// no rules live inside the package: an @import may only resolve inside the user's home
|
|
55
|
+
importRoots: [],
|
|
56
|
+
})
|
|
57
|
+
configured = true
|
|
58
|
+
return engine
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---- where state lives ----
|
|
62
|
+
export function harnessDir() { return join(home(), 'harness') }
|
|
63
|
+
export const bundleDir = () => join(harnessDir(), 'bundle')
|
|
64
|
+
export const captureFile = () => join(harnessDir(), 'capture.json')
|
|
65
|
+
export const historyFile = () => join(harnessDir(), 'history.jsonl')
|
|
66
|
+
export const policyFile = () => join(harnessDir(), 'policy.json')
|
|
67
|
+
|
|
68
|
+
function readJson(file, fallback = null) {
|
|
69
|
+
if (!existsSync(file)) return fallback
|
|
70
|
+
try { return JSON.parse(readFileSync(file, 'utf8')) } catch { return fallback }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---- the port policy (what is deliberately not carried) ----
|
|
74
|
+
// Leg ships a neutral default: nothing excluded, nothing dropped, and the
|
|
75
|
+
// Codex model ladder that maps the four Claude tiers onto Codex models. A user
|
|
76
|
+
// customises it in $LEG_HOME/harness/policy.json, which is deep-merged over
|
|
77
|
+
// this (docs/harness.md, "The port policy").
|
|
78
|
+
export const DEFAULT_POLICY = Object.freeze({
|
|
79
|
+
targets: 'installed',
|
|
80
|
+
rules: { dropSectionsForTargets: [] },
|
|
81
|
+
hooks: { exclude: [], extra: {} },
|
|
82
|
+
skills: { exclude: {}, codexDisable: [] },
|
|
83
|
+
mcp: { exclude: {} },
|
|
84
|
+
agents: { modelLadder: { codex: { fable: ['gpt-6-astra', 'high'], opus: ['gpt-5.6-sol', 'high'], sonnet: ['gpt-5.6-terra', 'medium'], haiku: ['gpt-5.6-luna', 'low'] } } },
|
|
85
|
+
})
|
|
86
|
+
function merge(base, over) {
|
|
87
|
+
if (!over || typeof over !== 'object' || Array.isArray(over)) return over === undefined ? base : over
|
|
88
|
+
const out = { ...base }
|
|
89
|
+
for (const [k, v] of Object.entries(over)) out[k] = base && typeof base[k] === 'object' && !Array.isArray(base[k]) ? merge(base[k], v) : v
|
|
90
|
+
return out
|
|
91
|
+
}
|
|
92
|
+
export function readPolicy() {
|
|
93
|
+
const user = readJson(policyFile(), null)
|
|
94
|
+
const merged = merge(structuredClone(DEFAULT_POLICY), user ?? {})
|
|
95
|
+
merged.baseDir = harnessDir()
|
|
96
|
+
return merged
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---- configuration (consent + mode + source) ----
|
|
100
|
+
export function harnessConfig() { return readPreferences().harness }
|
|
101
|
+
export function effectivePolicy(cfg = harnessConfig()) { return cfg.enabled ? cfg.policy : 'off' }
|
|
102
|
+
|
|
103
|
+
// ---- evidence ----
|
|
104
|
+
export function appendHistory(entry) {
|
|
105
|
+
mkdirSync(harnessDir(), { recursive: true })
|
|
106
|
+
appendFileSync(historyFile(), JSON.stringify({ ts: new Date().toISOString(), ...entry }) + '\n')
|
|
107
|
+
}
|
|
108
|
+
export function readHistory(limit = 50) {
|
|
109
|
+
if (!existsSync(historyFile())) return []
|
|
110
|
+
return readFileSync(historyFile(), 'utf8').split('\n').filter(Boolean).map((l) => { try { return JSON.parse(l) } catch { return null } }).filter(Boolean).slice(-limit)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---- capture ----
|
|
114
|
+
function sourceTarget(source, opts) {
|
|
115
|
+
const t = registry(opts).find((x) => x.id === source)
|
|
116
|
+
if (!t) throw new Error(`"${source}" is not a client Leg can capture a harness from (${SOURCE_IDS.join(', ')})`)
|
|
117
|
+
return t
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function resolveSource({ env = process.env, cfg = harnessConfig() } = {}) {
|
|
121
|
+
if (cfg.source) return cfg.source
|
|
122
|
+
const found = detectSources({ env })
|
|
123
|
+
return found.length ? found[0].id : null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Read the source client into the neutral bundle under $LEG_HOME/harness/bundle.
|
|
127
|
+
// Cheap when nothing changed: the source surfaces are fingerprinted first and a
|
|
128
|
+
// matching capture.json means the bundle on disk is current.
|
|
129
|
+
export function captureHarness({ source = null, force = false, env = process.env } = {}) {
|
|
130
|
+
configureEngine()
|
|
131
|
+
const src = source ?? resolveSource({ env })
|
|
132
|
+
if (!src) throw new Error('no source client found: none of ~/.claude/CLAUDE.md or ~/.codex/AGENTS.md exists (leg harness source <claude|codex>)')
|
|
133
|
+
const homeDir = harnessHome(env)
|
|
134
|
+
const reg = registry({ env, home: homeDir })
|
|
135
|
+
const target = sourceTarget(src, { env, home: homeDir })
|
|
136
|
+
if (!sourcePresent(src, target)) throw new Error(`source client ${src} has no rules file under ${target.home}`)
|
|
137
|
+
const fp = sourceFingerprint(src, target, homeDir)
|
|
138
|
+
const t0 = Date.now()
|
|
139
|
+
mkdirSync(harnessDir(), { recursive: true })
|
|
140
|
+
return withFileLock(join(harnessDir(), '.lock'), () => {
|
|
141
|
+
const cache = readJson(captureFile())
|
|
142
|
+
if (!force && cache && cache.source === src && cache.surfaces_hash === fp.hash && existsSync(join(bundleDir(), 'manifest.json'))) {
|
|
143
|
+
// a bundle that no longer loads, or no longer matches the fingerprint it
|
|
144
|
+
// was recorded with, is repaired by capturing again, never trusted
|
|
145
|
+
let bundle = null
|
|
146
|
+
try { bundle = engine.bundle.load(bundleDir()) } catch { bundle = null }
|
|
147
|
+
if (bundle && bundle.manifest.fingerprint === cache.fingerprint) return { bundle, warnings: cache.warnings ?? [], cached: true, source: src, elapsed_ms: Date.now() - t0 }
|
|
148
|
+
}
|
|
149
|
+
const { bundle, warnings } = engine.capture({ from: src, home: homeDir, outDir: bundleDir(), registry: reg, port: readPolicy() })
|
|
150
|
+
const record = { source: src, fingerprint: bundle.manifest.fingerprint, surfaces_hash: fp.hash, surfaces: fp.surfaces, captured_at: bundle.manifest.capturedAt, components: bundle.manifest.components, warnings }
|
|
151
|
+
mkdirSync(harnessDir(), { recursive: true })
|
|
152
|
+
writeJsonAtomic(captureFile(), record)
|
|
153
|
+
appendHistory({ op: 'capture', source: src, fingerprint: record.fingerprint, components: record.components, warnings, elapsed_ms: Date.now() - t0 })
|
|
154
|
+
return { bundle, warnings, cached: false, source: src, elapsed_ms: Date.now() - t0 }
|
|
155
|
+
}, LOCK)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// One writer at a time across every Leg process: a hand-off in one terminal,
|
|
159
|
+
// a `leg harness sync` in another. Thirty seconds covers a cold apply; a
|
|
160
|
+
// caller that cannot get the lock fails its own step rather than tearing the
|
|
161
|
+
// ownership record two processes would otherwise both rewrite.
|
|
162
|
+
const LOCK = { retries: 600, waitMs: 50, staleMs: 60000, mustHold: true }
|
|
163
|
+
|
|
164
|
+
export function loadBundle() {
|
|
165
|
+
configureEngine()
|
|
166
|
+
try { return engine.bundle.load(bundleDir()) } catch (err) { return { corrupt: err.message } }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// What the bundle holds, by name, for `leg harness inspect` and the board.
|
|
170
|
+
export function inspectHarness() {
|
|
171
|
+
const cap = readJson(captureFile())
|
|
172
|
+
const b = loadBundle()
|
|
173
|
+
if (!b) return { captured: false, source: cap?.source ?? null }
|
|
174
|
+
if (b.corrupt) return { captured: true, corrupt: b.corrupt, source: cap?.source ?? null }
|
|
175
|
+
return {
|
|
176
|
+
captured: true,
|
|
177
|
+
source: b.manifest.source,
|
|
178
|
+
captured_at: b.manifest.capturedAt,
|
|
179
|
+
fingerprint: b.manifest.fingerprint,
|
|
180
|
+
components: b.manifest.components,
|
|
181
|
+
rules_bytes: b.rules.length,
|
|
182
|
+
identity: Boolean(b.identity),
|
|
183
|
+
hooks: Object.fromEntries(Object.entries(b.hooks.events).map(([ev, groups]) => [ev, groups.reduce((n, g) => n + g.hooks.length, 0)])),
|
|
184
|
+
skills: b.skills.skills.map((s) => s.name),
|
|
185
|
+
agents: b.agents.map((a) => ({ name: a.name, model: a.meta.model, readonly: String(a.meta.readonly) === 'true' })),
|
|
186
|
+
commands: b.commands.map((c) => c.name),
|
|
187
|
+
mcp: Object.entries(b.mcp.servers).map(([name, s]) => ({ name, transport: s.transport, env_refs: [...Object.values(s.env ?? {}), ...Object.values(s.headers ?? {})].filter((v) => /^\$\{[A-Za-z0-9_]+\}$/.test(String(v))).length })),
|
|
188
|
+
permissions: b.permissions,
|
|
189
|
+
warnings: cap?.warnings ?? [],
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---- normalising an engine report into Leg's vocabulary ----
|
|
194
|
+
const ROW_STATE = { synced: 'synced', written: 'synced', stale: 'stale', skipped: 'attention', unsupported: 'unsupported', error: 'error', source: 'source' }
|
|
195
|
+
const COMPONENT_STATE = { synced: 'synced', written: 'synced', stale: 'stale', skipped: 'attention', unsupported: 'unsupported', error: 'error' }
|
|
196
|
+
const COUNTED = { skills: 'skills', agents: 'agents', commands: 'commands', mcp: 'mcp', hooks: 'hooks', permissions: 'permissions' }
|
|
197
|
+
|
|
198
|
+
function excludedBy(policy, component, item) {
|
|
199
|
+
const name = String(item).replace(/^(skill|mcp|agent|command)\s+/, '')
|
|
200
|
+
if (component === 'skills' && policy.skills?.exclude?.[name]) return true
|
|
201
|
+
if (component === 'mcp' && policy.mcp?.exclude?.[name]) return true
|
|
202
|
+
if (component === 'hooks' && (policy.hooks?.exclude ?? []).some((e) => { try { return new RegExp(e.match).test(item) } catch { return String(item).includes(e.match) } })) return true
|
|
203
|
+
if (component === 'rules' && /^rules section/.test(item)) return true
|
|
204
|
+
return false
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function normalizeRow(row, bundle, policy = readPolicy()) {
|
|
208
|
+
const counts = bundle?.manifest?.components ?? {}
|
|
209
|
+
const components = {}
|
|
210
|
+
const dropped = []
|
|
211
|
+
const attention = []
|
|
212
|
+
let files = 0
|
|
213
|
+
for (const c of COMPONENTS) {
|
|
214
|
+
const r = row.components?.[c]
|
|
215
|
+
if (!r) { components[c] = { state: row.status === 'source' ? 'source' : 'unsupported', note: null }; continue }
|
|
216
|
+
const state = COMPONENT_STATE[r.status] ?? 'error'
|
|
217
|
+
const drops = (r.dropped ?? []).map((d) => ({ component: c, item: d.item, reason: d.reason, excluded: excludedBy(policy, c, d.item) }))
|
|
218
|
+
dropped.push(...drops)
|
|
219
|
+
// a deny or ask the destination cannot express leaves it with a WIDER
|
|
220
|
+
// policy than the source: that is not a drop to note, it is a gap to fix
|
|
221
|
+
for (const d of drops) if (c === 'permissions' && /^(deny|ask):/.test(d.item) && !d.excluded) attention.push({ component: c, file: null, reason: `${d.item} cannot be expressed here (${d.reason}); the destination is less restricted than the source` })
|
|
222
|
+
for (const f of r.files ?? []) {
|
|
223
|
+
if (f.action === 'written' || f.action === 'linked' || f.action === 'pruned' || f.action === 'removed') files++
|
|
224
|
+
if (f.action === 'skipped-hand-edited') attention.push({ component: c, file: f.path, reason: 'hand-edited since Leg last wrote it; left alone (backup taken). Run `leg harness sync --force` to replace it, or edit the source client instead.' })
|
|
225
|
+
}
|
|
226
|
+
if (r.status === 'error') attention.push({ component: c, file: null, reason: r.error ?? r.note ?? 'error' })
|
|
227
|
+
const total = COUNTED[c] ? (counts[c] ?? 0) : null
|
|
228
|
+
const notCarried = drops.filter((d) => !d.excluded).length
|
|
229
|
+
const carried = total === null ? null : Math.max(0, total - drops.length)
|
|
230
|
+
components[c] = { state, note: r.note ?? null, total, carried, dropped: drops.length, excluded: drops.length - notCarried, error: r.error ?? null }
|
|
231
|
+
}
|
|
232
|
+
let state = ROW_STATE[row.status] ?? 'error'
|
|
233
|
+
if (state === 'synced' && dropped.some((d) => !d.excluded)) state = 'partial'
|
|
234
|
+
if (attention.length && state !== 'error') state = 'attention'
|
|
235
|
+
return { state, installed: row.installed, adapter: row.adapter, components, dropped, attention, files_written: files, backups: [], note: row.note ?? null, error: row.error ?? null }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function sortedTargets(report, bundle, policy) {
|
|
239
|
+
const out = {}
|
|
240
|
+
for (const id of CLIENT_IDS) if (report.targets[id]) out[id] = { id, name: report.targets[id].name, ...normalizeRow(report.targets[id], bundle, policy) }
|
|
241
|
+
return out
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ---- compare / apply ----
|
|
245
|
+
export function compareHarness({ to = null, env = process.env, bundle = null } = {}) {
|
|
246
|
+
configureEngine()
|
|
247
|
+
const b = bundle ?? loadBundle()
|
|
248
|
+
if (!b || b.corrupt) return { captured: Boolean(b), corrupt: b?.corrupt ?? null, targets: {} }
|
|
249
|
+
const homeDir = harnessHome(env)
|
|
250
|
+
const policy = readPolicy()
|
|
251
|
+
mkdirSync(harnessDir(), { recursive: true })
|
|
252
|
+
const report = withFileLock(join(harnessDir(), '.lock'), () => engine.status({ bundle: b, home: homeDir, registry: registry({ env, home: homeDir }), port: policy, storageDir: harnessDir(), to: to ? [].concat(to) : undefined, quiet: true, log: () => {} }).report, LOCK)
|
|
253
|
+
return { captured: true, source: b.manifest.source, fingerprint: b.manifest.fingerprint, captured_at: b.manifest.capturedAt, targets: sortedTargets(report, b, policy), stale: report.stale }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function applyHarness({ to = null, force = false, dryRun = false, env = process.env, bundle = null, sessionId = null } = {}) {
|
|
257
|
+
configureEngine()
|
|
258
|
+
const b = bundle ?? loadBundle()
|
|
259
|
+
if (!b) throw new Error('no harness captured yet: leg harness capture')
|
|
260
|
+
if (b.corrupt) throw new Error(`the captured harness is unreadable (${b.corrupt}); run leg harness capture --force`)
|
|
261
|
+
const homeDir = harnessHome(env)
|
|
262
|
+
const policy = readPolicy()
|
|
263
|
+
const t0 = Date.now()
|
|
264
|
+
const backupsDir = join(harnessDir(), 'backups')
|
|
265
|
+
const before = backupNames(backupsDir)
|
|
266
|
+
mkdirSync(harnessDir(), { recursive: true })
|
|
267
|
+
const report = withFileLock(join(harnessDir(), '.lock'), () => engine.apply({
|
|
268
|
+
bundle: b, home: homeDir, registry: registry({ env, home: homeDir }), port: policy, storageDir: harnessDir(),
|
|
269
|
+
to: to ? [].concat(to) : undefined, force, dryRun, check: dryRun, quiet: true, log: () => {},
|
|
270
|
+
}), LOCK)
|
|
271
|
+
const targets = sortedTargets(report, b, policy)
|
|
272
|
+
// The writer names every backup it takes (<target>-<file>-<stamp>.bak) but
|
|
273
|
+
// adapters keep only the action, so the count comes from the directory.
|
|
274
|
+
const created = [...backupNames(backupsDir)].filter((n) => !before.has(n))
|
|
275
|
+
for (const [id, t] of Object.entries(targets)) t.backups = created.filter((n) => n.startsWith(`${id}-`)).map((n) => join(backupsDir, n))
|
|
276
|
+
const out = { source: b.manifest.source, fingerprint: b.manifest.fingerprint, captured_at: b.manifest.capturedAt, mode: report.mode, targets, applied_at: report.appliedAt, elapsed_ms: Date.now() - t0 }
|
|
277
|
+
if (!dryRun) {
|
|
278
|
+
for (const [id, t] of Object.entries(targets)) {
|
|
279
|
+
const files = []
|
|
280
|
+
for (const r of Object.values(report.targets[id].components ?? {})) for (const f of r.files ?? []) if (f.action && f.action !== 'unchanged' && f.action !== 'inspected') files.push({ path: engine.common.tildePath(f.path, homeDir), action: f.action })
|
|
281
|
+
appendHistory({ op: 'apply', source: out.source, target: id, fingerprint: out.fingerprint, captured_at: out.captured_at, state: t.state, session_id: sessionId, force, components: Object.fromEntries(Object.entries(t.components).map(([c, v]) => [c, v.state])), dropped: t.dropped.slice(0, 50), attention: t.attention, warnings: [], files, written: t.files_written, backups: t.backups, elapsed_ms: out.elapsed_ms })
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return out
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function backupNames(dir) {
|
|
288
|
+
try { return new Set(readdirSync(dir).filter((n) => { try { return statSync(join(dir, n)).isFile() } catch { return false } })) } catch { return new Set() }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---- status ----
|
|
292
|
+
export function getHarnessStatus({ env = process.env, fresh = false } = {}) {
|
|
293
|
+
const cfg = harnessConfig()
|
|
294
|
+
const cap = readJson(captureFile())
|
|
295
|
+
const source = cfg.source ?? cap?.source ?? resolveSource({ env, cfg })
|
|
296
|
+
const out = {
|
|
297
|
+
enabled: cfg.enabled, policy: effectivePolicy(cfg), configured_policy: cfg.policy, source,
|
|
298
|
+
sources_detected: detectSources({ env }).map((s) => s.id),
|
|
299
|
+
captured: Boolean(cap), captured_at: cap?.captured_at ?? null, fingerprint: cap?.fingerprint ?? null, components: cap?.components ?? null,
|
|
300
|
+
source_changed: null, targets: {}, unsupported: {}, warnings: cap?.warnings ?? [],
|
|
301
|
+
}
|
|
302
|
+
for (const [agent, reason] of Object.entries(NO_ADAPTER)) out.unsupported[agent] = reason
|
|
303
|
+
if (!source) return out
|
|
304
|
+
try {
|
|
305
|
+
const homeDir = harnessHome(env)
|
|
306
|
+
const target = registry({ env, home: homeDir }).find((t) => t.id === source)
|
|
307
|
+
if (target && sourcePresent(source, target)) out.source_changed = cap ? sourceFingerprint(source, target, homeDir).hash !== cap.surfaces_hash : null
|
|
308
|
+
} catch { /* the status page must render whatever the source looks like */ }
|
|
309
|
+
if (fresh && out.enabled) {
|
|
310
|
+
try { const c = captureHarness({ source, env }); out.captured = true; out.captured_at = c.bundle.manifest.capturedAt; out.fingerprint = c.bundle.manifest.fingerprint; out.components = c.bundle.manifest.components; out.source_changed = false; out.warnings = c.warnings } catch (err) { out.capture_error = err.message }
|
|
311
|
+
}
|
|
312
|
+
if (out.captured) {
|
|
313
|
+
const cmp = compareHarness({ env })
|
|
314
|
+
out.targets = cmp.targets
|
|
315
|
+
out.corrupt = cmp.corrupt ?? null
|
|
316
|
+
}
|
|
317
|
+
return out
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function explainHarnessDrops({ to = null, env = process.env } = {}) {
|
|
321
|
+
const cmp = compareHarness({ to, env })
|
|
322
|
+
const out = []
|
|
323
|
+
for (const t of Object.values(cmp.targets)) for (const d of t.dropped) out.push({ target: t.id, ...d })
|
|
324
|
+
return { source: cmp.source ?? null, dropped: out, unsupported: NO_ADAPTER }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ---- enable / disable ----
|
|
328
|
+
export function setHarnessConfig(patch) { return writePreferences({ harness: patch }).harness }
|
|
329
|
+
|
|
330
|
+
// ---- the hand-off decision ----
|
|
331
|
+
// Called before every leg Leg launches. Deterministic, never interactive,
|
|
332
|
+
// never throws: the answer is a record the session keeps and the board shows.
|
|
333
|
+
// proceed whether the launch goes ahead (only strict mode says no)
|
|
334
|
+
// state one of STATES
|
|
335
|
+
export function prepareHarnessForHandoff({ from = null, to, sessionId = null, env = process.env, cfg = harnessConfig(), log = null } = {}) {
|
|
336
|
+
const t0 = Date.now()
|
|
337
|
+
const policy = effectivePolicy(cfg)
|
|
338
|
+
const base = { policy, from: from ?? null, to, target: clientFor(to), proceed: true, elapsed_ms: 0, at: new Date().toISOString() }
|
|
339
|
+
const finish = (rest) => {
|
|
340
|
+
const out = { ...base, ...rest, elapsed_ms: Date.now() - t0 }
|
|
341
|
+
if (out.state !== 'off') appendHistory({ op: 'prepare', session_id: sessionId, from: out.from, to: out.to, target: out.target, policy, state: out.state, proceed: out.proceed, fingerprint: out.fingerprint ?? null, reason: out.reason ?? null, elapsed_ms: out.elapsed_ms })
|
|
342
|
+
if (log) log(out)
|
|
343
|
+
return out
|
|
344
|
+
}
|
|
345
|
+
if (policy === 'off') return finish({ state: 'off', summary: 'portable harness is off' })
|
|
346
|
+
const target = clientFor(to)
|
|
347
|
+
const fromClient = clientFor(from)
|
|
348
|
+
if (!target) return finish({ state: 'unsupported', reason: `${to} runs under no client Leg can carry a harness to`, summary: `${to}: no client to carry a harness to` })
|
|
349
|
+
if (fromClient && fromClient === target) return finish({ state: 'same-client', summary: `${target} to ${target}: same client, same harness` })
|
|
350
|
+
try {
|
|
351
|
+
const source = resolveSource({ env, cfg })
|
|
352
|
+
if (!source) return finish({ state: 'error', reason: 'no source client configured or detected', proceed: policy !== 'strict', summary: 'no source harness to carry' })
|
|
353
|
+
if (target === source) return finish({ state: 'source', source, summary: `${target} is the source of the harness; nothing to carry` })
|
|
354
|
+
if (NO_ADAPTER[target] || !CLIENT_IDS.includes(target)) {
|
|
355
|
+
return finish({ state: 'unsupported', source, reason: NO_ADAPTER[target] ?? `no harness adapter for ${target}`, proceed: policy !== 'strict', summary: `${target}: ${NO_ADAPTER[target] ?? 'no harness adapter'}` })
|
|
356
|
+
}
|
|
357
|
+
const cap = captureHarness({ source, env })
|
|
358
|
+
const check = compareHarness({ to: target, env, bundle: cap.bundle })
|
|
359
|
+
let row = check.targets[target]
|
|
360
|
+
if (!row) return finish({ state: 'error', source, reason: `no status for ${target}`, proceed: policy !== 'strict', summary: `${target}: no status` })
|
|
361
|
+
if (!row.installed) return finish({ state: 'unsupported', source, reason: `${target} is not installed under ${harnessHome(env)}`, proceed: policy !== 'strict', summary: `${target} is not installed` })
|
|
362
|
+
let synced_at = null
|
|
363
|
+
let mode = 'check'
|
|
364
|
+
if (policy !== 'warn' && (row.state === 'stale' || row.state === 'attention')) {
|
|
365
|
+
const applied = applyHarness({ to: target, env, bundle: cap.bundle, sessionId })
|
|
366
|
+
row = applied.targets[target]
|
|
367
|
+
synced_at = applied.applied_at
|
|
368
|
+
mode = 'sync'
|
|
369
|
+
}
|
|
370
|
+
const blocked = policy === 'strict' && !(row.state === 'synced' || row.state === 'partial')
|
|
371
|
+
const state = blocked ? 'blocked' : row.state
|
|
372
|
+
const summary = describe({ state, target, row, policy, mode, cached: cap.cached })
|
|
373
|
+
return finish({
|
|
374
|
+
state, source, fingerprint: cap.bundle.manifest.fingerprint, captured_at: cap.bundle.manifest.capturedAt, capture_cached: cap.cached,
|
|
375
|
+
synced_at, mode, proceed: !blocked, reason: blocked ? `strict policy: ${target} is ${row.state}` : null,
|
|
376
|
+
components: row.components, dropped: row.dropped.slice(0, 50), attention: row.attention, warnings: cap.warnings,
|
|
377
|
+
files_written: row.files_written, backups: row.backups.length, summary,
|
|
378
|
+
})
|
|
379
|
+
} catch (err) {
|
|
380
|
+
return finish({ state: 'error', reason: String(err.message).slice(0, 300), proceed: policy !== 'strict', summary: `harness error: ${String(err.message).slice(0, 120)}` })
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function describe({ state, target, row, policy, mode, cached }) {
|
|
385
|
+
const carried = COMPONENTS.filter((c) => row.components[c]?.state === 'synced').length
|
|
386
|
+
const supported = COMPONENTS.filter((c) => row.components[c]?.state !== 'unsupported').length
|
|
387
|
+
const parts = [`${target} harness ${state}`]
|
|
388
|
+
parts.push(`${carried}/${supported} components${row.dropped.length ? `, ${row.dropped.length} dropped` : ''}`)
|
|
389
|
+
if (mode === 'sync') parts.push(`${row.files_written} file(s) written`)
|
|
390
|
+
else if (state === 'stale') parts.push(policy === 'warn' ? 'warn policy: not synced' : 'not synced')
|
|
391
|
+
if (row.attention.length) parts.push(`${row.attention.length} need(s) attention`)
|
|
392
|
+
if (cached) parts.push('capture reused')
|
|
393
|
+
return parts.join(' · ')
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// The one-line summary a session's timeline and the terminal print.
|
|
397
|
+
export function harnessLine(outcome) {
|
|
398
|
+
if (!outcome || outcome.state === 'off') return null
|
|
399
|
+
if (outcome.state === 'same-client') return null
|
|
400
|
+
return outcome.summary ?? `${outcome.target}: ${outcome.state}`
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// A status a session card can show beside the agent, or null when there is nothing to say.
|
|
404
|
+
export function harnessBadge(outcome) {
|
|
405
|
+
if (!outcome || ['off', 'same-client'].includes(outcome.state)) return null
|
|
406
|
+
return { state: outcome.state, target: outcome.target, source: outcome.source ?? null, synced_at: outcome.synced_at ?? null, captured_at: outcome.captured_at ?? null }
|
|
407
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// registry — which coding clients Leg can carry a harness between, and where
|
|
2
|
+
// each keeps its configuration. Built from the engine's own client registry
|
|
3
|
+
// (src/harness/vendor/agnostic-ai/core/templates/targets.json, byte-for-byte
|
|
4
|
+
// from upstream) narrowed to the clients Leg launches and given Leg's own
|
|
5
|
+
// generated-file preambles. Paths honour the same config-dir variables the
|
|
6
|
+
// trust module and the account layer honour (CLAUDE_CONFIG_DIR, CODEX_HOME,
|
|
7
|
+
// GEMINI_CONFIG_DIR), so a harness lands where the agent Leg starts will read.
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
9
|
+
import { homedir } from 'node:os'
|
|
10
|
+
import { dirname, join, resolve } from 'node:path'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
import engine from './vendor/agnostic-ai/engine/harness/index.cjs'
|
|
13
|
+
|
|
14
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
15
|
+
export const VENDOR = join(HERE, 'vendor', 'agnostic-ai')
|
|
16
|
+
const TARGETS_FILE = join(VENDOR, 'core', 'templates', 'targets.json')
|
|
17
|
+
|
|
18
|
+
// The clients a `leg <agent>` session can hand off between, plus Gemini CLI,
|
|
19
|
+
// which shares GEMINI.md with Antigravity and is kept coherent with it.
|
|
20
|
+
export const CLIENT_IDS = ['claude', 'codex', 'gemini', 'agy']
|
|
21
|
+
// Clients the engine can read a harness FROM.
|
|
22
|
+
export const SOURCE_IDS = ['claude', 'codex']
|
|
23
|
+
// Leg agents with no harness adapter: reported as unsupported, never guessed.
|
|
24
|
+
export const NO_ADAPTER = { grok: 'the Grok CLI has no harness adapter yet: its rules, skills and MCP servers are not carried' }
|
|
25
|
+
|
|
26
|
+
export const BRAND = Object.freeze({ id: 'leg-harness', mark: 'GENERATED by Leg harness', region: 'leg harness' })
|
|
27
|
+
|
|
28
|
+
const PATH_FIELDS = ['home', 'rulesFile', 'traitsFile', 'hooksConfigFile', 'skillsDir', 'agentsDir', 'commandsDir', 'mcpConfigFile', 'permissionsFile']
|
|
29
|
+
|
|
30
|
+
// Leg's own preamble per client: the ownership claim (BRAND.mark) in the first
|
|
31
|
+
// lines, and the command that regenerates the file.
|
|
32
|
+
const PREAMBLES = {
|
|
33
|
+
claude: '# Leg harness rules (Claude Code)\n\nGENERATED by Leg harness from the captured source client. Do not hand-edit; edit the source client and run `leg harness sync`. Loaded via an @import line in ~/.claude/CLAUDE.md.\n',
|
|
34
|
+
codex: '# AGENTS.md (Codex CLI)\n\nGENERATED by Leg harness from the captured source client. Do not hand-edit; edit the source client and run `leg harness sync`.\n',
|
|
35
|
+
gemini: '# GEMINI.md (Gemini CLI)\n\nGENERATED by Leg harness from the captured source client. Do not hand-edit; edit the source client and run `leg harness sync`.\n',
|
|
36
|
+
agy: '# GEMINI.md (Gemini CLI)\n\nGENERATED by Leg harness from the captured source client. Do not hand-edit; edit the source client and run `leg harness sync`.\n',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Leg writes its rules file under its own name so it never collides with a
|
|
40
|
+
// file another tool generated into the same client.
|
|
41
|
+
const RENAMES = {
|
|
42
|
+
claude: { rulesFile: '~/.claude/leg-rules.md' },
|
|
43
|
+
codex: { permissionsFile: '~/.codex/rules/leg-harness.rules' },
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// A drive-letter path is absolute on every host (src/trust.mjs makes the same call).
|
|
47
|
+
const DRIVE_ABS = /^[A-Za-z]:[\\/]/
|
|
48
|
+
const absolutely = (p) => (DRIVE_ABS.test(p) ? p : resolve(p))
|
|
49
|
+
|
|
50
|
+
// `~/.codex` -> $CODEX_HOME, and so on, applied to every path field of one
|
|
51
|
+
// entry. The variable names a directory; the file names inside it stay.
|
|
52
|
+
function rebase(entry, from, to) {
|
|
53
|
+
const out = { ...entry }
|
|
54
|
+
for (const field of PATH_FIELDS) {
|
|
55
|
+
const v = entry[field]
|
|
56
|
+
if (typeof v !== 'string') continue
|
|
57
|
+
if (v === from) out[field] = to
|
|
58
|
+
else if (v.startsWith(from + '/')) out[field] = join(to, v.slice(from.length + 1))
|
|
59
|
+
}
|
|
60
|
+
return out
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function envOverrides(entry, env) {
|
|
64
|
+
let e = entry
|
|
65
|
+
if (entry.id === 'claude' && env.CLAUDE_CONFIG_DIR) {
|
|
66
|
+
const dir = absolutely(env.CLAUDE_CONFIG_DIR)
|
|
67
|
+
e = rebase(e, '~/.claude', dir)
|
|
68
|
+
// Claude Code keeps its MCP servers beside the config dir, not inside it
|
|
69
|
+
if (e.mcpConfigFile === '~/.claude.json') e.mcpConfigFile = join(dir, '.claude.json')
|
|
70
|
+
}
|
|
71
|
+
if (entry.id === 'codex' && env.CODEX_HOME) e = rebase(e, '~/.codex', absolutely(env.CODEX_HOME))
|
|
72
|
+
if ((entry.id === 'gemini' || entry.id === 'agy') && env.GEMINI_CONFIG_DIR) e = rebase(e, '~/.gemini', absolutely(env.GEMINI_CONFIG_DIR))
|
|
73
|
+
return e
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The OS home the client registry expands `~/` against. LEG_HARNESS_HOME is
|
|
77
|
+
// the test seam: the suite points it at a throwaway directory so nothing it
|
|
78
|
+
// does can reach the developer's real ~/.claude, ~/.codex or ~/.gemini.
|
|
79
|
+
export function harnessHome(env = process.env) {
|
|
80
|
+
const v = env.LEG_HARNESS_HOME || env.BATON_HARNESS_HOME
|
|
81
|
+
return v ? absolutely(v) : homedir()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let rawCache = null
|
|
85
|
+
function rawTargets() {
|
|
86
|
+
if (!rawCache) rawCache = JSON.parse(readFileSync(TARGETS_FILE, 'utf8')).targets
|
|
87
|
+
return rawCache
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The expanded registry: one entry per client, `installed` read from disk.
|
|
91
|
+
export function registry({ env = process.env, home = harnessHome(env) } = {}) {
|
|
92
|
+
const targets = rawTargets()
|
|
93
|
+
.filter((t) => CLIENT_IDS.includes(t.id))
|
|
94
|
+
.map((t) => ({ ...t, ...(RENAMES[t.id] ?? {}), preamble: PREAMBLES[t.id] ?? t.preamble }))
|
|
95
|
+
.map((t) => envOverrides(t, env))
|
|
96
|
+
return engine.loadRegistry(home, { targets })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function targetFor(id, opts) {
|
|
100
|
+
return registry(opts).find((t) => t.id === id) ?? null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// A Leg adapter name -> the client whose harness it runs under. Fake adapters
|
|
104
|
+
// emulate a real client (bin/fake-agent.mjs); the plain `fake` emulates none.
|
|
105
|
+
export function clientFor(adapter) {
|
|
106
|
+
if (!adapter) return null
|
|
107
|
+
const name = String(adapter).replace(/^fake-/, '')
|
|
108
|
+
if (name === 'fake') return null
|
|
109
|
+
return name
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Which sources exist on this machine, decided from disk: the rules file a
|
|
113
|
+
// user of that client necessarily has.
|
|
114
|
+
export function detectSources({ env = process.env, home = harnessHome(env) } = {}) {
|
|
115
|
+
const reg = registry({ env, home })
|
|
116
|
+
const out = []
|
|
117
|
+
for (const id of SOURCE_IDS) {
|
|
118
|
+
const t = reg.find((x) => x.id === id)
|
|
119
|
+
if (!t) continue
|
|
120
|
+
const rulesFile = id === 'claude' ? join(t.home, 'CLAUDE.md') : t.rulesFile
|
|
121
|
+
if (existsSync(rulesFile)) out.push({ id, home: t.home, rulesFile })
|
|
122
|
+
}
|
|
123
|
+
return out
|
|
124
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wes Sander
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Written by scripts/sync-harness-engine.mjs; never edited by hand. Every file under this directory is a byte-for-byte copy of the Agnostic AI engine at the commit below, and `npm test` fails if one drifts.",
|
|
3
|
+
"upstream": "https://github.com/ucsandman/Agnostic-AI",
|
|
4
|
+
"license": "MIT (see LICENSE beside this file)",
|
|
5
|
+
"commit": "7e35b5164d01fa8d738bfe1153b0fc0f7b86c23d",
|
|
6
|
+
"upstreamDirty": false,
|
|
7
|
+
"syncedAt": "2026-09-16T20:27:22.033Z",
|
|
8
|
+
"files": {
|
|
9
|
+
"LICENSE": "4bd581dc7a60a4ffc3f1ec5c8f3bed3a681f446eff6457e6051963e83da7066d",
|
|
10
|
+
"engine/harness/README.md": "10b4273325add7aa50f4705cd5ad3bc1361609fdfcc2b70b3bae3383a90711ba",
|
|
11
|
+
"engine/harness/index.cjs": "655273cdaa881b97188306f1a2b815012f0e1934890f3920d948a141e098c0e5",
|
|
12
|
+
"engine/harness/common.cjs": "dccf2d5c9fa035dcf63d7490195f7ef9da694d71f7397f2dfa864c1842c67fe7",
|
|
13
|
+
"engine/harness/bundle.cjs": "630ebc4540d32451e7a32527941ceafe7906beb65fead4e49bc8e7ac90d7df43",
|
|
14
|
+
"engine/harness/capture.cjs": "8a26fccd980bbcdb87b67429bde50ab01822cf918346208091921a622c30e9a1",
|
|
15
|
+
"engine/harness/apply.cjs": "ac15d0a4e318bd6316860c3763f8e90cc1ce1d34238ef0df168d6f7f1f3194d9",
|
|
16
|
+
"engine/harness/status.cjs": "748a8bfdfbc76f215bfe0091ccbdaf260cbbd3642df53d8cf8f8fb7ecd16624c",
|
|
17
|
+
"engine/harness/toml.cjs": "2cd95222f36d23e1b7efff27476c5a4ea8aa083e70b98752de288be6959c8bf3",
|
|
18
|
+
"engine/harness/sources/claude.cjs": "3818d4d3ab9a12ac920b8e3c32624490ae59a7a7500069df697b543ed4ba2506",
|
|
19
|
+
"engine/harness/sources/codex.cjs": "9f4668370d225a6fd4ecd0940494b3fbcfa5b2992a26a57068eb27b93b0f9d9a",
|
|
20
|
+
"engine/harness/targets/generic.cjs": "cc2ce7796689a82716630853217aa8ef39657e8139f4b2a74521956f453012ea",
|
|
21
|
+
"engine/harness/targets/claude.cjs": "21240425a80d5ae1e11707abdc97e88d83baf3af99f3a7544194b0a9b0abac3f",
|
|
22
|
+
"engine/harness/targets/codex.cjs": "3714cafd41ec64b402cd547f3a39edc46ee40044b564d9841bfdcc5f570b633f",
|
|
23
|
+
"engine/harness/targets/gemini.cjs": "dcf2bdc1d1122874c426f95bec9ff2d363dff429949b573db7cd8fb1f7835cf6",
|
|
24
|
+
"engine/harness/targets/agy.cjs": "e504db894caa590116c105ec3a4bc3d457753d4acbdd32218dbaa6b42d8eb000",
|
|
25
|
+
"engine/harness/targets/cursor.cjs": "5617e05d31c18c447136bcb8c0df3ad536018c46e65bad597eed9b7197150f5c",
|
|
26
|
+
"engine/hooks/shim.cjs": "83684ad537b5df456fcfaf61487fed14989877d1d16997ae49d124c4ce314f95",
|
|
27
|
+
"core/templates/targets.json": "bfa9787c7a998ca2eabede129fcd642ef5b2ac5d5a063099c6f70cf994c223ae",
|
|
28
|
+
"core/safety/guards.json": "fee1cfb1a629facdac39ff93462ae4ed573c5755f2ab9ea3422645edbe761066"
|
|
29
|
+
}
|
|
30
|
+
}
|