pantheon-opencode 1.6.0-beta.1 → 1.6.0-beta.10
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/AGENTS.global.md +182 -0
- package/AGENTS.md +10 -891
- package/CHANGELOG.md +326 -0
- package/README.md +496 -32
- package/README.pt-BR.md +285 -20
- package/bin/pantheon-init.mjs +26 -3
- package/commands/pantheon-audit.md +7 -1
- package/commands/pantheon-consolidate.md +1 -1
- package/commands/pantheon-deepwork.md +14 -3
- package/commands/pantheon-focus.md +17 -0
- package/commands/pantheon-vision.md +61 -12
- package/package.json +21 -14
- package/plugin.json +2 -2
- package/pyproject.toml +8 -6
- package/scripts/build-agents-md.mjs +374 -56
- package/scripts/changelog-merge.mjs +161 -0
- package/scripts/{code_mode_server.py → code_mode.py} +3 -3
- package/scripts/cost.mjs +244 -41
- package/scripts/doctor.mjs +151 -1
- package/scripts/install/agents-md.mjs +4 -4
- package/scripts/install/config-migration.mjs +317 -4
- package/scripts/install/health-check.mjs +21 -19
- package/scripts/install/interactive.mjs +1 -1
- package/scripts/install/opencode-version.mjs +246 -4
- package/scripts/install/opencode.mjs +627 -179
- package/scripts/install/plugin.mjs +43 -9
- package/scripts/install/shared.mjs +120 -12
- package/scripts/install/vision-prompt.mjs +161 -0
- package/scripts/install-mcp.mjs +28 -9
- package/scripts/{mcp_persistence_server.py → mcp_persistence.py} +2 -2
- package/{src/mcp/mcp_resources_server.py → scripts/mcp_resources.py} +2 -2
- package/scripts/{memory_mcp_server.py → memory_mcp.py} +2 -2
- package/scripts/probe-context-rehydrate.mjs +8 -2
- package/scripts/probe-pantheon-cost.mjs +71 -15
- package/scripts/run-node-tests.mjs +189 -0
- package/scripts/sync-artifacts.mjs +26 -13
- package/scripts/sync-tui.mjs +24 -3
- package/scripts/test-opencode-v2-sandbox.sh +180 -26
- package/scripts/validate-routing.mjs +3 -3
- package/scripts/validate-sandbox-install.mjs +138 -0
- package/scripts/versioning.mjs +13 -1
- package/scripts/vision-status.mjs +682 -0
- package/skills-lock.json +8 -8
- package/src/agents/aphrodite.md +20 -37
- package/src/agents/apollo.md +7 -3
- package/src/agents/athena.md +63 -35
- package/src/agents/demeter.md +8 -28
- package/src/agents/gaia.md +1 -1
- package/src/agents/hephaestus.md +10 -21
- package/src/agents/hermes.md +15 -38
- package/src/agents/iris.md +1 -1
- package/src/agents/mnemosyne.md +18 -54
- package/src/agents/nyx.md +1 -1
- package/src/agents/prometheus.md +16 -20
- package/src/agents/talos.md +10 -2
- package/src/agents/themis.md +5 -5
- package/src/agents/zeus.md +99 -129
- package/src/instructions/agent-return-format.instructions.md +18 -3
- package/src/instructions/backend-standards.instructions.md +2 -0
- package/src/instructions/frontend-standards.instructions.md +3 -1
- package/src/instructions/memory-protocol.instructions.md +11 -110
- package/src/instructions/zeus-anti-stall.instructions.md +14 -61
- package/src/instructions/zeus-communication-rules.instructions.md +0 -1
- package/src/instructions/zeus-council-synthesis.instructions.md +32 -260
- package/src/instructions/zeus-memory-operations.instructions.md +26 -0
- package/src/instructions/zeus-timeout-retry.instructions.md +40 -87
- package/src/mcp/{code_mode_server.py → code_mode.py} +3 -3
- package/src/mcp/eval_store.py +2 -2
- package/src/mcp/{mcp_persistence_server.py → mcp_persistence.py} +2 -2
- package/{scripts/mcp_resources_server.py → src/mcp/mcp_resources.py} +2 -2
- package/src/mcp/{memory_mcp_server.py → memory_mcp.py} +2 -2
- package/src/mcp/{pantheon_vision_server.py → pantheon_vision.py} +286 -42
- package/src/mcp/requirements-mcp.txt +6 -4
- package/src/mcp/requirements-vision.txt +2 -2
- package/src/pantheon/cost-command.ts +399 -101
- package/src/pantheon/delegation-enforce.ts +10 -1
- package/src/pantheon/presets.d.mts +0 -8
- package/src/pantheon/presets.mjs +5 -32
- package/src/pantheon/v2-bounded-read.ts +640 -0
- package/src/pantheon/v2-delegation-enforce.ts +214 -0
- package/src/pantheon/v2-execute-after.ts +186 -0
- package/src/pantheon/v2-tools.ts +429 -0
- package/src/pantheon/v2-unsupported.d.mts +11 -0
- package/src/pantheon/v2-unsupported.mjs +90 -0
- package/src/pantheon/v2-vision.ts +622 -0
- package/src/pantheon/vision-local.ts +531 -0
- package/src/pantheon/vision.ts +1160 -115
- package/src/plugin-v2.ts +1145 -219
- package/src/plugin.ts +12 -9
- package/src/plugins/tui/dist/server.d.ts +1 -2
- package/src/plugins/tui/dist/server.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui-v2.d.ts +154 -0
- package/src/plugins/tui/dist/tui-v2.d.ts.map +1 -0
- package/src/plugins/tui/dist/tui-v2.js +659 -0
- package/src/plugins/tui/dist/tui-v2.js.map +1 -0
- package/src/plugins/tui/dist/tui.d.ts +85 -82
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +72 -59
- package/src/plugins/tui/index-v2.ts +6 -0
- package/src/plugins/tui/package-lock.json +5516 -1353
- package/src/plugins/tui/package.json +22 -12
- package/src/plugins/tui/src/index-v2.tsx +734 -0
- package/src/plugins/tui/src/index.tsx +106 -85
- package/src/plugins/tui/src/opencode-v2.d.ts +13 -0
- package/src/plugins/tui/tsconfig.build.json +5 -0
- package/src/plugins/tui/tsconfig.typecheck.json +5 -0
- package/src/plugins/tui/tsdown.config.ts +22 -2
- package/src/plugins/tui/tui-v2.ts +2 -0
- package/src/plugins/tui/tui.ts +3 -0
- package/src/routing.yml +11 -21
- package/src/skills/README.md +3 -3
- package/src/skills/agent-coordination/SKILL.md +7 -2
- package/src/skills/artifact-management/SKILL.md +12 -5
- package/src/skills/auto-continue/SKILL.md +39 -30
- package/src/skills/code-review-checklist/SKILL.md +5 -5
- package/src/skills/context-compression/SKILL.md +26 -879
- package/src/skills/session-goal/SKILL.md +14 -5
- package/src/skills/spec-driven-development/SKILL.md +3 -1
- package/src/skills/tdd-with-agents/SKILL.md +18 -10
- package/opencode.json +0 -58
|
@@ -56,11 +56,48 @@ function setEnvironment(values) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Write the rows in the family the requested host generation actually uses.
|
|
61
|
+
*
|
|
62
|
+
* Call sites describe an assistant turn the way they always have
|
|
63
|
+
* (`role: 'assistant'`, plus a `phase`). This function translates that
|
|
64
|
+
* descriptor into what is on disk: for V2, `role` becomes the `type` COLUMN and
|
|
65
|
+
* `phase` is dropped, because a real V2 payload carries neither — measured, the
|
|
66
|
+
* `role` key is absent from 40052 of 40052 rows on a host 2.0.22 database.
|
|
67
|
+
*
|
|
68
|
+
* Keeping that translation in ONE place is the point: a fixture that wrote
|
|
69
|
+
* `role` into a session_message row asserts the V1 format under a V2 name, which
|
|
70
|
+
* is exactly how the stale V1 predicate in the CLI mirror stayed green — the
|
|
71
|
+
* synthetic database agreed with the bug instead of contradicting it.
|
|
72
|
+
*/
|
|
59
73
|
function createSyntheticDb(DatabaseSync, path, rows) {
|
|
60
74
|
const db = new DatabaseSync(path)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
75
|
+
if (requestedVersion === 'v2') {
|
|
76
|
+
db.exec(
|
|
77
|
+
'CREATE TABLE session_message (id TEXT, session_id TEXT, type TEXT, seq INTEGER, time_created INTEGER, time_updated INTEGER, data TEXT)',
|
|
78
|
+
)
|
|
79
|
+
const insert = db.prepare(
|
|
80
|
+
'INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES (?,?,?,?,?,?,?)',
|
|
81
|
+
)
|
|
82
|
+
rows.forEach((row, index) => {
|
|
83
|
+
const payload = { ...row }
|
|
84
|
+
delete payload.role
|
|
85
|
+
delete payload.phase
|
|
86
|
+
insert.run(
|
|
87
|
+
`msg_${index + 1}`,
|
|
88
|
+
'ses_probe',
|
|
89
|
+
row.role,
|
|
90
|
+
index + 1,
|
|
91
|
+
Date.now(),
|
|
92
|
+
Date.now(),
|
|
93
|
+
JSON.stringify(payload),
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
} else {
|
|
97
|
+
db.exec('CREATE TABLE message (data TEXT NOT NULL, time_created INTEGER NOT NULL)')
|
|
98
|
+
const insert = db.prepare('INSERT INTO message (data, time_created) VALUES (?, ?)')
|
|
99
|
+
for (const data of rows) insert.run(JSON.stringify(data), Date.now())
|
|
100
|
+
}
|
|
64
101
|
db.close()
|
|
65
102
|
}
|
|
66
103
|
|
|
@@ -129,7 +166,10 @@ async function main() {
|
|
|
129
166
|
const root = mkdtempSync(join(tmpdir(), 'pantheon-cost-probe-'))
|
|
130
167
|
const dataHome = join(root, 'data')
|
|
131
168
|
const opencodeDir = join(dataHome, 'opencode')
|
|
132
|
-
|
|
169
|
+
// Every real installation writes `opencode.db`; the v1/v2 distinction is the
|
|
170
|
+
// SCHEMA of that file, never its name. Deriving `opencode-v2.db` here made the
|
|
171
|
+
// probe assert a path that exists on no real machine.
|
|
172
|
+
const selectedDb = join(opencodeDir, 'opencode.db')
|
|
133
173
|
const otherDb = join(root, 'other.db')
|
|
134
174
|
const envDb = join(root, 'env.db')
|
|
135
175
|
const explicitDb = join(root, 'explicit.db')
|
|
@@ -139,7 +179,9 @@ async function main() {
|
|
|
139
179
|
try {
|
|
140
180
|
mkdirSync(opencodeDir, { recursive: true })
|
|
141
181
|
// These rows intentionally include a monetary-looking field to prove it is
|
|
142
|
-
// ignored; only input/output/total tokens may appear in the report.
|
|
182
|
+
// ignored; only input/output/total tokens may appear in the report. Every
|
|
183
|
+
// V2 phase reads back as `unknown` because the V2 reader emits that literal:
|
|
184
|
+
// a V2 payload carries no phase field for a reader to recover.
|
|
143
185
|
const selectedRows =
|
|
144
186
|
requestedVersion === 'v2'
|
|
145
187
|
? [
|
|
@@ -224,8 +266,8 @@ async function main() {
|
|
|
224
266
|
selectedOutput,
|
|
225
267
|
requestedVersion === 'v2'
|
|
226
268
|
? [
|
|
227
|
-
{ agent: 'aphrodite', phase: '
|
|
228
|
-
{ agent: 'zeus', phase: '
|
|
269
|
+
{ agent: 'aphrodite', phase: 'unknown', input: 8, output: 10, total: 18 },
|
|
270
|
+
{ agent: 'zeus', phase: 'unknown', input: 4, output: 6, total: 10 },
|
|
229
271
|
]
|
|
230
272
|
: [
|
|
231
273
|
{ agent: 'hermes', phase: 'plan', input: 13, output: 24, total: 37 },
|
|
@@ -250,7 +292,12 @@ async function main() {
|
|
|
250
292
|
{},
|
|
251
293
|
{ sessionID: 'cost-probe' },
|
|
252
294
|
)
|
|
253
|
-
|
|
295
|
+
// V2 has no phase to report, so the same row reads back as 'unknown'.
|
|
296
|
+
requireOutput(
|
|
297
|
+
output,
|
|
298
|
+
`| env-agent | ${requestedVersion === 'v2' ? 'unknown' : 'override'} | 1 | 2 | 3 |`,
|
|
299
|
+
'PANTHEON_COST_DB',
|
|
300
|
+
)
|
|
254
301
|
if (output.includes('wrong-version') || output.includes('env-agent') === false) {
|
|
255
302
|
throw new Error('PANTHEON_COST_DB did not override the version-selected database')
|
|
256
303
|
}
|
|
@@ -269,7 +316,11 @@ async function main() {
|
|
|
269
316
|
{},
|
|
270
317
|
{ sessionID: 'cost-probe' },
|
|
271
318
|
)
|
|
272
|
-
requireOutput(
|
|
319
|
+
requireOutput(
|
|
320
|
+
output,
|
|
321
|
+
`| explicit-agent | ${requestedVersion === 'v2' ? 'unknown' : 'explicit'} | 9 | 1 | 10 |`,
|
|
322
|
+
'explicit dbPath',
|
|
323
|
+
)
|
|
273
324
|
if (output.includes('env-agent')) throw new Error('explicit dbPath did not take precedence')
|
|
274
325
|
checks.push('explicit dbPath precedence: PASS')
|
|
275
326
|
} finally {
|
|
@@ -287,11 +338,7 @@ async function main() {
|
|
|
287
338
|
{ sessionID: 'cost-probe' },
|
|
288
339
|
)
|
|
289
340
|
requireOutput(output, 'not found', 'missing DB')
|
|
290
|
-
requireOutput(
|
|
291
|
-
output,
|
|
292
|
-
requestedVersion === 'v2' ? 'opencode-v2.db' : 'opencode.db',
|
|
293
|
-
'selected missing DB path',
|
|
294
|
-
)
|
|
341
|
+
requireOutput(output, 'opencode.db', 'selected missing DB path')
|
|
295
342
|
checks.push('missing DB: PASS')
|
|
296
343
|
} finally {
|
|
297
344
|
missingRestore()
|
|
@@ -302,7 +349,16 @@ async function main() {
|
|
|
302
349
|
}).pantheon_cost.execute({}, { sessionID: 'cost-probe' })
|
|
303
350
|
requireOutput(incompatibleOutput, 'incompatible opencode.db schema', 'incompatible schema')
|
|
304
351
|
checks.push('incompatible schema: PASS')
|
|
305
|
-
|
|
352
|
+
// Names the synthetic fixture that was built, because the previous wording
|
|
353
|
+
// ("no real DB was read") was true of every run and therefore true of the run
|
|
354
|
+
// in which the V2 ledger was read as V1 and silently dropped. A verdict that
|
|
355
|
+
// cannot fail is not a verdict: it stayed green across the exact defect it
|
|
356
|
+
// was meant to cover.
|
|
357
|
+
emit(
|
|
358
|
+
'PASS',
|
|
359
|
+
`${checks.length} synthetic ${requestedVersion} checks passed against a temporary opencode.db (no user database was read)`,
|
|
360
|
+
checks,
|
|
361
|
+
)
|
|
306
362
|
} catch (error) {
|
|
307
363
|
emit('FAIL', diagnostic(error), checks)
|
|
308
364
|
} finally {
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Run `node --test` over an explicit file list, and FAIL when zero tests are collected.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: `node --test <glob>` exits 0 when the glob matches nothing (Node >= 22).
|
|
6
|
+
* A test script whose file set is absent from the checkout -- or absent from the published
|
|
7
|
+
* package, because the `files` allow-list omits it -- therefore reports success while
|
|
8
|
+
* collecting nothing. That is the worst possible failure mode for a release gate: it
|
|
9
|
+
* manufactures confidence.
|
|
10
|
+
*
|
|
11
|
+
* This runner closes that hole from two independent sides:
|
|
12
|
+
* 1. It expands the globs ITSELF and refuses to start the runner with zero files, so the
|
|
13
|
+
* runner is never handed an unexpanded pattern that silently matches nothing.
|
|
14
|
+
* 2. After a run that exited 0, it re-reads the collected count from the machine-readable
|
|
15
|
+
* TAP summary and fails closed when that count is 0, unparseable, or missing.
|
|
16
|
+
*
|
|
17
|
+
* Side 2 is deliberately independent of side 1: a future Node change to `--test` glob
|
|
18
|
+
* handling, a glob that expands to a directory of empty files, or an emptied test file all
|
|
19
|
+
* still get caught.
|
|
20
|
+
*
|
|
21
|
+
* Human-readable output is unchanged: the default `spec` reporter still goes to stdout, and
|
|
22
|
+
* the TAP reporter is diverted to a scratch file that is read once and deleted.
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* node scripts/run-node-tests.mjs [--label <name>] [-- <node flags...>] <glob>...
|
|
26
|
+
*
|
|
27
|
+
* Examples:
|
|
28
|
+
* node scripts/run-node-tests.mjs 'tests/*.mjs' 'tests/pantheon/*.mjs'
|
|
29
|
+
* node scripts/run-node-tests.mjs -- --experimental-test-coverage 'tests/*.mjs'
|
|
30
|
+
*/
|
|
31
|
+
import { spawnSync } from 'node:child_process'
|
|
32
|
+
import { globSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
|
33
|
+
import { tmpdir } from 'node:os'
|
|
34
|
+
import { join } from 'node:path'
|
|
35
|
+
import { fileURLToPath } from 'node:url'
|
|
36
|
+
|
|
37
|
+
const ROOT = join(fileURLToPath(new URL('.', import.meta.url)), '..')
|
|
38
|
+
|
|
39
|
+
function usage(message) {
|
|
40
|
+
console.error(`zero-test-guard: ${message}`)
|
|
41
|
+
console.error(
|
|
42
|
+
'usage: node scripts/run-node-tests.mjs [--label <name>] [-- <node flags...>] <glob>...',
|
|
43
|
+
)
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Parse `[--label <name>] [-- <node flags...>] <glob>...`. */
|
|
48
|
+
function parseArgs(argv) {
|
|
49
|
+
const separator = argv.indexOf('--')
|
|
50
|
+
const own = separator === -1 ? argv : argv.slice(0, separator)
|
|
51
|
+
const nodeFlags = separator === -1 ? [] : argv.slice(separator + 1)
|
|
52
|
+
|
|
53
|
+
let label = 'node tests'
|
|
54
|
+
const globs = []
|
|
55
|
+
for (let i = 0; i < own.length; i++) {
|
|
56
|
+
if (own[i] === '--label') {
|
|
57
|
+
const value = own[i + 1]
|
|
58
|
+
if (value === undefined) usage('--label requires a value')
|
|
59
|
+
label = value
|
|
60
|
+
i += 1
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
if (own[i].startsWith('-')) usage(`unknown option ${own[i]}`)
|
|
64
|
+
globs.push(own[i])
|
|
65
|
+
}
|
|
66
|
+
if (globs.length === 0) usage('at least one glob is required')
|
|
67
|
+
return { label, globs, nodeFlags }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Expand globs to a deduped, ordered file list.
|
|
72
|
+
* Order follows the positional order of the globs so that the suite runs in the same order
|
|
73
|
+
* as the shell used to; within a glob, matches are sorted for determinism.
|
|
74
|
+
*/
|
|
75
|
+
function expand(globs) {
|
|
76
|
+
const files = []
|
|
77
|
+
const seen = new Set()
|
|
78
|
+
for (const pattern of globs) {
|
|
79
|
+
const matches = globSync(pattern, { cwd: ROOT })
|
|
80
|
+
.filter((match) => !match.includes('node_modules') && !match.includes('__pycache__'))
|
|
81
|
+
.sort()
|
|
82
|
+
for (const match of matches) {
|
|
83
|
+
if (seen.has(match)) continue
|
|
84
|
+
seen.add(match)
|
|
85
|
+
files.push(match)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return files
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function reportMissingFiles(label, globs) {
|
|
92
|
+
console.error(`zero-test-guard: ${label} matched 0 test files — refusing to report success.`)
|
|
93
|
+
console.error(` globs: ${globs.join(' ')}`)
|
|
94
|
+
console.error(' This is the "test suite silently passes when it collects nothing" failure.')
|
|
95
|
+
console.error(' Either the test files are missing from this checkout, or they are missing')
|
|
96
|
+
console.error(' from the published package (check the "files" allow-list in package.json).')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function reportZeroCollected(label, files) {
|
|
100
|
+
console.error(`zero-test-guard: ${label} collected 0 tests — refusing to report success.`)
|
|
101
|
+
console.error(` ${files.length} file(s) matched, but not one test case was collected:`)
|
|
102
|
+
for (const file of files) console.error(` ${file}`)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Count the real test cases collected in a TAP summary.
|
|
107
|
+
*
|
|
108
|
+
* Node counts a test FILE that declares no test cases as one passing "test"
|
|
109
|
+
* (`ok 1 - tests/foo.test.mjs`), so the summary's `# tests N` is >= the file count and
|
|
110
|
+
* can never reach 0 once any file matched. Relying on it alone would let a suite whose
|
|
111
|
+
* every test file was emptied out report success.
|
|
112
|
+
*
|
|
113
|
+
* The reliable discriminator is the subtest NAME: a file-level placeholder is named with
|
|
114
|
+
* the file path that was passed to the runner, a real test case is named with its test
|
|
115
|
+
* name. So a top-level subtest whose name is one of our matched paths is a placeholder
|
|
116
|
+
* and is not counted as a collected case.
|
|
117
|
+
*
|
|
118
|
+
* Returns null when the summary cannot be read or parsed -- the caller fails closed.
|
|
119
|
+
*/
|
|
120
|
+
function countCollectedTests(tapPath, files) {
|
|
121
|
+
let tap = ''
|
|
122
|
+
try {
|
|
123
|
+
tap = readFileSync(tapPath, 'utf8')
|
|
124
|
+
} catch {
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
// The summary count is still asserted to exist: without it the stream is not TAP.
|
|
128
|
+
if (!/^#\s*tests\s+\d+\s*$/m.test(tap)) return null
|
|
129
|
+
|
|
130
|
+
const placeholders = new Set(files)
|
|
131
|
+
let collected = 0
|
|
132
|
+
for (const match of tap.matchAll(/^# Subtest: (.+)$/gm)) {
|
|
133
|
+
if (placeholders.has(match[1].trim())) continue
|
|
134
|
+
collected += 1
|
|
135
|
+
}
|
|
136
|
+
return collected
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const { label, globs, nodeFlags } = parseArgs(process.argv.slice(2))
|
|
140
|
+
const files = expand(globs)
|
|
141
|
+
|
|
142
|
+
if (files.length === 0) {
|
|
143
|
+
reportMissingFiles(label, globs)
|
|
144
|
+
process.exit(1)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const scratch = mkdtempSync(join(tmpdir(), 'pantheon-node-tests-'))
|
|
148
|
+
const tapPath = join(scratch, 'summary.tap')
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const run = spawnSync(
|
|
152
|
+
process.execPath,
|
|
153
|
+
[
|
|
154
|
+
'--test',
|
|
155
|
+
'--test-reporter=spec',
|
|
156
|
+
'--test-reporter-destination=stdout',
|
|
157
|
+
'--test-reporter=tap',
|
|
158
|
+
`--test-reporter-destination=${tapPath}`,
|
|
159
|
+
...nodeFlags,
|
|
160
|
+
...files,
|
|
161
|
+
],
|
|
162
|
+
{ cwd: ROOT, stdio: 'inherit' },
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if (run.error) {
|
|
166
|
+
console.error(`zero-test-guard: could not start the test runner: ${run.error.message}`)
|
|
167
|
+
process.exit(1)
|
|
168
|
+
}
|
|
169
|
+
if (run.signal) {
|
|
170
|
+
console.error(`zero-test-guard: the test runner was killed by ${run.signal}`)
|
|
171
|
+
process.exit(1)
|
|
172
|
+
}
|
|
173
|
+
// Real failures keep the runner's own exit code; the guard only speaks up when the
|
|
174
|
+
// runner claimed success it cannot justify.
|
|
175
|
+
if (run.status !== 0) process.exit(run.status)
|
|
176
|
+
|
|
177
|
+
const collected = countCollectedTests(tapPath, files)
|
|
178
|
+
if (collected === null) {
|
|
179
|
+
console.error('zero-test-guard: could not read the collected test count from the TAP summary.')
|
|
180
|
+
console.error(' Failing closed: an unreadable summary cannot certify that any test ran.')
|
|
181
|
+
process.exit(1)
|
|
182
|
+
}
|
|
183
|
+
if (collected === 0) {
|
|
184
|
+
reportZeroCollected(label, files)
|
|
185
|
+
process.exit(1)
|
|
186
|
+
}
|
|
187
|
+
} finally {
|
|
188
|
+
rmSync(scratch, { recursive: true, force: true })
|
|
189
|
+
}
|
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs'
|
|
21
21
|
import { join } from 'node:path'
|
|
22
|
+
import { copyAgentPrompts, writeAgentsMdManaged } from './build-agents-md.mjs'
|
|
22
23
|
|
|
23
24
|
import {
|
|
24
25
|
collectSkillNames,
|
|
25
|
-
copyFiles,
|
|
26
26
|
installSkills,
|
|
27
27
|
ROOT,
|
|
28
28
|
syncDir,
|
|
@@ -31,15 +31,16 @@ import {
|
|
|
31
31
|
|
|
32
32
|
/** Canonical MCP script sources — mirrors installOpenCode phase 2.10. */
|
|
33
33
|
const MCP_SCRIPTS = {
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'
|
|
34
|
+
'mcp_resources.py': () => join(ROOT, 'src', 'mcp', 'mcp_resources.py'),
|
|
35
|
+
'code_mode.py': () => join(ROOT, 'src', 'mcp', 'code_mode.py'),
|
|
36
|
+
'memory_mcp.py': () => join(ROOT, 'src', 'mcp', 'memory_mcp.py'),
|
|
37
|
+
'mcp_persistence.py': () => join(ROOT, 'src', 'mcp', 'mcp_persistence.py'),
|
|
38
38
|
'_pantheon_paths.py': () => join(ROOT, 'src', 'mcp', '_pantheon_paths.py'),
|
|
39
39
|
'mcp_codemap_module.py': () => join(ROOT, 'src', 'mcp', 'mcp_codemap_module.py'),
|
|
40
|
-
'
|
|
40
|
+
'pantheon_vision.py': () => join(ROOT, 'src', 'mcp', 'pantheon_vision.py'),
|
|
41
41
|
'eval_store.py': () => join(ROOT, 'src', 'mcp', 'eval_store.py'),
|
|
42
42
|
'scrub-secrets.py': () => join(ROOT, 'scripts', 'scrub-secrets.py'),
|
|
43
|
+
'themis_heuristic_scan.py': () => join(ROOT, 'scripts', 'themis_heuristic_scan.py'),
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/**
|
|
@@ -59,7 +60,7 @@ export function syncCopyArtifacts(configDir) {
|
|
|
59
60
|
const dstAgents = join(configDir, 'agents')
|
|
60
61
|
if (existsSync(join(ROOT, 'src', 'agents'))) {
|
|
61
62
|
mkdirSync(dstAgents, { recursive: true })
|
|
62
|
-
bump(
|
|
63
|
+
bump(copyAgentPrompts(join(ROOT, 'src', 'agents'), dstAgents, false))
|
|
63
64
|
const routing = join(ROOT, 'src', 'routing.yml')
|
|
64
65
|
if (existsSync(routing)) {
|
|
65
66
|
if (
|
|
@@ -82,15 +83,27 @@ export function syncCopyArtifacts(configDir) {
|
|
|
82
83
|
result.errors.push(`skills: ${err.message}`)
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
// ── instructions (AGENTS.md) ──
|
|
86
|
+
// ── instructions (global AGENTS.md artifact) ──
|
|
87
|
+
// The config dir is GLOBAL context: it receives the repo-agnostic global
|
|
88
|
+
// artifact, never the repository artifact. Managed-region writer keeps any
|
|
89
|
+
// user content around the markers intact.
|
|
86
90
|
try {
|
|
87
|
-
const src = join(ROOT, 'AGENTS.md')
|
|
91
|
+
const src = join(ROOT, 'AGENTS.global.md')
|
|
88
92
|
if (existsSync(src)) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
const status = writeAgentsMdManaged(
|
|
94
|
+
join(configDir, 'AGENTS.md'),
|
|
95
|
+
readFileSync(src, 'utf8'),
|
|
96
|
+
false,
|
|
97
|
+
)
|
|
98
|
+
if (status === 'skipped') result.skipped++
|
|
99
|
+
else {
|
|
92
100
|
result.created++
|
|
93
|
-
|
|
101
|
+
if (status === 'merged') {
|
|
102
|
+
result.errors.push(
|
|
103
|
+
'instructions: non-generated content found in AGENTS.md — preserved around the Pantheon managed region',
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
94
107
|
}
|
|
95
108
|
} catch (err) {
|
|
96
109
|
result.errors.push(`instructions: ${err.message}`)
|
package/scripts/sync-tui.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync
|
|
|
14
14
|
import { homedir } from 'node:os'
|
|
15
15
|
import { join } from 'node:path'
|
|
16
16
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
17
|
+
import { resolveOpenCodeVersion } from './install/opencode-version.mjs'
|
|
17
18
|
|
|
18
19
|
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
|
19
20
|
const ROOT = join(__dirname, '..')
|
|
@@ -63,7 +64,7 @@ function resolveConfigDir() {
|
|
|
63
64
|
* The raw TSX source is never copied — the loader consumes only the bundle.
|
|
64
65
|
* @returns {{ created: number, skipped: number }}
|
|
65
66
|
*/
|
|
66
|
-
function copyPluginFiles(srcDir, dstDir) {
|
|
67
|
+
function copyPluginFiles(srcDir, dstDir, version) {
|
|
67
68
|
const result = { created: 0, skipped: 0 }
|
|
68
69
|
|
|
69
70
|
mkdirSync(dstDir, { recursive: true })
|
|
@@ -87,11 +88,30 @@ function copyPluginFiles(srcDir, dstDir) {
|
|
|
87
88
|
// package.json
|
|
88
89
|
const pkgSrc = join(srcDir, 'package.json')
|
|
89
90
|
if (existsSync(pkgSrc)) {
|
|
90
|
-
const
|
|
91
|
+
const packageJson = JSON.parse(readFileSync(pkgSrc, 'utf8'))
|
|
92
|
+
if (version === 'v2' && packageJson.exports?.['./tui-v2']) {
|
|
93
|
+
packageJson.exports['./tui'] = packageJson.exports['./tui-v2']
|
|
94
|
+
}
|
|
95
|
+
const content = `${JSON.stringify(packageJson, null, 2)}\n`
|
|
91
96
|
if (writeIfChanged(join(dstDir, 'package.json'), content)) result.created++
|
|
92
97
|
else result.skipped++
|
|
93
98
|
}
|
|
94
99
|
|
|
100
|
+
const tuiEntry = join(srcDir, version === 'v2' ? 'tui-v2.ts' : 'tui.ts')
|
|
101
|
+
if (existsSync(tuiEntry)) {
|
|
102
|
+
if (writeIfChanged(join(dstDir, 'tui.ts'), readFileSync(tuiEntry, 'utf8'))) result.created++
|
|
103
|
+
else result.skipped++
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const v2ServerEntry = join(srcDir, 'index-v2.ts')
|
|
107
|
+
const v2ServerDestination = join(dstDir, 'index.ts')
|
|
108
|
+
if (version === 'v2' && existsSync(v2ServerEntry)) {
|
|
109
|
+
if (writeIfChanged(v2ServerDestination, readFileSync(v2ServerEntry, 'utf8'))) result.created++
|
|
110
|
+
else result.skipped++
|
|
111
|
+
} else if (existsSync(v2ServerDestination)) {
|
|
112
|
+
rmSync(v2ServerDestination, { force: true })
|
|
113
|
+
}
|
|
114
|
+
|
|
95
115
|
// Keep the copied plugin installable with deterministic npm ci.
|
|
96
116
|
const lockSrc = join(srcDir, 'package-lock.json')
|
|
97
117
|
if (existsSync(lockSrc)) {
|
|
@@ -169,7 +189,8 @@ async function main() {
|
|
|
169
189
|
const sourceVersion = readVersion(join(tuiSrcDir, 'package.json'))
|
|
170
190
|
|
|
171
191
|
// Copy fresh files
|
|
172
|
-
const {
|
|
192
|
+
const version = resolveOpenCodeVersion('auto', { warn: (message) => console.warn(message) })
|
|
193
|
+
const { created } = copyPluginFiles(tuiSrcDir, tuiCopyDir, version)
|
|
173
194
|
|
|
174
195
|
// Lockfile installation is the only accepted dependency path. Strip
|
|
175
196
|
// global-context npm vars so a global postinstall can't trigger ECIGLOBAL.
|