@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ucsandman/legcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Usage-limit monitor and automatic handoff for Claude Code, Codex, agy and Grok. Type leg claude|codex|agy|grok and get the same interactive agent with a board alongside, auto-approve on by default, usage tracking per agent and account, a live context handoff bundle, and at the limit the next agent continuing in the same terminal. $79 once, 30-day money-back guarantee.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -49,10 +49,14 @@
|
|
|
49
49
|
"node": ">=22"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
|
-
"
|
|
53
|
-
"
|
|
52
|
+
"test": "node --test --test-concurrency=4 && node scripts/privacy-check.mjs && node scripts/check-branding.mjs && node scripts/check-claims.mjs && node scripts/sync-leg-agents.mjs --check && node scripts/sync-harness-engine.mjs --check",
|
|
53
|
+
"sync-alias": "node scripts/sync-leg-agents.mjs",
|
|
54
|
+
"harness:engine-check": "node scripts/sync-harness-engine.mjs --check",
|
|
55
|
+
"harness:engine-sync": "node scripts/sync-harness-engine.mjs",
|
|
56
|
+
"version": "node scripts/sync-leg-agents.mjs",
|
|
54
57
|
"lint": "eslint .",
|
|
55
58
|
"privacy": "node scripts/privacy-check.mjs",
|
|
59
|
+
"branding": "node scripts/check-branding.mjs",
|
|
56
60
|
"claims": "node scripts/check-claims.mjs",
|
|
57
61
|
"claims:drift": "node scripts/check-claims.mjs --strict",
|
|
58
62
|
"docs": "node scripts/build-docs-site.mjs",
|
|
@@ -58,12 +58,26 @@ const PAGES = [
|
|
|
58
58
|
title: 'Leg configuration and environment variables',
|
|
59
59
|
description: 'Every Leg environment variable, the config file, network exposure, the token seam and the folder-trust switch.',
|
|
60
60
|
},
|
|
61
|
+
{
|
|
62
|
+
slug: 'harness',
|
|
63
|
+
nav: 'Harness',
|
|
64
|
+
source: 'docs/harness.md',
|
|
65
|
+
title: 'The portable harness: carry rules, hooks, skills and MCP servers between agents',
|
|
66
|
+
description: 'How Leg carries the source agent\'s working environment to the agent a handoff lands on, what moves and what does not, the policies, ownership and backups, and how secrets are handled.',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
slug: 'history',
|
|
70
|
+
nav: 'History',
|
|
71
|
+
source: 'docs/history.md',
|
|
72
|
+
title: 'Every conversation on this machine: leg history and leg worktrees',
|
|
73
|
+
description: 'One read-only index over the conversations Claude Code, Codex, Grok, Antigravity and Copilot CLI keep in their own stores, plus the sessions Leg started itself, and every checkout: the support matrix, what is read, what is written, and how to continue one.',
|
|
74
|
+
},
|
|
61
75
|
{
|
|
62
76
|
slug: 'cli-contracts',
|
|
63
77
|
nav: 'What it reads',
|
|
64
78
|
source: 'docs/cli-contracts.md',
|
|
65
79
|
title: 'What Leg reads from Claude Code, Codex and agy',
|
|
66
|
-
description: 'The exact usage endpoints, hooks, log lines and limit strings
|
|
80
|
+
description: 'The exact usage endpoints, hooks, log lines and limit strings Leg reads from Claude Code, Codex and agy, each cited to its source.',
|
|
67
81
|
},
|
|
68
82
|
{
|
|
69
83
|
slug: 'adapters',
|
|
@@ -176,14 +190,14 @@ function page ({ slug, title, description, body, headings }) {
|
|
|
176
190
|
<head>
|
|
177
191
|
<meta charset="utf-8">
|
|
178
192
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
179
|
-
<title>${escapeHtml(/legcli/i.test(title) ? title : `${title} |
|
|
193
|
+
<title>${escapeHtml(/legcli/i.test(title) ? title : `${title} | Leg`)}</title>
|
|
180
194
|
<meta name="description" content="${escapeHtml(description)}">
|
|
181
195
|
<link rel="canonical" href="${url}">
|
|
182
196
|
<meta name="theme-color" content="#0E1012">
|
|
183
197
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
184
198
|
<meta property="og:type" content="article">
|
|
185
199
|
<meta property="og:url" content="${url}">
|
|
186
|
-
<meta property="og:site_name" content="
|
|
200
|
+
<meta property="og:site_name" content="Leg">
|
|
187
201
|
<meta property="og:title" content="${escapeHtml(title)}">
|
|
188
202
|
<meta property="og:image" content="${ORIGIN}/og.png?v=3">
|
|
189
203
|
<meta property="og:image:secure_url" content="${ORIGIN}/og.png?v=3">
|
|
@@ -202,7 +216,7 @@ function page ({ slug, title, description, body, headings }) {
|
|
|
202
216
|
<a class="skip" href="#doc">Skip to the document</a>
|
|
203
217
|
<header class="docs-top">
|
|
204
218
|
<div class="docs-top-wrap">
|
|
205
|
-
<a class="docs-home" href="/">
|
|
219
|
+
<a class="docs-home" href="/">Leg</a>
|
|
206
220
|
<nav aria-label="Site"><a href="/docs">Docs</a> <a href="/#pricing">Pricing</a> <a href="/license">License</a></nav>
|
|
207
221
|
</div>
|
|
208
222
|
</header>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// check-branding — refuses user-facing copy that still calls the product
|
|
3
|
+
// Baton, LegCLI, or "Leg CLI". Compatibility identifiers (BATON_*, .baton
|
|
4
|
+
// paths, legacy license prefixes) and historical changelog entries are
|
|
5
|
+
// allowed via the lists below. Runs in `npm test` and the commit hook
|
|
6
|
+
// (`--staged`). Exit 1 on any hit.
|
|
7
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
|
8
|
+
import { execFileSync } from 'node:child_process'
|
|
9
|
+
import { join, relative, resolve, dirname } from 'node:path'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
|
|
12
|
+
const SELF = fileURLToPath(import.meta.url)
|
|
13
|
+
const ROOT = resolve(dirname(SELF), '..')
|
|
14
|
+
|
|
15
|
+
// Forbidden in product surfaces. Domain/package `legcli` is lowercase and
|
|
16
|
+
// is not matched. Add a file to ALLOW_FILES or a line pattern to ALLOW_LINE
|
|
17
|
+
// rather than deleting a pattern.
|
|
18
|
+
const PATTERNS = [
|
|
19
|
+
['Baton', /\bBaton\b/],
|
|
20
|
+
['LegCLI', /LegCLI/],
|
|
21
|
+
['LegCli', /LegCli/],
|
|
22
|
+
['Leg CLI', /Leg CLI/],
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
const SKIP_DIRS = new Set([
|
|
26
|
+
'node_modules', '.git', '.supergoal', 'fixtures', 'marketing',
|
|
27
|
+
'.leg', '.leg-worktrees', '.baton', '.baton-worktrees', '.context-handoffs',
|
|
28
|
+
'.playwright-cli', '.design',
|
|
29
|
+
])
|
|
30
|
+
let GITIGNORED = new Set()
|
|
31
|
+
try {
|
|
32
|
+
GITIGNORED = new Set(readFileSync(resolve(ROOT, '.gitignore'), 'utf8').split(/\r?\n/)
|
|
33
|
+
.map((l) => l.trim().replace(/\/$/, ''))
|
|
34
|
+
.filter((l) => l && !l.startsWith('#') && !/[*?[]/.test(l)))
|
|
35
|
+
} catch {}
|
|
36
|
+
const BINARY_EXT = /\.(png|jpg|jpeg|gif|webp|ico|woff2?|ttf|pdf|zip|gz|tgz|mp4|mp3)$/i
|
|
37
|
+
|
|
38
|
+
// Whole files that may still name the old product: history, migration tests,
|
|
39
|
+
// this checker. Keep this list short.
|
|
40
|
+
const ALLOW_FILES = new Set([
|
|
41
|
+
'CHANGELOG.md',
|
|
42
|
+
'site/docs/changelog.html',
|
|
43
|
+
'docs/DEVIATIONS.md',
|
|
44
|
+
'test/resume.test.mjs',
|
|
45
|
+
'scripts/check-branding.mjs',
|
|
46
|
+
].map((p) => p.replace(/\\/g, '/')))
|
|
47
|
+
|
|
48
|
+
// A line is allowed when it is only documenting a compatibility identifier,
|
|
49
|
+
// not presenting the old name as the product.
|
|
50
|
+
const ALLOW_LINE = [
|
|
51
|
+
/\bBATON_[A-Z0-9_]+\b/,
|
|
52
|
+
/\bBATON-/,
|
|
53
|
+
/\.baton\b/,
|
|
54
|
+
/events-baton/,
|
|
55
|
+
/baton\.pid/,
|
|
56
|
+
/baton_simulated/,
|
|
57
|
+
/baton-lock/,
|
|
58
|
+
/baton_(price|site|personal|team)/,
|
|
59
|
+
/type['":\s]+baton/,
|
|
60
|
+
/\bbaton\//,
|
|
61
|
+
/CustomEvent\('baton:/,
|
|
62
|
+
/addEventListener\('baton:/,
|
|
63
|
+
/classList\.contains\('baton'\)/,
|
|
64
|
+
/\blegacy\b/i,
|
|
65
|
+
/\bLEGACY_/,
|
|
66
|
+
/or a baton/,
|
|
67
|
+
/older Baton/,
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
function posix(p) { return p.replace(/\\/g, '/') }
|
|
71
|
+
|
|
72
|
+
function walk(dir, out) {
|
|
73
|
+
for (const name of readdirSync(dir)) {
|
|
74
|
+
if (SKIP_DIRS.has(name) || GITIGNORED.has(name) || name.includes('\uf03a')) continue
|
|
75
|
+
const full = join(dir, name)
|
|
76
|
+
const st = statSync(full)
|
|
77
|
+
if (st.isDirectory()) walk(full, out)
|
|
78
|
+
else if (!BINARY_EXT.test(name) && full !== SELF) out.push(full)
|
|
79
|
+
}
|
|
80
|
+
return out
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function stagedFiles() {
|
|
84
|
+
const out = execFileSync('git', ['diff', '--cached', '--name-only', '--diff-filter=ACMR'],
|
|
85
|
+
{ cwd: ROOT, encoding: 'utf8' })
|
|
86
|
+
return out.split(/\r?\n/).filter(Boolean).map((p) => join(ROOT, p))
|
|
87
|
+
.filter((p) => p !== SELF)
|
|
88
|
+
.filter((p) => { try { return statSync(p).isFile() && !BINARY_EXT.test(p) } catch { return false } })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function allowedFile(file) {
|
|
92
|
+
return ALLOW_FILES.has(posix(relative(ROOT, file)))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function allowedLine(line) {
|
|
96
|
+
return ALLOW_LINE.some((re) => re.test(line))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const staged = process.argv.includes('--staged')
|
|
100
|
+
const files = staged ? stagedFiles() : walk(ROOT, [])
|
|
101
|
+
let hits = 0
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
if (allowedFile(file)) continue
|
|
104
|
+
let text
|
|
105
|
+
try { text = readFileSync(file, 'utf8') } catch { continue }
|
|
106
|
+
const lines = text.split('\n')
|
|
107
|
+
for (let i = 0; i < lines.length; i++) {
|
|
108
|
+
if (allowedLine(lines[i])) continue
|
|
109
|
+
for (const [label, re] of PATTERNS) {
|
|
110
|
+
if (re.test(lines[i])) {
|
|
111
|
+
hits += 1
|
|
112
|
+
process.stdout.write(`${posix(relative(ROOT, file))}:${i + 1}: ${label}\n`)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
process.stdout.write(`check-branding: hits=${hits} files=${files.length}${staged ? ' (staged)' : ''}\n`)
|
|
118
|
+
process.exit(hits ? 1 : 0)
|
package/scripts/check-claims.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Every dated or numbered claim
|
|
2
|
+
// Every dated or numbered claim Leg makes in public, checked against one
|
|
3
3
|
// file. Two separate jobs:
|
|
4
4
|
//
|
|
5
5
|
// node scripts/check-claims.mjs consistency, offline, runs in CI
|
package/scripts/license-sign.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Sign a
|
|
2
|
+
// Sign a Leg license key by hand (the site's /api/key does the same for
|
|
3
3
|
// paid orders). Needs BATON_LICENSE_PRIVATE_KEY in the environment:
|
|
4
4
|
// node --env-file=.env scripts/license-sign.mjs --plan personal --email you@example.com
|
|
5
5
|
// node --env-file=.env scripts/license-sign.mjs --plan team --email ops@acme.com --seats 5 --months 1
|
package/scripts/limits-table.mjs
CHANGED
|
@@ -15,7 +15,7 @@ const rows = SIGNALS.map((s) =>
|
|
|
15
15
|
`| ${s.id} | ${s.adapter} | ${s.classification} | ${s.where} | **${s.source}** | ${cell(s.text).slice(0, 90)} | ${cell(s.produced_by)} |`)
|
|
16
16
|
const counts = SIGNALS.reduce((m, s) => { m[s.source] = (m[s.source] ?? 0) + 1; return m }, {})
|
|
17
17
|
const table = [
|
|
18
|
-
`Generated by \`node scripts/limits-table.mjs\` from ${SIGNALS.length} fixtures (${counts['observed-live'] ?? 0} observed-live, ${counts['docs-only'] ?? 0} docs-only). Classification \`limit\` hands the card to the next agent as a usage limit; \`auth\` is a failed launch (never a limit); \`launch\` is a failed launch that the next agent may still try; \`budget\` is a turn or spend cap set by
|
|
18
|
+
`Generated by \`node scripts/limits-table.mjs\` from ${SIGNALS.length} fixtures (${counts['observed-live'] ?? 0} observed-live, ${counts['docs-only'] ?? 0} docs-only). Classification \`limit\` hands the card to the next agent as a usage limit; \`auth\` is a failed launch (never a limit); \`launch\` is a failed launch that the next agent may still try; \`budget\` is a turn or spend cap set by Leg itself; \`info\` must never classify as a limit.`,
|
|
19
19
|
'',
|
|
20
20
|
'| id | adapter | class | where | source | text (excerpt) | produced by |',
|
|
21
21
|
'|----|---------|-------|-------|--------|----------------|-------------|',
|
package/scripts/live-limits.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { join, dirname, resolve } from 'node:path'
|
|
|
11
11
|
import { fileURLToPath } from 'node:url'
|
|
12
12
|
|
|
13
13
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
14
|
-
const LIVE = process.env.BATON_LIVE_DIR || join(ROOT, 'fixtures', 'live')
|
|
14
|
+
const LIVE = process.env.BATON_LIVE_DIR || process.env.LEG_LIVE_DIR || join(ROOT, 'fixtures', 'live')
|
|
15
15
|
const DOCS = ['docs/adapters.md', 'docs/cli-contracts.md'].map((p) => join(ROOT, p))
|
|
16
16
|
const MARK = /\*\*(docs-only|observed-live(?: [0-9-]+)?)\*\* <!-- live:([a-z]+)\/([a-z_-]+) -->/g
|
|
17
17
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Registry gate for a main-branch publish. Checks @ucsandman/legcli and
|
|
3
|
+
// leg-agents, refuses to ship if their versions differ, and writes
|
|
4
|
+
// GITHUB_OUTPUT so ci.yml can publish only the missing package(s).
|
|
5
|
+
// Fail closed: network errors, invalid JSON, prereleases, stale versions,
|
|
6
|
+
// and metadata drift exit 1.
|
|
7
|
+
import { appendFileSync, readFileSync } from 'node:fs'
|
|
8
|
+
import { dirname, join } from 'node:path'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
|
|
11
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
12
|
+
const EXPECTED_REPOSITORY = 'https://github.com/ucsandman/legcli.git'
|
|
13
|
+
const PRIMARY_NAME = '@ucsandman/legcli'
|
|
14
|
+
const ALIAS_NAME = 'leg-agents'
|
|
15
|
+
|
|
16
|
+
const fail = (message) => { throw new Error(message) }
|
|
17
|
+
|
|
18
|
+
const stableVersion = (value) => {
|
|
19
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(value)
|
|
20
|
+
if (!match) return null
|
|
21
|
+
const parts = match.slice(1).map(Number)
|
|
22
|
+
return parts.every(Number.isSafeInteger) ? parts : null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const writeOutput = (name, value) => {
|
|
26
|
+
if (process.env.GITHUB_OUTPUT) appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`)
|
|
27
|
+
console.log(`${name}=${value}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function registryStatus (name, version) {
|
|
31
|
+
const candidate = stableVersion(version)
|
|
32
|
+
if (!candidate) fail(`invalid stable package version: ${name}@${version}`)
|
|
33
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(name)}/${encodeURIComponent(version)}`
|
|
34
|
+
let response
|
|
35
|
+
try {
|
|
36
|
+
response = await fetch(url, { headers: { Accept: 'application/json' } })
|
|
37
|
+
} catch (error) {
|
|
38
|
+
fail(`npm registry request failed for ${name}@${version}: ${error.message}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (response.status === 404) {
|
|
42
|
+
let packageResponse
|
|
43
|
+
try {
|
|
44
|
+
packageResponse = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}`, { headers: { Accept: 'application/json' } })
|
|
45
|
+
} catch (error) {
|
|
46
|
+
fail(`npm package metadata request failed for ${name}: ${error.message}`)
|
|
47
|
+
}
|
|
48
|
+
if (packageResponse.status === 404) {
|
|
49
|
+
console.log(`${name}@${version} is missing (package not yet on npm); publication is required`)
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
if (packageResponse.status !== 200) {
|
|
53
|
+
fail(`npm package metadata for ${name} returned unexpected status ${packageResponse.status}`)
|
|
54
|
+
}
|
|
55
|
+
let metadata
|
|
56
|
+
try {
|
|
57
|
+
metadata = await packageResponse.json()
|
|
58
|
+
} catch {
|
|
59
|
+
fail(`npm package metadata for ${name} returned invalid JSON`)
|
|
60
|
+
}
|
|
61
|
+
const latestText = metadata?.['dist-tags']?.latest
|
|
62
|
+
const latest = stableVersion(latestText)
|
|
63
|
+
if (!latest) fail(`npm latest tag for ${name} is not a stable version: ${latestText ?? '(missing)'}`)
|
|
64
|
+
const changedPart = candidate.findIndex((part, index) => part !== latest[index])
|
|
65
|
+
const newer = changedPart !== -1 && candidate[changedPart] > latest[changedPart]
|
|
66
|
+
if (!newer) fail(`refusing to publish missing ${name}@${version} because npm latest is ${latestText}`)
|
|
67
|
+
console.log(`${name}@${version} is missing and newer than npm latest ${latestText}; publication is required`)
|
|
68
|
+
return false
|
|
69
|
+
}
|
|
70
|
+
if (response.status !== 200) fail(`npm registry returned unexpected status ${response.status} for ${name}@${version}`)
|
|
71
|
+
let published
|
|
72
|
+
try {
|
|
73
|
+
published = await response.json()
|
|
74
|
+
} catch {
|
|
75
|
+
fail(`npm registry returned invalid JSON for existing ${name}@${version}`)
|
|
76
|
+
}
|
|
77
|
+
if (published?.name !== name || published?.version !== version) {
|
|
78
|
+
fail(`npm registry response did not match the requested ${name}@${version}`)
|
|
79
|
+
}
|
|
80
|
+
console.log(`${name}@${version} already exists; skipping publication`)
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
|
85
|
+
const lock = JSON.parse(readFileSync(join(root, 'package-lock.json'), 'utf8'))
|
|
86
|
+
const alias = JSON.parse(readFileSync(join(root, 'packages', 'leg-agents', 'package.json'), 'utf8'))
|
|
87
|
+
const lockRoot = lock.packages?.['']
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
if (!lockRoot || pkg.name !== PRIMARY_NAME || lockRoot.name !== pkg.name || lockRoot.version !== pkg.version || lock.version !== pkg.version) {
|
|
91
|
+
fail('package.json and package-lock.json root metadata do not match')
|
|
92
|
+
}
|
|
93
|
+
if (pkg.repository?.url !== EXPECTED_REPOSITORY || lockRoot.repository?.url !== EXPECTED_REPOSITORY) {
|
|
94
|
+
fail('package repository metadata does not match the npm trusted publisher binding')
|
|
95
|
+
}
|
|
96
|
+
if (alias.name !== ALIAS_NAME) fail(`alias package name is ${alias.name}, expected ${ALIAS_NAME}`)
|
|
97
|
+
if (alias.version !== pkg.version) {
|
|
98
|
+
fail(`refusing to publish: ${ALIAS_NAME}@${alias.version} is not in lockstep with ${PRIMARY_NAME}@${pkg.version}`)
|
|
99
|
+
}
|
|
100
|
+
if (alias.dependencies?.[PRIMARY_NAME] !== pkg.version) {
|
|
101
|
+
fail(`refusing to publish: ${ALIAS_NAME} pins ${PRIMARY_NAME}@${alias.dependencies?.[PRIMARY_NAME] ?? '(missing)'}, expected ${pkg.version}`)
|
|
102
|
+
}
|
|
103
|
+
if (alias.repository?.url !== EXPECTED_REPOSITORY) {
|
|
104
|
+
fail('alias package repository metadata does not match the npm trusted publisher binding')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const primaryPublished = await registryStatus(pkg.name, pkg.version)
|
|
108
|
+
const aliasPublished = await registryStatus(alias.name, alias.version)
|
|
109
|
+
writeOutput('published', String(primaryPublished))
|
|
110
|
+
writeOutput('alias_published', String(aliasPublished))
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error(`registry gate failed: ${error.message}`)
|
|
113
|
+
process.exitCode = 1
|
|
114
|
+
}
|
package/scripts/probe.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { scrub } from '../src/runner.mjs'
|
|
|
13
13
|
|
|
14
14
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
15
15
|
const SRC = join(ROOT, 'src')
|
|
16
|
-
const HOME = process.env.BATON_HOME || join(homedir(), '.baton')
|
|
16
|
+
const HOME = process.env.LEG_HOME || process.env.BATON_HOME || (existsSync(join(homedir(), '.leg')) ? join(homedir(), '.leg') : existsSync(join(homedir(), '.baton')) ? join(homedir(), '.baton') : join(homedir(), '.leg'))
|
|
17
17
|
|
|
18
18
|
const args = {}
|
|
19
19
|
const argv = process.argv.slice(2)
|
|
@@ -28,11 +28,12 @@ const timeoutS = parseInt(args['timeout-s'] ?? '300', 10)
|
|
|
28
28
|
const adapter = await getAdapter(name)
|
|
29
29
|
const mode = args.mode ?? adapter.modes.default
|
|
30
30
|
|
|
31
|
-
const PROMPT = `Create a file named hello-${name}.txt in the current directory containing exactly the word hi. Then create the directory .
|
|
31
|
+
const PROMPT = `Create a file named hello-${name}.txt in the current directory containing exactly the word hi. Then create the directory .leg if it is missing and write the file .leg/DONE containing the single line: done. Do nothing else. Do not ask questions.\n`
|
|
32
32
|
|
|
33
33
|
const node = (script, a) => execFileSync(process.execPath, [script, ...a], { encoding: 'utf8', env: process.env })
|
|
34
34
|
|
|
35
35
|
// Fresh DONE marker per probe so "done=yes" is this CLI's own work.
|
|
36
|
+
rmSync(join(repo, '.leg', 'DONE'), { force: true })
|
|
36
37
|
rmSync(join(repo, '.baton', 'DONE'), { force: true })
|
|
37
38
|
rmSync(join(repo, `hello-${name}.txt`), { force: true })
|
|
38
39
|
|
|
@@ -84,7 +85,7 @@ const textOf = (f) => (existsSync(join(runDir, f)) ? readFileSync(join(runDir, f
|
|
|
84
85
|
const authSource = /another auth source/i.test(textOf('err.log') + textOf('out.log'))
|
|
85
86
|
const file = existsSync(join(repo, `hello-${name}.txt`))
|
|
86
87
|
&& readFileSync(join(repo, `hello-${name}.txt`), 'utf8').trim() === 'hi'
|
|
87
|
-
const done = existsSync(join(repo, '.baton', 'DONE'))
|
|
88
|
+
const done = existsSync(join(repo, '.leg', 'DONE')) || existsSync(join(repo, '.baton', 'DONE'))
|
|
88
89
|
const exit = run?.status === 'exited' ? run.exit_code : `${run?.status ?? 'unknown'}`
|
|
89
90
|
const parsed = adapter.parseResult(textOf('out.log'))
|
|
90
91
|
writeFileSync(join(out, 'parsed.json'), scrubPaths(JSON.stringify({ ...parsed, raw: undefined }, null, 2)) + '\n')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// seed-fake-cards — fills a
|
|
2
|
+
// seed-fake-cards — fills a LEG_HOME with N backlog cards (fake chain, build
|
|
3
3
|
// pipeline) through the same path the CLI uses (src/cards.mjs createCard), for
|
|
4
4
|
// perf tests and manual board load-testing.
|
|
5
5
|
// node scripts/seed-fake-cards.mjs --home <dir> --repo <git repo path> --count 50
|
|
@@ -19,8 +19,9 @@ if (!args.home) { process.stderr.write('usage: seed-fake-cards.mjs --home <dir>
|
|
|
19
19
|
if (!args.repo) { process.stderr.write('usage: seed-fake-cards.mjs --home <dir> --repo <git repo path> [--count 50]\n'); process.exit(2) }
|
|
20
20
|
const count = parseInt(args.count ?? '50', 10) || 50
|
|
21
21
|
|
|
22
|
-
//
|
|
23
|
-
process.env.
|
|
22
|
+
// LEG_HOME must be set before store.mjs (and the ledger.mjs it imports) load.
|
|
23
|
+
process.env.LEG_HOME = resolve(args.home)
|
|
24
|
+
process.env.BATON_HOME = process.env.LEG_HOME
|
|
24
25
|
const { createCard } = await import('../src/cards.mjs')
|
|
25
26
|
|
|
26
27
|
const TITLES = ['Add retry to fetch', 'Fix flaky lease test', 'Refactor log tail', 'Wire up SSE health', 'Trim README', 'Bump adapter timeout', 'Dedupe blocked events', 'Guard null station', 'Speed up card list', 'Polish error copy']
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
// Seeds a throwaway
|
|
1
|
+
// Seeds a throwaway LEG_HOME with a floor that has something in every lane:
|
|
2
2
|
// two cards running, two waiting on a human, one queued and blocked by a lease
|
|
3
3
|
// another card holds, and a landing on the trunk. The floor is derived entirely
|
|
4
4
|
// from card records and their events (server.mjs floor()), so the states are
|
|
5
5
|
// written straight to the ledger rather than driven through real runs.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
7
|
+
// LEG_HOME=<dir> node scripts/seed-floor-board.mjs <repo path>
|
|
8
8
|
//
|
|
9
|
-
// Serve it on a spare port with
|
|
9
|
+
// Serve it on a spare port with LEG_TRUST=never. NEVER 4747.
|
|
10
10
|
import { join } from 'node:path'
|
|
11
11
|
import { tmpdir } from 'node:os'
|
|
12
12
|
|
|
13
|
-
process.env.
|
|
13
|
+
process.env.LEG_HOME ||= process.env.BATON_HOME || join(tmpdir(), 'leg-floor-board')
|
|
14
|
+
process.env.BATON_HOME ||= process.env.LEG_HOME
|
|
14
15
|
const repo = process.argv[2] || join(tmpdir(), 'toy-demo')
|
|
15
16
|
const { createCard } = await import('../src/cards.mjs')
|
|
16
17
|
const { ledgerAppend, ledgerUpdate, cardDir } = await import('../src/store.mjs')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
2
|
import { tmpdir } from 'node:os'
|
|
3
|
-
// Seeds a throwaway
|
|
3
|
+
// Seeds a throwaway LEG_HOME shaped like a real working board: a few live
|
|
4
4
|
// terminals and a larger pile of lost and ended ones, long absolute temp paths,
|
|
5
5
|
// and an image tag in a prompt.
|
|
6
6
|
//
|
|
@@ -10,12 +10,13 @@ import { tmpdir } from 'node:os'
|
|
|
10
10
|
// fixture cost more than any other mistake in the v4 redesign.
|
|
11
11
|
//
|
|
12
12
|
// node scripts/seed-wes-board.mjs # seeds ./scripts/wes-home
|
|
13
|
-
//
|
|
13
|
+
// LEG_HOME=<dir> node scripts/seed-wes-board.mjs
|
|
14
14
|
//
|
|
15
|
-
// Serve it on an isolated port with a throwaway
|
|
15
|
+
// Serve it on an isolated port with a throwaway LEG_HOME and LEG_TRUST=never.
|
|
16
16
|
// NEVER 4747: that is the operator's live board with real sessions on it.
|
|
17
17
|
// a throwaway home under the OS temp dir, so nothing is written into the repo
|
|
18
|
-
process.env.
|
|
18
|
+
process.env.LEG_HOME ||= process.env.BATON_HOME || join(tmpdir(), 'leg-seed-board')
|
|
19
|
+
process.env.BATON_HOME ||= process.env.LEG_HOME
|
|
19
20
|
const { createSession, updateSession } = await import('../src/sessions.mjs')
|
|
20
21
|
const { recordUsage, markLimited } = await import('../src/usage.mjs')
|
|
21
22
|
const { spawn } = await import('node:child_process')
|
package/scripts/stripe-setup.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Create (idempotently) the two
|
|
2
|
+
// Create (idempotently) the two Leg plans in Stripe, their payment links,
|
|
3
3
|
// and the webhook endpoint for the site. Test mode by default, --live for the
|
|
4
4
|
// live account. Secrets never print: the webhook signing secret and the link
|
|
5
5
|
// URLs are written to .env (gitignored); ids and public URLs print.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// sync-harness-engine — the one way the vendored harness engine changes.
|
|
3
|
+
//
|
|
4
|
+
// Leg's portable-harness subsystem (src/harness/) runs the Agnostic AI port
|
|
5
|
+
// engine (MIT, github.com/ucsandman/Agnostic-AI) as a library. The engine is
|
|
6
|
+
// copied byte for byte into src/harness/vendor/agnostic-ai/ so the package has
|
|
7
|
+
// no runtime dependency and no build step, and this script is the only thing
|
|
8
|
+
// that writes there:
|
|
9
|
+
//
|
|
10
|
+
// node scripts/sync-harness-engine.mjs <path-to-agnostic-ai-checkout>
|
|
11
|
+
// copy the engine files, record the upstream commit and every file's
|
|
12
|
+
// sha256 in src/harness/vendor/agnostic-ai/UPSTREAM.json
|
|
13
|
+
// node scripts/sync-harness-engine.mjs --check
|
|
14
|
+
// verify every vendored file still matches the recorded hash (runs in
|
|
15
|
+
// `npm test`); a hand edit under vendor/ fails the build, because a
|
|
16
|
+
// fix belongs upstream and then here through this script
|
|
17
|
+
// node scripts/sync-harness-engine.mjs --diff <path-to-agnostic-ai-checkout>
|
|
18
|
+
// list vendored files that differ from that checkout (exit 1 if any)
|
|
19
|
+
//
|
|
20
|
+
// Every verdict prints the number of files it looked at.
|
|
21
|
+
import { createHash } from 'node:crypto'
|
|
22
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
23
|
+
import { execFileSync } from 'node:child_process'
|
|
24
|
+
import { dirname, join, relative, resolve } from 'node:path'
|
|
25
|
+
import { fileURLToPath } from 'node:url'
|
|
26
|
+
|
|
27
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
28
|
+
export const VENDOR_DIR = join(ROOT, 'src', 'harness', 'vendor', 'agnostic-ai')
|
|
29
|
+
export const MANIFEST = join(VENDOR_DIR, 'UPSTREAM.json')
|
|
30
|
+
|
|
31
|
+
// What Leg embeds: the engine as a library plus the two data files it reads by
|
|
32
|
+
// default and the hook shim its targets point at. The repo's own CLI
|
|
33
|
+
// (engine/harness/cli.cjs) and everything outside engine/harness stay upstream.
|
|
34
|
+
export const FILES = [
|
|
35
|
+
'LICENSE',
|
|
36
|
+
'engine/harness/README.md',
|
|
37
|
+
'engine/harness/index.cjs',
|
|
38
|
+
'engine/harness/common.cjs',
|
|
39
|
+
'engine/harness/bundle.cjs',
|
|
40
|
+
'engine/harness/capture.cjs',
|
|
41
|
+
'engine/harness/apply.cjs',
|
|
42
|
+
'engine/harness/status.cjs',
|
|
43
|
+
'engine/harness/toml.cjs',
|
|
44
|
+
'engine/harness/sources/claude.cjs',
|
|
45
|
+
'engine/harness/sources/codex.cjs',
|
|
46
|
+
'engine/harness/targets/generic.cjs',
|
|
47
|
+
'engine/harness/targets/claude.cjs',
|
|
48
|
+
'engine/harness/targets/codex.cjs',
|
|
49
|
+
'engine/harness/targets/gemini.cjs',
|
|
50
|
+
'engine/harness/targets/agy.cjs',
|
|
51
|
+
'engine/harness/targets/cursor.cjs',
|
|
52
|
+
'engine/hooks/shim.cjs',
|
|
53
|
+
'core/templates/targets.json',
|
|
54
|
+
'core/safety/guards.json',
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
const posix = (p) => p.replace(/\\/g, '/')
|
|
58
|
+
const sha256 = (buf) => createHash('sha256').update(buf).digest('hex')
|
|
59
|
+
// Line endings are normalised before hashing so a checkout with autocrlf and one
|
|
60
|
+
// without agree on what "identical" means.
|
|
61
|
+
const hashFile = (file) => sha256(readFileSync(file).toString('latin1').replace(/\r\n/g, '\n'))
|
|
62
|
+
|
|
63
|
+
function git(cwd, args) {
|
|
64
|
+
try { return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() } catch { return null }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function walk(dir, out = []) {
|
|
68
|
+
for (const name of readdirSync(dir)) {
|
|
69
|
+
const full = join(dir, name)
|
|
70
|
+
if (statSync(full).isDirectory()) walk(full, out)
|
|
71
|
+
else out.push(full)
|
|
72
|
+
}
|
|
73
|
+
return out
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function readManifest() {
|
|
77
|
+
return JSON.parse(readFileSync(MANIFEST, 'utf8'))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function sync(upstream) {
|
|
81
|
+
const src = resolve(upstream)
|
|
82
|
+
for (const rel of FILES) if (!existsSync(join(src, rel))) throw new Error(`upstream is missing ${rel}: ${src}`)
|
|
83
|
+
if (existsSync(VENDOR_DIR)) {
|
|
84
|
+
for (const f of walk(VENDOR_DIR)) if (posix(relative(VENDOR_DIR, f)) !== 'UPSTREAM.json') rmSync(f, { force: true })
|
|
85
|
+
}
|
|
86
|
+
const files = {}
|
|
87
|
+
for (const rel of FILES) {
|
|
88
|
+
const dest = join(VENDOR_DIR, rel)
|
|
89
|
+
mkdirSync(dirname(dest), { recursive: true })
|
|
90
|
+
copyFileSync(join(src, rel), dest)
|
|
91
|
+
files[rel] = hashFile(dest)
|
|
92
|
+
}
|
|
93
|
+
const commit = git(src, ['rev-parse', 'HEAD'])
|
|
94
|
+
const dirty = git(src, ['status', '--porcelain']) || ''
|
|
95
|
+
const manifest = {
|
|
96
|
+
_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.',
|
|
97
|
+
upstream: 'https://github.com/ucsandman/Agnostic-AI',
|
|
98
|
+
license: 'MIT (see LICENSE beside this file)',
|
|
99
|
+
commit,
|
|
100
|
+
upstreamDirty: Boolean(dirty.split('\n').filter(Boolean).some((l) => FILES.some((rel) => l.endsWith(rel)))),
|
|
101
|
+
syncedAt: new Date().toISOString(),
|
|
102
|
+
files,
|
|
103
|
+
}
|
|
104
|
+
writeFileSync(MANIFEST, JSON.stringify(manifest, null, 2) + '\n')
|
|
105
|
+
return manifest
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function check() {
|
|
109
|
+
const manifest = readManifest()
|
|
110
|
+
const problems = []
|
|
111
|
+
let checked = 0
|
|
112
|
+
for (const [rel, hash] of Object.entries(manifest.files)) {
|
|
113
|
+
checked++
|
|
114
|
+
const file = join(VENDOR_DIR, rel)
|
|
115
|
+
if (!existsSync(file)) { problems.push(`${rel}: missing`); continue }
|
|
116
|
+
if (hashFile(file) !== hash) problems.push(`${rel}: differs from the recorded upstream hash (edit upstream, then re-run the sync)`)
|
|
117
|
+
}
|
|
118
|
+
for (const f of walk(VENDOR_DIR)) {
|
|
119
|
+
const rel = posix(relative(VENDOR_DIR, f))
|
|
120
|
+
if (rel === 'UNOWNED' || rel === 'UPSTREAM.json') continue
|
|
121
|
+
checked++
|
|
122
|
+
if (!manifest.files[rel]) problems.push(`${rel}: not in UPSTREAM.json (only the sync script adds files here)`)
|
|
123
|
+
}
|
|
124
|
+
return { problems, checked, commit: manifest.commit }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function diff(upstream) {
|
|
128
|
+
const manifest = readManifest()
|
|
129
|
+
const src = resolve(upstream)
|
|
130
|
+
const changed = []
|
|
131
|
+
let checked = 0
|
|
132
|
+
for (const rel of Object.keys(manifest.files)) {
|
|
133
|
+
checked++
|
|
134
|
+
const theirs = join(src, rel)
|
|
135
|
+
if (!existsSync(theirs)) { changed.push(`${rel}: gone upstream`); continue }
|
|
136
|
+
if (hashFile(theirs) !== hashFile(join(VENDOR_DIR, rel))) changed.push(rel)
|
|
137
|
+
}
|
|
138
|
+
return { changed, checked, commit: git(src, ['rev-parse', 'HEAD']) }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
142
|
+
const [mode, arg] = process.argv.slice(2)
|
|
143
|
+
if (mode === '--check') {
|
|
144
|
+
const r = check()
|
|
145
|
+
for (const p of r.problems) process.stdout.write(` ${p}\n`)
|
|
146
|
+
process.stdout.write(`harness-engine: ${r.problems.length ? 'DRIFT' : 'ok'} checked=${r.checked} upstream=${r.commit ?? 'unknown'}\n`)
|
|
147
|
+
process.exit(r.problems.length ? 1 : 0)
|
|
148
|
+
}
|
|
149
|
+
if (mode === '--diff') {
|
|
150
|
+
if (!arg) { process.stderr.write('usage: sync-harness-engine.mjs --diff <agnostic-ai checkout>\n'); process.exit(2) }
|
|
151
|
+
const r = diff(arg)
|
|
152
|
+
for (const p of r.changed) process.stdout.write(` ${p}\n`)
|
|
153
|
+
process.stdout.write(`harness-engine: ${r.changed.length} of ${r.checked} vendored file(s) differ from ${arg} (${r.commit ?? 'no git'})\n`)
|
|
154
|
+
process.exit(r.changed.length ? 1 : 0)
|
|
155
|
+
}
|
|
156
|
+
if (!mode || mode.startsWith('--')) { process.stderr.write('usage: sync-harness-engine.mjs <agnostic-ai checkout> | --check | --diff <checkout>\n'); process.exit(2) }
|
|
157
|
+
const m = sync(mode)
|
|
158
|
+
process.stdout.write(`harness-engine: vendored ${Object.keys(m.files).length} file(s) from ${m.commit ?? 'no git'}${m.upstreamDirty ? ' (upstream working tree had uncommitted engine changes)' : ''}\n`)
|
|
159
|
+
}
|