@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.
Files changed (35) hide show
  1. package/dist/api/cli.js +267 -7
  2. package/dist/api/cli.js.map +1 -1
  3. package/dist/api/doctor.js +1 -1
  4. package/dist/api/doctor.js.map +1 -1
  5. package/dist/bootstrap/DependencyBootstrap.d.ts +1 -0
  6. package/dist/bootstrap/DependencyBootstrap.js +137 -25
  7. package/dist/bootstrap/DependencyBootstrap.js.map +1 -1
  8. package/dist/capabilities/InstalledSkillsIntegration.js +29 -9
  9. package/dist/capabilities/InstalledSkillsIntegration.js.map +1 -1
  10. package/dist/context/ContextBudget.js +2 -2
  11. package/dist/core/GbrainRuntime.d.ts +25 -0
  12. package/dist/core/GbrainRuntime.js +270 -0
  13. package/dist/core/GbrainRuntime.js.map +1 -0
  14. package/dist/env/EnvironmentDoctor.js +221 -5
  15. package/dist/env/EnvironmentDoctor.js.map +1 -1
  16. package/dist/memory/MemoryProviders.js +38 -91
  17. package/dist/memory/MemoryProviders.js.map +1 -1
  18. package/dist/runtime/ModelUsageLedger.d.ts +53 -2
  19. package/dist/runtime/ModelUsageLedger.js +243 -39
  20. package/dist/runtime/ModelUsageLedger.js.map +1 -1
  21. package/dist/setup/SetupVerification.d.ts +42 -0
  22. package/dist/setup/SetupVerification.js +180 -0
  23. package/dist/setup/SetupVerification.js.map +1 -0
  24. package/dist/tools/ToolCapabilityRegistry.js +10 -0
  25. package/dist/tools/ToolCapabilityRegistry.js.map +1 -1
  26. package/dist/workflow/VerificationProfile.js +1 -1
  27. package/dist/workflow/VerificationProfile.js.map +1 -1
  28. package/docs/CONTEXT_BUDGET.md +12 -2
  29. package/docs/GOVERNANCE_DASHBOARD.md +7 -0
  30. package/docs/start/quickstart.md +1 -0
  31. package/package.json +3 -2
  32. package/scripts/workflow/lib/gbrain-runtime.mjs +185 -0
  33. package/scripts/workflow/lib/report-output.mjs +107 -0
  34. package/scripts/workflow/provider-rehearsal.mjs +129 -48
  35. 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 healthCheck = runCommand('gbrain-health', 'gbrain', ['list', '-n', '1'], { timeoutMs: 60_000 })
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 = (get.exitCode === 0 || get.timedOut) && get.stdout.includes(sentinel)
64
- const recallPassed = (query.exitCode === 0 || query.timedOut || search?.exitCode === 0 || search?.timedOut)
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].filter(Boolean),
79
- nextCommands: replayPassed ? [] : ['gbrain doctor --json', `gbrain get ${slug}`, `gbrain query ${sentinel}`],
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 projectPath = resolve(options.largeProject)
111
- const graphOut = options.semanticExtract ? resolve(workRoot, 'graphify-out') : join(projectPath, 'graphify-out')
112
- if (options.semanticExtract) mkdirSync(graphOut, { recursive: true })
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(projectPath)} --no-cluster`,
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: projectPath,
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].filter(Boolean),
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 stdout = String(result.stdout ?? '')
278
- const stderr = String(result.stderr ?? '') + (result.error ? `\n${result.error.message}` : '')
279
- const exitCode = typeof result.status === 'number' ? result.status : 1
280
- const timedOut = /ETIMEDOUT/i.test(String(result.error?.message ?? ''))
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: tail(stdout),
290
- stderrTail: tail(stderr),
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 = resolveDirectWindowsInvocation(command, args)
421
+ const direct = resolveDirectWindowsGbrainInvocation(command, args, resolveCommandPath)
328
422
  if (direct) {
329
- return spawnSync(direct.command, direct.args, {
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: tail(stdout),
136
- stderrTail: tail(stderr + (result.error ? `\n${result.error.message}` : '')),
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(`Usage: node scripts/workflow/setup-smoke.mjs [--scale-command "scale"] [--keep-temp] [--verbose]\n`)
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
- }