@hongmaple0820/scale-engine 0.40.1 → 0.40.2
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/dist/api/cli.js +267 -7
- package/dist/api/cli.js.map +1 -1
- package/dist/api/doctor.js +1 -1
- package/dist/api/doctor.js.map +1 -1
- package/dist/bootstrap/DependencyBootstrap.d.ts +1 -0
- package/dist/bootstrap/DependencyBootstrap.js +137 -25
- package/dist/bootstrap/DependencyBootstrap.js.map +1 -1
- package/dist/capabilities/InstalledSkillsIntegration.js +29 -9
- package/dist/capabilities/InstalledSkillsIntegration.js.map +1 -1
- package/dist/context/ContextBudget.js +2 -2
- package/dist/core/GbrainRuntime.d.ts +25 -0
- package/dist/core/GbrainRuntime.js +270 -0
- package/dist/core/GbrainRuntime.js.map +1 -0
- package/dist/env/EnvironmentDoctor.js +221 -5
- package/dist/env/EnvironmentDoctor.js.map +1 -1
- package/dist/memory/MemoryProviders.js +38 -91
- package/dist/memory/MemoryProviders.js.map +1 -1
- package/dist/runtime/ModelUsageLedger.d.ts +53 -2
- package/dist/runtime/ModelUsageLedger.js +243 -39
- package/dist/runtime/ModelUsageLedger.js.map +1 -1
- package/dist/setup/SetupVerification.d.ts +42 -0
- package/dist/setup/SetupVerification.js +180 -0
- package/dist/setup/SetupVerification.js.map +1 -0
- package/dist/tools/ToolCapabilityRegistry.js +10 -0
- package/dist/tools/ToolCapabilityRegistry.js.map +1 -1
- package/dist/workflow/VerificationProfile.js +1 -1
- package/dist/workflow/VerificationProfile.js.map +1 -1
- package/docs/CONTEXT_BUDGET.md +12 -2
- package/docs/GOVERNANCE_DASHBOARD.md +7 -0
- package/docs/start/quickstart.md +1 -0
- package/package.json +3 -2
- package/scripts/workflow/lib/gbrain-runtime.mjs +185 -0
- package/scripts/workflow/lib/report-output.mjs +107 -0
- package/scripts/workflow/provider-rehearsal.mjs +129 -48
- package/scripts/workflow/setup-smoke.mjs +142 -8
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
function stripAnsi(value) {
|
|
2
|
+
return String(value ?? '').replace(/\u001B\[[0-9;]*m/g, '')
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function summarizeCommandOutput(name, stream, value, max = 1600) {
|
|
6
|
+
const sanitized = sanitizeCommandOutput(name, stream, stripAnsi(value))
|
|
7
|
+
if (!sanitized.trim()) return ''
|
|
8
|
+
const summary = commandSpecificSummary(name, stream, sanitized)
|
|
9
|
+
return compactText(summary ?? sanitized, max)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function summarizeCommandRecord(record) {
|
|
13
|
+
if (!record) return record
|
|
14
|
+
const {
|
|
15
|
+
stdout,
|
|
16
|
+
stderr,
|
|
17
|
+
...rest
|
|
18
|
+
} = record
|
|
19
|
+
return {
|
|
20
|
+
...rest,
|
|
21
|
+
stdoutTail: summarizeCommandOutput(record.name, 'stdout', stdout ?? record.stdoutTail ?? ''),
|
|
22
|
+
stderrTail: summarizeCommandOutput(record.name, 'stderr', stderr ?? record.stderrTail ?? ''),
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sanitizeCommandOutput(name, stream, value) {
|
|
27
|
+
let text = String(value ?? '').replace(/\r\n/g, '\n').replace(/[�]+/g, '')
|
|
28
|
+
if (isGbrainCommand(name)) {
|
|
29
|
+
if (stream === 'stderr') {
|
|
30
|
+
text = text
|
|
31
|
+
.replace(/^\s*The system cannot find the path specified\.\s*$/gim, '')
|
|
32
|
+
.replace(/\n?={20,}\n[\s\S]*?The user owns this decision\.\n={20,}\n?/g, '\n')
|
|
33
|
+
}
|
|
34
|
+
if (stream === 'stdout' && /init/i.test(name)) {
|
|
35
|
+
text = text
|
|
36
|
+
.replace(/\n?═{10,}\n\[gbrain\] search mode tentatively set[\s\S]*?To see what is running: gbrain search modes\n*/g, '\n')
|
|
37
|
+
.replace(/\n--- GBrain Mod Status ---[\s\S]*$/g, '')
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return normalizeMultiline(text)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function commandSpecificSummary(name, stream, value) {
|
|
44
|
+
if (stream !== 'stdout') return undefined
|
|
45
|
+
if (name === 'gbrain-init' || name === 'gbrain-init-isolated-home') {
|
|
46
|
+
return collectMatchingLines(value, [
|
|
47
|
+
/migration\(s\) applied/i,
|
|
48
|
+
/^Brain ready at /i,
|
|
49
|
+
/^0 pages\./i,
|
|
50
|
+
/^Next: /i,
|
|
51
|
+
/^When you outgrow local:/i,
|
|
52
|
+
])
|
|
53
|
+
}
|
|
54
|
+
if (name === 'graphify-update' || name === 'graphify-extract') {
|
|
55
|
+
return collectMatchingLines(value, [
|
|
56
|
+
/Rebuilt/i,
|
|
57
|
+
/graph\.json updated/i,
|
|
58
|
+
/^Code graph updated\./i,
|
|
59
|
+
/^Tip:/i,
|
|
60
|
+
])
|
|
61
|
+
}
|
|
62
|
+
if (name === 'graphify-benchmark') {
|
|
63
|
+
return collectMatchingLines(value, [
|
|
64
|
+
/^graphify token reduction benchmark$/i,
|
|
65
|
+
/^\s*Corpus:/i,
|
|
66
|
+
/^\s*Graph:/i,
|
|
67
|
+
/^\s*Avg query cost:/i,
|
|
68
|
+
/^\s*Reduction:/i,
|
|
69
|
+
])
|
|
70
|
+
}
|
|
71
|
+
if (name === 'graphify-query') {
|
|
72
|
+
return firstInterestingLines(value, 12)
|
|
73
|
+
}
|
|
74
|
+
return undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function collectMatchingLines(value, patterns) {
|
|
78
|
+
const lines = normalizeMultiline(value).split('\n').map(line => line.trim()).filter(Boolean)
|
|
79
|
+
const selected = lines.filter(line => patterns.some(pattern => pattern.test(line)))
|
|
80
|
+
return selected.length > 0 ? selected.join('\n') : undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function firstInterestingLines(value, maxLines) {
|
|
84
|
+
const lines = normalizeMultiline(value)
|
|
85
|
+
.split('\n')
|
|
86
|
+
.map(line => line.trim())
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.filter(line => !/^[=._-]{6,}$/.test(line))
|
|
89
|
+
return lines.slice(0, maxLines).join('\n')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function compactText(value, max) {
|
|
93
|
+
const normalized = normalizeMultiline(value)
|
|
94
|
+
if (normalized.length <= max) return normalized
|
|
95
|
+
return `${normalized.slice(0, max - 1)}…`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalizeMultiline(value) {
|
|
99
|
+
return String(value ?? '')
|
|
100
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
101
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
102
|
+
.trim()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isGbrainCommand(name) {
|
|
106
|
+
return /^gbrain-/i.test(String(name ?? ''))
|
|
107
|
+
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from 'node:child_process'
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
4
|
import { tmpdir } from 'node:os'
|
|
5
5
|
import { dirname, extname, join, resolve } from 'node:path'
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import {
|
|
8
|
+
ensureMirroredGbrainInvocation,
|
|
9
|
+
normalizeGbrainSpawnResult,
|
|
10
|
+
resolveDirectWindowsGbrainInvocation,
|
|
11
|
+
shouldRetryWithMirroredGbrain,
|
|
12
|
+
} from './lib/gbrain-runtime.mjs'
|
|
13
|
+
import { summarizeCommandOutput, summarizeCommandRecord } from './lib/report-output.mjs'
|
|
7
14
|
|
|
8
15
|
const scriptDir = dirname(fileURLToPath(import.meta.url))
|
|
9
16
|
const repoRoot = resolve(scriptDir, '..', '..')
|
|
@@ -12,6 +19,7 @@ const runId = `provider-rehearsal-${Date.now()}-${Math.random().toString(16).sli
|
|
|
12
19
|
const workRoot = options.outDir ? resolve(options.outDir) : join(tmpdir(), runId)
|
|
13
20
|
const results = []
|
|
14
21
|
const RTK_BYPASS_COMMANDS = new Set(['gbrain'])
|
|
22
|
+
const GRAPHIFY_ALLOWED_HIDDEN_TOP_LEVEL_DIRS = new Set(['.github', '.scale', '.storybook', '.vscode', '.cursor'])
|
|
15
23
|
|
|
16
24
|
mkdirSync(workRoot, { recursive: true })
|
|
17
25
|
|
|
@@ -27,14 +35,34 @@ try {
|
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
function runGbrainReplay() {
|
|
30
|
-
const
|
|
38
|
+
const init = runCommand('gbrain-init', 'gbrain', ['init', '--pglite', '--no-embedding'], {
|
|
39
|
+
timeoutMs: 120_000,
|
|
40
|
+
env: gbrainCommandEnv(),
|
|
41
|
+
})
|
|
42
|
+
if (init.exitCode !== 0) {
|
|
43
|
+
return capability('gbrain', options.requireGbrain ? 'failed' : 'blocked', {
|
|
44
|
+
reason: failureLine(`${init.stdout}\n${init.stderr}`) || 'gbrain init failed for the isolated smoke brain',
|
|
45
|
+
required: options.requireGbrain,
|
|
46
|
+
commands: [init],
|
|
47
|
+
nextCommands: [
|
|
48
|
+
'gbrain init --pglite --no-embedding',
|
|
49
|
+
'gbrain doctor --json',
|
|
50
|
+
'npm run smoke:gbrain',
|
|
51
|
+
],
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const healthCheck = runCommand('gbrain-health', 'gbrain', ['list', '-n', '1'], {
|
|
56
|
+
timeoutMs: 60_000,
|
|
57
|
+
env: gbrainCommandEnv(),
|
|
58
|
+
})
|
|
31
59
|
const health = evaluateGbrainList(healthCheck)
|
|
32
60
|
if (!health.available) {
|
|
33
61
|
return capability('gbrain', options.requireGbrain ? 'failed' : 'blocked', {
|
|
34
62
|
reason: health.reason || failureLine(`${healthCheck.stdout}\n${healthCheck.stderr}`) || 'gbrain health check failed',
|
|
35
63
|
required: options.requireGbrain,
|
|
36
64
|
health,
|
|
37
|
-
commands: [healthCheck],
|
|
65
|
+
commands: [init, healthCheck],
|
|
38
66
|
nextCommands: [
|
|
39
67
|
'gbrain init --pglite --no-embedding',
|
|
40
68
|
'gbrain init --supabase',
|
|
@@ -50,22 +78,27 @@ function runGbrainReplay() {
|
|
|
50
78
|
const sentinel = `scale-engine-gbrain-replay-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
|
51
79
|
const body = `# ${slug}. Sentinel: ${sentinel}. This page verifies that SCALE can write memory in one process and read/query it in later processes.`
|
|
52
80
|
|
|
53
|
-
const put = runCommand('gbrain-put', 'gbrain', ['put', slug, '--content', body], { timeoutMs: 60_000 })
|
|
54
|
-
const get = runCommand('gbrain-get', 'gbrain', ['get', slug], { timeoutMs: 8_000 })
|
|
55
|
-
const query = runCommand('gbrain-query', 'gbrain', ['query', sentinel], { timeoutMs: 8_000 })
|
|
81
|
+
const put = runCommand('gbrain-put', 'gbrain', ['put', slug, '--content', body], { timeoutMs: 60_000, env: gbrainCommandEnv() })
|
|
82
|
+
const get = runCommand('gbrain-get', 'gbrain', ['get', slug], { timeoutMs: 8_000, env: gbrainCommandEnv() })
|
|
83
|
+
const query = runCommand('gbrain-query', 'gbrain', ['query', sentinel], { timeoutMs: 8_000, env: gbrainCommandEnv() })
|
|
56
84
|
const queryOutput = `${query.stdout}\n${query.stderr}`
|
|
57
85
|
const search = query.exitCode === 0 && (queryOutput.includes(sentinel) || queryOutput.includes(slug))
|
|
58
86
|
? undefined
|
|
59
|
-
: runCommand('gbrain-search', 'gbrain', ['search', sentinel], { timeoutMs: 8_000 })
|
|
60
|
-
const cleanup = options.keepGbrainPage ? undefined : runCommand('gbrain-delete', 'gbrain', ['delete', slug], { timeoutMs: 60_000 })
|
|
87
|
+
: runCommand('gbrain-search', 'gbrain', ['search', sentinel], { timeoutMs: 8_000, env: gbrainCommandEnv() })
|
|
88
|
+
const cleanup = options.keepGbrainPage ? undefined : runCommand('gbrain-delete', 'gbrain', ['delete', slug], { timeoutMs: 60_000, env: gbrainCommandEnv() })
|
|
61
89
|
|
|
62
90
|
const recallOutput = `${queryOutput}\n${search?.stdout ?? ''}\n${search?.stderr ?? ''}`
|
|
63
|
-
const getPassed =
|
|
64
|
-
const recallPassed = (query.exitCode === 0 ||
|
|
91
|
+
const getPassed = get.exitCode === 0 && get.stdout.includes(sentinel)
|
|
92
|
+
const recallPassed = (query.exitCode === 0 || search?.exitCode === 0)
|
|
65
93
|
&& (recallOutput.includes(sentinel) || recallOutput.includes(slug))
|
|
66
94
|
const replayPassed = put.exitCode === 0
|
|
67
95
|
&& getPassed
|
|
68
96
|
&& recallPassed
|
|
97
|
+
const recoveredCommands = [get, query, search]
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
.filter(command => command.recoveredTimeout)
|
|
100
|
+
.map(command => command.name)
|
|
101
|
+
const recoveredTimeouts = replayPassed && recoveredCommands.length > 0
|
|
69
102
|
|
|
70
103
|
return capability('gbrain', replayPassed ? 'passed' : 'failed', {
|
|
71
104
|
reason: replayPassed
|
|
@@ -73,10 +106,17 @@ function runGbrainReplay() {
|
|
|
73
106
|
: 'gbrain was configured, but write/get/query replay did not prove recall',
|
|
74
107
|
required: options.requireGbrain,
|
|
75
108
|
health,
|
|
109
|
+
degraded: false,
|
|
110
|
+
recoveredTimeouts,
|
|
111
|
+
recoveredCommands,
|
|
76
112
|
sentinel,
|
|
77
113
|
slug,
|
|
78
|
-
commands: [healthCheck, put, get, query, search, cleanup]
|
|
79
|
-
|
|
114
|
+
commands: summarizeCommands([init, healthCheck, put, get, query, search, cleanup]),
|
|
115
|
+
notes: recoveredCommands.map(name => `${name} returned complete output after Bun shutdown recovery`),
|
|
116
|
+
warnings: [],
|
|
117
|
+
nextCommands: replayPassed
|
|
118
|
+
? []
|
|
119
|
+
: ['gbrain doctor --json', `gbrain get ${slug}`, `gbrain query ${sentinel}`],
|
|
80
120
|
})
|
|
81
121
|
}
|
|
82
122
|
|
|
@@ -107,9 +147,9 @@ function runGraphifyRehearsal() {
|
|
|
107
147
|
})
|
|
108
148
|
}
|
|
109
149
|
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
|
|
150
|
+
const sourceProjectPath = resolve(options.largeProject)
|
|
151
|
+
const projectPath = prepareGraphifyProject(sourceProjectPath)
|
|
152
|
+
const graphOut = join(projectPath, 'graphify-out')
|
|
113
153
|
const extractCommand = options.semanticExtract ? 'extract' : 'update'
|
|
114
154
|
const extractArgs = options.semanticExtract
|
|
115
155
|
? ['extract', projectPath, '--out', graphOut]
|
|
@@ -131,7 +171,7 @@ function runGraphifyRehearsal() {
|
|
|
131
171
|
nextCommands: [
|
|
132
172
|
'graphify install --platform codex',
|
|
133
173
|
'graphify hook status',
|
|
134
|
-
`graphify update ${quoteArg(
|
|
174
|
+
`graphify update ${quoteArg(sourceProjectPath)} --no-cluster`,
|
|
135
175
|
'Use --semantic-extract only when semantic LLM extraction is explicitly allowed.',
|
|
136
176
|
],
|
|
137
177
|
})
|
|
@@ -171,11 +211,12 @@ function runGraphifyRehearsal() {
|
|
|
171
211
|
? `graphify ${extractCommand} built a real project graph and answered a graph query`
|
|
172
212
|
: 'graphify generated an artifact but graph stats or query validation failed',
|
|
173
213
|
required: options.requireGraphify,
|
|
174
|
-
project:
|
|
214
|
+
project: sourceProjectPath,
|
|
215
|
+
analyzedProject: projectPath,
|
|
175
216
|
mode: options.semanticExtract ? 'semantic-extract' : 'ast-update-no-llm',
|
|
176
217
|
graphPath,
|
|
177
218
|
stats,
|
|
178
|
-
commands: [help, extract, query, benchmark, globalAdd]
|
|
219
|
+
commands: summarizeCommands([help, extract, query, benchmark, globalAdd]),
|
|
179
220
|
nextCommands: passed ? [] : [`graphify query ${quoteArg(options.graphifyQuestion)} --graph ${quoteArg(graphPath)}`],
|
|
180
221
|
})
|
|
181
222
|
}
|
|
@@ -250,6 +291,15 @@ function buildReport(capabilities) {
|
|
|
250
291
|
}
|
|
251
292
|
}
|
|
252
293
|
|
|
294
|
+
function gbrainCommandEnv() {
|
|
295
|
+
return {
|
|
296
|
+
...process.env,
|
|
297
|
+
GBRAIN_HOME: join(workRoot, 'gbrain-home'),
|
|
298
|
+
GBRAIN_AUDIT_DIR: join(workRoot, 'gbrain-audit'),
|
|
299
|
+
GBRAIN_NO_BANNER: '1',
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
253
303
|
function writeReport(report) {
|
|
254
304
|
const target = options.reportFile
|
|
255
305
|
? resolve(options.reportFile)
|
|
@@ -259,6 +309,10 @@ function writeReport(report) {
|
|
|
259
309
|
writeFileSync(target, `${JSON.stringify(report, null, 2)}\n`, 'utf8')
|
|
260
310
|
}
|
|
261
311
|
|
|
312
|
+
function summarizeCommands(commands) {
|
|
313
|
+
return commands.filter(Boolean).map(command => summarizeCommandRecord(command))
|
|
314
|
+
}
|
|
315
|
+
|
|
262
316
|
function runCommand(name, command, args, opts = {}) {
|
|
263
317
|
const invocation = opts.wrapRtk === false
|
|
264
318
|
? { command, args, wrapped: false }
|
|
@@ -274,20 +328,21 @@ function runCommand(name, command, args, opts = {}) {
|
|
|
274
328
|
timeout: opts.timeoutMs ?? options.timeoutMs,
|
|
275
329
|
maxBuffer: 80 * 1024 * 1024,
|
|
276
330
|
})
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const timedOut
|
|
331
|
+
const normalized = command === 'gbrain'
|
|
332
|
+
? normalizeGbrainSpawnResult(args, result)
|
|
333
|
+
: normalizeSpawnResult(result)
|
|
334
|
+
const { stdout, stderr, exitCode, timedOut, recoveredTimeout } = normalized
|
|
281
335
|
const entry = {
|
|
282
336
|
name,
|
|
283
337
|
command: commandLine,
|
|
284
338
|
wrappedByRtk: invocation.wrapped,
|
|
285
339
|
exitCode,
|
|
286
340
|
timedOut,
|
|
341
|
+
recoveredTimeout,
|
|
287
342
|
startedAt,
|
|
288
343
|
endedAt: new Date().toISOString(),
|
|
289
|
-
stdoutTail:
|
|
290
|
-
stderrTail:
|
|
344
|
+
stdoutTail: summarizeCommandOutput(name, 'stdout', stdout),
|
|
345
|
+
stderrTail: summarizeCommandOutput(name, 'stderr', stderr),
|
|
291
346
|
}
|
|
292
347
|
results.push(entry)
|
|
293
348
|
return { ...entry, stdout, stderr }
|
|
@@ -317,17 +372,65 @@ function removeGeneratedGraphJson(graphOut) {
|
|
|
317
372
|
rmSync(join(outputDir, 'graph.json'), { force: true })
|
|
318
373
|
}
|
|
319
374
|
|
|
375
|
+
function prepareGraphifyProject(sourceProjectPath) {
|
|
376
|
+
const snapshotPath = join(workRoot, 'graphify-project')
|
|
377
|
+
rmSync(snapshotPath, { recursive: true, force: true })
|
|
378
|
+
cpSync(sourceProjectPath, snapshotPath, {
|
|
379
|
+
recursive: true,
|
|
380
|
+
force: true,
|
|
381
|
+
filter: sourcePath => shouldIncludeGraphifySnapshotPath(sourceProjectPath, sourcePath),
|
|
382
|
+
})
|
|
383
|
+
return snapshotPath
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function shouldIncludeGraphifySnapshotPath(sourceProjectPath, sourcePath) {
|
|
387
|
+
const relativePath = sourcePath.slice(sourceProjectPath.length).replace(/^[\\/]+/, '')
|
|
388
|
+
if (!relativePath) return true
|
|
389
|
+
const segments = relativePath.split(/[\\/]+/)
|
|
390
|
+
if (segments[0].startsWith('.') && !GRAPHIFY_ALLOWED_HIDDEN_TOP_LEVEL_DIRS.has(segments[0])) return false
|
|
391
|
+
if (segments.some(segment => ['.git', '.codegraph', 'node_modules', '.tmp', 'graphify-out', 'dist', 'coverage'].includes(segment))) {
|
|
392
|
+
return false
|
|
393
|
+
}
|
|
394
|
+
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
395
|
+
if (segments[index] === '.scale' && ['reports', 'ai-os', 'runtime', 'model-usage', 'cache', 'events'].includes(segments[index + 1])) {
|
|
396
|
+
return false
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return true
|
|
400
|
+
}
|
|
401
|
+
|
|
320
402
|
function wrapWithRtk(command, args) {
|
|
321
403
|
if (RTK_BYPASS_COMMANDS.has(command)) return { command, args, wrapped: false }
|
|
322
404
|
if (!options.useRtk || command === 'rtk' || !commandExists('rtk')) return { command, args, wrapped: false }
|
|
323
405
|
return { command: 'rtk', args: [command, ...args], wrapped: true }
|
|
324
406
|
}
|
|
325
407
|
|
|
408
|
+
function normalizeSpawnResult(result) {
|
|
409
|
+
const stdout = String(result.stdout ?? '')
|
|
410
|
+
const stderr = `${String(result.stderr ?? '')}${result.error ? `\n${result.error.message}` : ''}`.trim()
|
|
411
|
+
return {
|
|
412
|
+
stdout,
|
|
413
|
+
stderr,
|
|
414
|
+
exitCode: typeof result.status === 'number' ? result.status : 1,
|
|
415
|
+
timedOut: /ETIMEDOUT/i.test(String(result.error?.message ?? '')),
|
|
416
|
+
recoveredTimeout: false,
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
326
420
|
function spawnStructured(command, args, options) {
|
|
327
|
-
const direct =
|
|
421
|
+
const direct = resolveDirectWindowsGbrainInvocation(command, args, resolveCommandPath)
|
|
328
422
|
if (direct) {
|
|
329
|
-
|
|
423
|
+
const result = spawnSync(direct.command, direct.args, {
|
|
330
424
|
...options,
|
|
425
|
+
cwd: options.cwd ?? direct.cwd,
|
|
426
|
+
shell: false,
|
|
427
|
+
windowsHide: true,
|
|
428
|
+
})
|
|
429
|
+
if (!shouldRetryWithMirroredGbrain(direct, result)) return result
|
|
430
|
+
const mirrored = ensureMirroredGbrainInvocation(direct)
|
|
431
|
+
return spawnSync(mirrored.command, mirrored.args, {
|
|
432
|
+
...options,
|
|
433
|
+
cwd: options.cwd ?? mirrored.cwd,
|
|
331
434
|
shell: false,
|
|
332
435
|
windowsHide: true,
|
|
333
436
|
})
|
|
@@ -348,24 +451,6 @@ function spawnStructured(command, args, options) {
|
|
|
348
451
|
})
|
|
349
452
|
}
|
|
350
453
|
|
|
351
|
-
function resolveDirectWindowsInvocation(command, args) {
|
|
352
|
-
if (process.platform !== 'win32' || command !== 'gbrain') return null
|
|
353
|
-
const gbrainShim = resolveCommandPath('gbrain')
|
|
354
|
-
if (!gbrainShim || !/\.cmd$/i.test(gbrainShim) || !existsSync(gbrainShim)) return null
|
|
355
|
-
try {
|
|
356
|
-
const content = readFileSync(gbrainShim, 'utf8')
|
|
357
|
-
const cliPath = content.match(/"([^"]*gbrain[^"]*src[\\/]cli\.ts)"/i)?.[1]
|
|
358
|
-
const bunShim = resolveCommandPath('bun')
|
|
359
|
-
const bunExe = bunShim ? join(dirname(bunShim), 'node_modules', 'bun', 'bin', 'bun.exe') : ''
|
|
360
|
-
if (cliPath && bunExe && existsSync(bunExe)) {
|
|
361
|
-
return { command: bunExe, args: [cliPath, ...args] }
|
|
362
|
-
}
|
|
363
|
-
} catch {
|
|
364
|
-
return null
|
|
365
|
-
}
|
|
366
|
-
return null
|
|
367
|
-
}
|
|
368
|
-
|
|
369
454
|
function resolveWindowsCommandShim(command) {
|
|
370
455
|
if (process.platform !== 'win32') return command
|
|
371
456
|
if (!/[\\/]/.test(command) || extname(command)) return command
|
|
@@ -510,7 +595,3 @@ function quoteArg(value) {
|
|
|
510
595
|
function powershellQuote(value) {
|
|
511
596
|
return `'${String(value).replace(/'/g, "''")}'`
|
|
512
597
|
}
|
|
513
|
-
|
|
514
|
-
function tail(value, max = 4000) {
|
|
515
|
-
return value.length > max ? value.slice(-max) : value
|
|
516
|
-
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from 'node:child_process'
|
|
3
|
-
import { existsSync, mkdirSync, rmSync } from 'node:fs'
|
|
3
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
4
|
import { tmpdir } from 'node:os'
|
|
5
5
|
import { dirname, extname, join, resolve } from 'node:path'
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import {
|
|
8
|
+
ensureMirroredGbrainInvocation,
|
|
9
|
+
normalizeGbrainSpawnResult,
|
|
10
|
+
resolveDirectWindowsGbrainInvocation,
|
|
11
|
+
shouldRetryWithMirroredGbrain,
|
|
12
|
+
} from './lib/gbrain-runtime.mjs'
|
|
13
|
+
import { summarizeCommandOutput } from './lib/report-output.mjs'
|
|
7
14
|
|
|
8
15
|
const scriptDir = dirname(fileURLToPath(import.meta.url))
|
|
9
16
|
const repoRoot = resolve(scriptDir, '..', '..')
|
|
@@ -13,12 +20,19 @@ const smokeRoot = options.tempDir
|
|
|
13
20
|
: mkSmokeRoot()
|
|
14
21
|
const projectDir = join(smokeRoot, 'project')
|
|
15
22
|
const scaleDir = join(smokeRoot, '.scale')
|
|
23
|
+
const homeDir = join(smokeRoot, 'home')
|
|
24
|
+
const appDataDir = join(homeDir, 'AppData', 'Roaming')
|
|
25
|
+
const localAppDataDir = join(homeDir, 'AppData', 'Local')
|
|
26
|
+
const gbrainHomeDir = join(smokeRoot, 'gbrain-home')
|
|
27
|
+
const gbrainAuditDir = join(smokeRoot, 'gbrain-audit')
|
|
16
28
|
const scaleInvocation = parseCommandLine(options.scaleCommand ?? 'node --import tsx src/api/cli.ts')
|
|
17
29
|
const scaleCommand = formatCommand(scaleInvocation.file, scaleInvocation.args)
|
|
18
30
|
const results = []
|
|
19
31
|
|
|
20
32
|
mkdirSync(projectDir, { recursive: true })
|
|
21
33
|
mkdirSync(scaleDir, { recursive: true })
|
|
34
|
+
mkdirSync(appDataDir, { recursive: true })
|
|
35
|
+
mkdirSync(localAppDataDir, { recursive: true })
|
|
22
36
|
|
|
23
37
|
try {
|
|
24
38
|
runSetupSmoke()
|
|
@@ -33,10 +47,19 @@ try {
|
|
|
33
47
|
function runSetupSmoke() {
|
|
34
48
|
const baseEnv = {
|
|
35
49
|
...process.env,
|
|
50
|
+
HOME: homeDir,
|
|
51
|
+
USERPROFILE: homeDir,
|
|
52
|
+
APPDATA: appDataDir,
|
|
53
|
+
LOCALAPPDATA: localAppDataDir,
|
|
36
54
|
SCALE_DIR: scaleDir,
|
|
37
55
|
SCALE_PROJECT_DIR: projectDir,
|
|
38
56
|
SCALE_LOG_LEVEL: '',
|
|
57
|
+
GBRAIN_HOME: gbrainHomeDir,
|
|
58
|
+
GBRAIN_AUDIT_DIR: gbrainAuditDir,
|
|
59
|
+
GBRAIN_NO_BANNER: '1',
|
|
39
60
|
}
|
|
61
|
+
initProject(baseEnv)
|
|
62
|
+
initIsolatedGbrain(baseEnv)
|
|
40
63
|
|
|
41
64
|
const zh = runCommand('bootstrap-ui-zh', ['bootstrap', 'deps', '--dir', projectDir, '--pack', 'ui', '--lang', 'zh'], baseEnv)
|
|
42
65
|
assertIncludes(zh.stdout, 'SCALE 依赖安装计划', 'Chinese bootstrap output should use Chinese title')
|
|
@@ -64,6 +87,7 @@ function runSetupSmoke() {
|
|
|
64
87
|
const envDoctor = runJson('doctor-env-json', ['doctor', 'env', '--json'], baseEnv)
|
|
65
88
|
assert(envDoctor.ok === true, 'environment doctor should pass when required core commands are available')
|
|
66
89
|
assertArrayContains(envDoctor.checks?.map(check => check.id), ['git', 'npm', 'npx', 'rtk', 'gbrain', 'graphify', 'codegraph'], 'environment doctor should report core and third-party commands')
|
|
90
|
+
assert(envDoctor.checks?.find(check => check.id === 'gbrain')?.status === 'ok', 'environment doctor should validate gbrain when an isolated brain is initialized')
|
|
67
91
|
|
|
68
92
|
const localMemory = runJson('setup-memory-scale-local-json', [
|
|
69
93
|
'setup',
|
|
@@ -97,11 +121,107 @@ function runSetupSmoke() {
|
|
|
97
121
|
assert(gbrainMemory.memoryProviderSwitch?.mode === 'external-first', 'gbrain provider should support external-first mode')
|
|
98
122
|
assert(gbrainMemory.memoryProviderSwitch?.nextOrder?.[0] === 'gbrain', 'gbrain should become the first provider')
|
|
99
123
|
|
|
124
|
+
const apply = runJson('setup-governed-apply-json', [
|
|
125
|
+
'setup',
|
|
126
|
+
'--dir',
|
|
127
|
+
projectDir,
|
|
128
|
+
'--pack',
|
|
129
|
+
'external-cli,memory,knowledge',
|
|
130
|
+
'--apply',
|
|
131
|
+
'--memory-provider',
|
|
132
|
+
'gbrain',
|
|
133
|
+
'--memory-mode',
|
|
134
|
+
'external-first',
|
|
135
|
+
'--json',
|
|
136
|
+
], baseEnv)
|
|
137
|
+
assert(apply.final?.ok === true, 'setup apply should succeed for external-cli, memory, and knowledge packs')
|
|
138
|
+
assert(apply.final?.complete === true, 'setup apply should leave the selected packs fully installed')
|
|
139
|
+
assert(apply.final?.summary?.needsInit === 0, 'setup apply should not leave RTK, GBrain, Graphify, or CodeGraph uninitialized')
|
|
140
|
+
assert(apply.memoryProviderSwitch?.provider === 'gbrain', 'setup apply should keep gbrain as the selected provider')
|
|
141
|
+
assert(existsSync(join(projectDir, '.codegraph')), 'setup apply should initialize a CodeGraph index in the target project')
|
|
142
|
+
assert(existsSync(join(projectDir, 'graphify-out', 'graph.json')), 'setup apply should generate a Graphify graph artifact without LLM usage')
|
|
143
|
+
assert(existsSync(join(projectDir, '.git', 'hooks', 'post-commit')), 'setup apply should install the Graphify post-commit hook')
|
|
144
|
+
assert(existsSync(join(projectDir, '.git', 'hooks', 'post-checkout')), 'setup apply should install the Graphify post-checkout hook')
|
|
145
|
+
assert(
|
|
146
|
+
['installed', 'installed-now'].includes(apply.final?.items?.find(item => item.id === 'rtk')?.status),
|
|
147
|
+
'setup apply should leave RTK in an installed state after Codex initialization',
|
|
148
|
+
)
|
|
149
|
+
assert(
|
|
150
|
+
['installed', 'installed-now'].includes(apply.final?.items?.find(item => item.id === 'graphify')?.status),
|
|
151
|
+
'setup apply should leave Graphify in an installed state after hook and graph initialization',
|
|
152
|
+
)
|
|
153
|
+
assert((apply.final?.postCheckSummary?.warned ?? 0) === 0, 'setup apply should not leave memory/code post-checks in a warned state')
|
|
154
|
+
|
|
155
|
+
const verify = runJson('setup-governed-verify-json', [
|
|
156
|
+
'setup',
|
|
157
|
+
'--verify',
|
|
158
|
+
'--dir',
|
|
159
|
+
projectDir,
|
|
160
|
+
'--pack',
|
|
161
|
+
'external-cli,memory,knowledge',
|
|
162
|
+
'--json',
|
|
163
|
+
], baseEnv)
|
|
164
|
+
assert(verify.ok === true, 'setup verify should pass after governed apply in the isolated home')
|
|
165
|
+
assert((verify.summary?.blockingIssues?.length ?? 0) === 0, 'setup verify should not report any blocking dependency issues after apply')
|
|
166
|
+
|
|
100
167
|
const codegraph = runJson('codegraph-status-json', ['codegraph', 'status', '--dir', repoRoot, '--json'], baseEnv)
|
|
101
168
|
assertArrayContains(codegraph.providers?.map(provider => provider.id), ['codegraph', 'graphify'], 'codegraph status should expose CodeGraph and Graphify providers')
|
|
102
169
|
assert(typeof codegraph.projectIndexExists === 'boolean', 'codegraph status should report project index state')
|
|
103
170
|
}
|
|
104
171
|
|
|
172
|
+
function initProject(env) {
|
|
173
|
+
writeFileSync(join(projectDir, 'AGENTS.md'), '# Setup smoke project\n', 'utf-8')
|
|
174
|
+
writeFileSync(join(projectDir, 'smoke.ts'), 'export const setupSmoke = "ready"\n', 'utf-8')
|
|
175
|
+
const result = spawnStructured('git', ['init'], {
|
|
176
|
+
cwd: projectDir,
|
|
177
|
+
env,
|
|
178
|
+
encoding: 'utf8',
|
|
179
|
+
timeout: options.timeoutMs,
|
|
180
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
181
|
+
})
|
|
182
|
+
const stdout = String(result.stdout ?? '')
|
|
183
|
+
const stderr = String(result.stderr ?? '')
|
|
184
|
+
results.push({
|
|
185
|
+
name: 'git-init-project',
|
|
186
|
+
command: 'git init',
|
|
187
|
+
exitCode: typeof result.status === 'number' ? result.status : 1,
|
|
188
|
+
startedAt: new Date().toISOString(),
|
|
189
|
+
endedAt: new Date().toISOString(),
|
|
190
|
+
stdoutTail: summarizeCommandOutput('git-init-project', 'stdout', stdout),
|
|
191
|
+
stderrTail: summarizeCommandOutput('git-init-project', 'stderr', stderr + (result.error ? `\n${result.error.message}` : '')),
|
|
192
|
+
})
|
|
193
|
+
if (result.status !== 0) {
|
|
194
|
+
throw new Error(`git-init-project failed with exit code ${result.status ?? 1}\n${stderr || stdout}`)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function initIsolatedGbrain(env) {
|
|
199
|
+
const startedAt = new Date().toISOString()
|
|
200
|
+
const result = spawnStructured('gbrain', ['init', '--pglite', '--no-embedding'], {
|
|
201
|
+
cwd: repoRoot,
|
|
202
|
+
env,
|
|
203
|
+
encoding: 'utf8',
|
|
204
|
+
timeout: options.timeoutMs,
|
|
205
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
206
|
+
})
|
|
207
|
+
const normalized = normalizeGbrainSpawnResult(['init', '--pglite', '--no-embedding'], result)
|
|
208
|
+
const { stdout, stderr, exitCode, timedOut, recoveredTimeout } = normalized
|
|
209
|
+
results.push({
|
|
210
|
+
name: 'gbrain-init-isolated-home',
|
|
211
|
+
command: 'gbrain init --pglite --no-embedding',
|
|
212
|
+
exitCode,
|
|
213
|
+
timedOut,
|
|
214
|
+
recoveredTimeout,
|
|
215
|
+
startedAt,
|
|
216
|
+
endedAt: new Date().toISOString(),
|
|
217
|
+
stdoutTail: summarizeCommandOutput('gbrain-init-isolated-home', 'stdout', stdout),
|
|
218
|
+
stderrTail: summarizeCommandOutput('gbrain-init-isolated-home', 'stderr', stderr),
|
|
219
|
+
})
|
|
220
|
+
if (exitCode !== 0) {
|
|
221
|
+
throw new Error(`gbrain-init-isolated-home failed with exit code ${exitCode}\n${summarizeCommandOutput('gbrain-init-isolated-home', 'stderr', stderr) || summarizeCommandOutput('gbrain-init-isolated-home', 'stdout', stdout)}`)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
105
225
|
function runJson(name, args, env) {
|
|
106
226
|
const result = runCommand(name, args, env)
|
|
107
227
|
try {
|
|
@@ -132,8 +252,8 @@ function runCommand(name, args, env) {
|
|
|
132
252
|
exitCode,
|
|
133
253
|
startedAt,
|
|
134
254
|
endedAt: new Date().toISOString(),
|
|
135
|
-
stdoutTail:
|
|
136
|
-
stderrTail:
|
|
255
|
+
stdoutTail: summarizeCommandOutput(name, 'stdout', stdout),
|
|
256
|
+
stderrTail: summarizeCommandOutput(name, 'stderr', stderr + (result.error ? `\n${result.error.message}` : '')),
|
|
137
257
|
}
|
|
138
258
|
results.push(entry)
|
|
139
259
|
if (exitCode !== 0) {
|
|
@@ -143,6 +263,23 @@ function runCommand(name, args, env) {
|
|
|
143
263
|
}
|
|
144
264
|
|
|
145
265
|
function spawnStructured(command, args, options) {
|
|
266
|
+
const direct = resolveDirectWindowsGbrainInvocation(command, args, resolveCommandPath)
|
|
267
|
+
if (direct) {
|
|
268
|
+
const result = spawnSync(direct.command, direct.args, {
|
|
269
|
+
...options,
|
|
270
|
+
cwd: options.cwd ?? direct.cwd,
|
|
271
|
+
shell: false,
|
|
272
|
+
windowsHide: true,
|
|
273
|
+
})
|
|
274
|
+
if (!shouldRetryWithMirroredGbrain(direct, result)) return result
|
|
275
|
+
const mirrored = ensureMirroredGbrainInvocation(direct)
|
|
276
|
+
return spawnSync(mirrored.command, mirrored.args, {
|
|
277
|
+
...options,
|
|
278
|
+
cwd: options.cwd ?? mirrored.cwd,
|
|
279
|
+
shell: false,
|
|
280
|
+
windowsHide: true,
|
|
281
|
+
})
|
|
282
|
+
}
|
|
146
283
|
const resolved = resolveWindowsCommandShim(resolveCommandPath(command) ?? command)
|
|
147
284
|
if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(resolved)) {
|
|
148
285
|
const comspec = process.env.ComSpec || 'cmd.exe'
|
|
@@ -200,7 +337,7 @@ function parseArgs(args) {
|
|
|
200
337
|
else if (arg === '--temp-dir') parsed.tempDir = args[++index]
|
|
201
338
|
else if (arg === '--timeout-ms') parsed.timeoutMs = Number.parseInt(args[++index] ?? '', 10)
|
|
202
339
|
else if (arg === '--help' || arg === '-h') {
|
|
203
|
-
process.stdout.write(
|
|
340
|
+
process.stdout.write('Usage: node scripts/workflow/setup-smoke.mjs [--scale-command "scale"] [--keep-temp] [--verbose]\n')
|
|
204
341
|
process.exit(0)
|
|
205
342
|
} else {
|
|
206
343
|
throw new Error(`Unknown argument: ${arg}`)
|
|
@@ -288,12 +425,9 @@ function writeSummary(status, error) {
|
|
|
288
425
|
repoRoot,
|
|
289
426
|
projectDir,
|
|
290
427
|
scaleDir,
|
|
428
|
+
homeDir,
|
|
291
429
|
results,
|
|
292
430
|
error: error ? String(error.stack ?? error.message ?? error) : undefined,
|
|
293
431
|
}
|
|
294
432
|
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
|
|
295
433
|
}
|
|
296
|
-
|
|
297
|
-
function tail(value, max = 4000) {
|
|
298
|
-
return value.length > max ? value.slice(-max) : value
|
|
299
|
-
}
|