@hongmaple0820/scale-engine 0.38.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +15 -0
  2. package/dist/api/cli.js +142 -40
  3. package/dist/api/cli.js.map +1 -1
  4. package/dist/api/doctor.js +1 -1
  5. package/dist/api/doctor.js.map +1 -1
  6. package/dist/bootstrap/DependencyBootstrap.d.ts +22 -1
  7. package/dist/bootstrap/DependencyBootstrap.js +427 -33
  8. package/dist/bootstrap/DependencyBootstrap.js.map +1 -1
  9. package/dist/bootstrap/DependencyBootstrapRenderer.d.ts +3 -0
  10. package/dist/bootstrap/DependencyBootstrapRenderer.js +140 -0
  11. package/dist/bootstrap/DependencyBootstrapRenderer.js.map +1 -0
  12. package/dist/cli/gateStatusCommands.d.ts +1 -0
  13. package/dist/cli/gateStatusCommands.js +52 -0
  14. package/dist/cli/gateStatusCommands.js.map +1 -0
  15. package/dist/cli/phaseCommands.js +15 -3
  16. package/dist/cli/phaseCommands.js.map +1 -1
  17. package/dist/cli/promptCommands.d.ts +1 -0
  18. package/dist/cli/promptCommands.js +57 -0
  19. package/dist/cli/promptCommands.js.map +1 -0
  20. package/dist/cli/scoreCommands.d.ts +1 -0
  21. package/dist/cli/scoreCommands.js +112 -0
  22. package/dist/cli/scoreCommands.js.map +1 -0
  23. package/dist/codegraph/CodeIntelligence.js +1 -1
  24. package/dist/codegraph/CodeIntelligence.js.map +1 -1
  25. package/dist/context/SessionStartSequence.js +13 -4
  26. package/dist/context/SessionStartSequence.js.map +1 -1
  27. package/dist/core/ExternalCommand.js +18 -4
  28. package/dist/core/ExternalCommand.js.map +1 -1
  29. package/dist/env/EnvironmentDoctor.d.ts +66 -0
  30. package/dist/env/EnvironmentDoctor.js +365 -0
  31. package/dist/env/EnvironmentDoctor.js.map +1 -0
  32. package/dist/i18n/Language.d.ts +9 -0
  33. package/dist/i18n/Language.js +38 -0
  34. package/dist/i18n/Language.js.map +1 -0
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +1 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/memory/MemoryProviders.d.ts +8 -0
  39. package/dist/memory/MemoryProviders.js +217 -16
  40. package/dist/memory/MemoryProviders.js.map +1 -1
  41. package/dist/prompts/PromptOptimizer.d.ts +42 -0
  42. package/dist/prompts/PromptOptimizer.js +309 -0
  43. package/dist/prompts/PromptOptimizer.js.map +1 -0
  44. package/dist/setup/SetupWizard.d.ts +42 -0
  45. package/dist/setup/SetupWizard.js +156 -0
  46. package/dist/setup/SetupWizard.js.map +1 -0
  47. package/dist/skills/SkillRepository.js +2 -2
  48. package/dist/skills/SkillRepository.js.map +1 -1
  49. package/dist/testing/DiffTestSelector.js +1 -1
  50. package/dist/testing/DiffTestSelector.js.map +1 -1
  51. package/dist/tools/ToolCapabilityRegistry.d.ts +4 -0
  52. package/dist/tools/ToolCapabilityRegistry.js +11 -6
  53. package/dist/tools/ToolCapabilityRegistry.js.map +1 -1
  54. package/dist/workflow/CommitDiscipline.js +8 -7
  55. package/dist/workflow/CommitDiscipline.js.map +1 -1
  56. package/dist/workflow/CrossRepoOrchestrator.js +15 -7
  57. package/dist/workflow/CrossRepoOrchestrator.js.map +1 -1
  58. package/dist/workflow/GateCatalog.d.ts +61 -0
  59. package/dist/workflow/GateCatalog.js +212 -0
  60. package/dist/workflow/GateCatalog.js.map +1 -0
  61. package/dist/workflow/GovernanceTemplatePacks.js +19 -4
  62. package/dist/workflow/GovernanceTemplatePacks.js.map +1 -1
  63. package/dist/workflow/SessionPreamble.js +7 -2
  64. package/dist/workflow/SessionPreamble.js.map +1 -1
  65. package/dist/workflow/TaskScoreEngine.d.ts +42 -0
  66. package/dist/workflow/TaskScoreEngine.js +181 -0
  67. package/dist/workflow/TaskScoreEngine.js.map +1 -0
  68. package/dist/workflow/WorkspaceTopology.d.ts +3 -0
  69. package/dist/workflow/WorkspaceTopology.js +40 -3
  70. package/dist/workflow/WorkspaceTopology.js.map +1 -1
  71. package/dist/workflow/gates/GateSystem.js +2 -2
  72. package/dist/workflow/gates/GateSystem.js.map +1 -1
  73. package/dist/workflow/index.d.ts +2 -0
  74. package/dist/workflow/index.js +2 -0
  75. package/dist/workflow/index.js.map +1 -1
  76. package/docs/CODE_INTELLIGENCE.md +27 -2
  77. package/docs/MEMORY_FABRIC.md +22 -1
  78. package/docs/THIRD_PARTY_SKILLS.md +50 -1
  79. package/docs/guides/GETTING_STARTED.md +24 -0
  80. package/docs/start/quickstart.md +103 -76
  81. package/docs/workflow/GATES_AND_SCORE.md +56 -0
  82. package/docs/workflow/PROMPT_OPTIMIZATION.md +44 -0
  83. package/docs/workflow/README.md +7 -0
  84. package/docs/workflow/node-library.md +3 -3
  85. package/package.json +12 -5
  86. package/scripts/workflow/provider-rehearsal.mjs +516 -0
  87. package/scripts/workflow/setup-smoke.mjs +299 -0
@@ -0,0 +1,516 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process'
3
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
4
+ import { tmpdir } from 'node:os'
5
+ import { dirname, extname, join, resolve } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ const scriptDir = dirname(fileURLToPath(import.meta.url))
9
+ const repoRoot = resolve(scriptDir, '..', '..')
10
+ const options = parseArgs(process.argv.slice(2))
11
+ const runId = `provider-rehearsal-${Date.now()}-${Math.random().toString(16).slice(2)}`
12
+ const workRoot = options.outDir ? resolve(options.outDir) : join(tmpdir(), runId)
13
+ const results = []
14
+ const RTK_BYPASS_COMMANDS = new Set(['gbrain'])
15
+
16
+ mkdirSync(workRoot, { recursive: true })
17
+
18
+ try {
19
+ const gbrain = options.skipGbrain ? skipped('gbrain') : runGbrainReplay()
20
+ const graphify = options.skipGraphify ? skipped('graphify') : runGraphifyRehearsal()
21
+ const report = buildReport([gbrain, graphify])
22
+ if (options.writeReport || options.reportFile) writeReport(report)
23
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
24
+ if (!report.ok) process.exitCode = 1
25
+ } finally {
26
+ if (!options.keepOutput && !options.outDir) rmSync(workRoot, { recursive: true, force: true })
27
+ }
28
+
29
+ function runGbrainReplay() {
30
+ const healthCheck = runCommand('gbrain-health', 'gbrain', ['list', '-n', '1'], { timeoutMs: 60_000 })
31
+ const health = evaluateGbrainList(healthCheck)
32
+ if (!health.available) {
33
+ return capability('gbrain', options.requireGbrain ? 'failed' : 'blocked', {
34
+ reason: health.reason || failureLine(`${healthCheck.stdout}\n${healthCheck.stderr}`) || 'gbrain health check failed',
35
+ required: options.requireGbrain,
36
+ health,
37
+ commands: [healthCheck],
38
+ nextCommands: [
39
+ 'gbrain init --pglite --no-embedding',
40
+ 'gbrain init --supabase',
41
+ 'gbrain init --url <remote-gbrain-url>',
42
+ 'gbrain list -n 1',
43
+ 'gbrain doctor --json',
44
+ 'npm run smoke:gbrain',
45
+ ],
46
+ })
47
+ }
48
+
49
+ const slug = `scale-rehearsal-${Date.now()}-${Math.random().toString(16).slice(2)}`
50
+ const sentinel = `scale-engine-gbrain-replay-${Date.now()}-${Math.random().toString(16).slice(2)}`
51
+ const body = `# ${slug}. Sentinel: ${sentinel}. This page verifies that SCALE can write memory in one process and read/query it in later processes.`
52
+
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 })
56
+ const queryOutput = `${query.stdout}\n${query.stderr}`
57
+ const search = query.exitCode === 0 && (queryOutput.includes(sentinel) || queryOutput.includes(slug))
58
+ ? 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 })
61
+
62
+ 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)
65
+ && (recallOutput.includes(sentinel) || recallOutput.includes(slug))
66
+ const replayPassed = put.exitCode === 0
67
+ && getPassed
68
+ && recallPassed
69
+
70
+ return capability('gbrain', replayPassed ? 'passed' : 'failed', {
71
+ reason: replayPassed
72
+ ? 'gbrain write/get/query replay passed across separate CLI processes'
73
+ : 'gbrain was configured, but write/get/query replay did not prove recall',
74
+ required: options.requireGbrain,
75
+ health,
76
+ sentinel,
77
+ slug,
78
+ commands: [healthCheck, put, get, query, search, cleanup].filter(Boolean),
79
+ nextCommands: replayPassed ? [] : ['gbrain doctor --json', `gbrain get ${slug}`, `gbrain query ${sentinel}`],
80
+ })
81
+ }
82
+
83
+ function evaluateGbrainList(command) {
84
+ const output = `${command.stdout}\n${command.stderr}`
85
+ if (/no brain configured/i.test(output)) {
86
+ return { available: false, degraded: false, reason: 'gbrain CLI is installed but no brain is configured' }
87
+ }
88
+ return {
89
+ available: command.exitCode === 0,
90
+ degraded: false,
91
+ reason: command.exitCode === 0 ? 'gbrain list passed; brain is reachable' : failureLine(output),
92
+ }
93
+ }
94
+
95
+ function runGraphifyRehearsal() {
96
+ const help = runCommand('graphify-help', 'graphify', ['--help'], { timeoutMs: 30_000 })
97
+ if (help.exitCode !== 0) {
98
+ return capability('graphify', options.requireGraphify ? 'failed' : 'blocked', {
99
+ reason: failureLine(`${help.stdout}\n${help.stderr}`) || 'graphify CLI is not available',
100
+ required: options.requireGraphify,
101
+ commands: [help],
102
+ nextCommands: [
103
+ 'uv tool install graphify',
104
+ 'graphify install --platform codex',
105
+ 'npm run smoke:graphify',
106
+ ],
107
+ })
108
+ }
109
+
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 })
113
+ const extractCommand = options.semanticExtract ? 'extract' : 'update'
114
+ const extractArgs = options.semanticExtract
115
+ ? ['extract', projectPath, '--out', graphOut]
116
+ : ['update', projectPath]
117
+ if (!options.semanticExtract) removeGeneratedGraphJson(graphOut)
118
+ if (options.semanticExtract && options.graphifyBackend) extractArgs.push('--backend', options.graphifyBackend)
119
+ if (options.noCluster) extractArgs.push('--no-cluster')
120
+ if (options.semanticExtract && options.globalExtract) extractArgs.push('--global')
121
+
122
+ const extract = runCommand(`graphify-${extractCommand}`, 'graphify', extractArgs, {
123
+ timeoutMs: options.timeoutMs,
124
+ env: graphifyNoModelEnv(),
125
+ })
126
+ if (extract.exitCode !== 0) {
127
+ return capability('graphify', options.requireGraphify ? 'failed' : 'blocked', {
128
+ reason: failureLine(`${extract.stdout}\n${extract.stderr}`) || `graphify ${extractCommand} failed`,
129
+ required: options.requireGraphify,
130
+ commands: [help, extract],
131
+ nextCommands: [
132
+ 'graphify install --platform codex',
133
+ 'graphify hook status',
134
+ `graphify update ${quoteArg(projectPath)} --no-cluster`,
135
+ 'Use --semantic-extract only when semantic LLM extraction is explicitly allowed.',
136
+ ],
137
+ })
138
+ }
139
+
140
+ const graphPath = findGraphJson(graphOut)
141
+ if (!graphPath) {
142
+ return capability('graphify', options.requireGraphify ? 'failed' : 'blocked', {
143
+ reason: `graphify ${extractCommand} completed but graph.json was not found under ${graphOut}`,
144
+ required: options.requireGraphify,
145
+ commands: [help, extract],
146
+ nextCommands: [`Get-ChildItem -Recurse ${quoteArg(graphOut)}`],
147
+ })
148
+ }
149
+
150
+ const stats = parseGraphStats(graphPath)
151
+ const query = runCommand('graphify-query', 'graphify', [
152
+ 'query',
153
+ options.graphifyQuestion,
154
+ '--graph',
155
+ graphPath,
156
+ ], { timeoutMs: 120_000, env: graphifyNoModelEnv() })
157
+ const benchmark = runCommand('graphify-benchmark', 'graphify', ['benchmark', graphPath], {
158
+ timeoutMs: 120_000,
159
+ env: graphifyNoModelEnv(),
160
+ })
161
+ const globalAdd = options.globalAdd
162
+ ? runCommand('graphify-global-add', 'graphify', ['global', 'add', graphPath, '--as', options.globalTag], {
163
+ timeoutMs: 120_000,
164
+ env: graphifyNoModelEnv(),
165
+ })
166
+ : undefined
167
+
168
+ const passed = stats.ok && query.exitCode === 0
169
+ return capability('graphify', passed ? 'passed' : options.requireGraphify ? 'failed' : 'blocked', {
170
+ reason: passed
171
+ ? `graphify ${extractCommand} built a real project graph and answered a graph query`
172
+ : 'graphify generated an artifact but graph stats or query validation failed',
173
+ required: options.requireGraphify,
174
+ project: projectPath,
175
+ mode: options.semanticExtract ? 'semantic-extract' : 'ast-update-no-llm',
176
+ graphPath,
177
+ stats,
178
+ commands: [help, extract, query, benchmark, globalAdd].filter(Boolean),
179
+ nextCommands: passed ? [] : [`graphify query ${quoteArg(options.graphifyQuestion)} --graph ${quoteArg(graphPath)}`],
180
+ })
181
+ }
182
+
183
+ function parseGraphStats(graphPath) {
184
+ try {
185
+ const graph = JSON.parse(readFileSync(graphPath, 'utf8'))
186
+ const nodes = firstArray(graph.nodes, graph.graph?.nodes, graph.data?.nodes)
187
+ const edges = firstArray(graph.edges, graph.links, graph.graph?.edges, graph.graph?.links, graph.data?.edges, graph.data?.links)
188
+ return {
189
+ ok: nodes.length > 0,
190
+ nodes: nodes.length,
191
+ edges: edges.length,
192
+ }
193
+ } catch (error) {
194
+ return {
195
+ ok: false,
196
+ nodes: 0,
197
+ edges: 0,
198
+ error: String(error.message ?? error),
199
+ }
200
+ }
201
+ }
202
+
203
+ function firstArray(...values) {
204
+ return values.find(Array.isArray) ?? []
205
+ }
206
+
207
+ function findGraphJson(root) {
208
+ const direct = [
209
+ join(root, 'graph.json'),
210
+ join(root, 'graphify-out', 'graph.json'),
211
+ join(root, 'graph', 'graph.json'),
212
+ ].find(candidate => existsSync(candidate))
213
+ if (direct) return direct
214
+
215
+ const scan = runCommand('find-graph-json', process.platform === 'win32' ? 'powershell' : 'find', process.platform === 'win32'
216
+ ? ['-NoProfile', '-Command', `Get-ChildItem -Path ${quoteArg(root)} -Recurse -Filter graph.json | Select-Object -First 1 -ExpandProperty FullName`]
217
+ : [root, '-name', 'graph.json', '-print', '-quit'], { timeoutMs: 30_000, wrapRtk: false })
218
+ const found = scan.stdout.trim().split(/\r?\n/).find(Boolean)
219
+ return found && existsSync(found) ? found : null
220
+ }
221
+
222
+ function skipped(id) {
223
+ return capability(id, 'skipped', { reason: `${id} rehearsal was skipped`, required: false, commands: [], nextCommands: [] })
224
+ }
225
+
226
+ function capability(id, status, details) {
227
+ return {
228
+ id,
229
+ status,
230
+ required: Boolean(details.required),
231
+ reason: details.reason,
232
+ nextCommands: details.nextCommands ?? [],
233
+ ...Object.fromEntries(Object.entries(details).filter(([key]) => !['required', 'reason', 'nextCommands'].includes(key))),
234
+ }
235
+ }
236
+
237
+ function buildReport(capabilities) {
238
+ const failedRequired = capabilities.filter(item => item.required && item.status !== 'passed' && item.status !== 'skipped')
239
+ return {
240
+ version: 1,
241
+ ok: failedRequired.length === 0,
242
+ status: failedRequired.length === 0 ? 'completed' : 'failed',
243
+ runId,
244
+ generatedAt: new Date().toISOString(),
245
+ repoRoot,
246
+ workRoot,
247
+ rtkWrapped: options.useRtk && commandExists('rtk'),
248
+ capabilities,
249
+ results,
250
+ }
251
+ }
252
+
253
+ function writeReport(report) {
254
+ const target = options.reportFile
255
+ ? resolve(options.reportFile)
256
+ : join(repoRoot, '.scale', 'reports', `${runId}.json`)
257
+ mkdirSync(dirname(target), { recursive: true })
258
+ report.reportFile = target
259
+ writeFileSync(target, `${JSON.stringify(report, null, 2)}\n`, 'utf8')
260
+ }
261
+
262
+ function runCommand(name, command, args, opts = {}) {
263
+ const invocation = opts.wrapRtk === false
264
+ ? { command, args, wrapped: false }
265
+ : wrapWithRtk(command, args)
266
+ const startedAt = new Date().toISOString()
267
+ const commandLine = formatCommand(invocation.command, invocation.args)
268
+ if (options.verbose) process.stderr.write(`[RUN] ${commandLine}\n`)
269
+ const result = spawnStructured(invocation.command, invocation.args, {
270
+ cwd: repoRoot,
271
+ env: opts.env ?? process.env,
272
+ input: opts.input,
273
+ encoding: 'utf8',
274
+ timeout: opts.timeoutMs ?? options.timeoutMs,
275
+ maxBuffer: 80 * 1024 * 1024,
276
+ })
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 ?? ''))
281
+ const entry = {
282
+ name,
283
+ command: commandLine,
284
+ wrappedByRtk: invocation.wrapped,
285
+ exitCode,
286
+ timedOut,
287
+ startedAt,
288
+ endedAt: new Date().toISOString(),
289
+ stdoutTail: tail(stdout),
290
+ stderrTail: tail(stderr),
291
+ }
292
+ results.push(entry)
293
+ return { ...entry, stdout, stderr }
294
+ }
295
+
296
+ function graphifyNoModelEnv() {
297
+ const env = { ...process.env }
298
+ for (const key of [
299
+ 'GEMINI_API_KEY',
300
+ 'GOOGLE_API_KEY',
301
+ 'MOONSHOT_API_KEY',
302
+ 'ANTHROPIC_API_KEY',
303
+ 'OPENAI_API_KEY',
304
+ 'DEEPSEEK_API_KEY',
305
+ 'KIMI_API_KEY',
306
+ ]) {
307
+ delete env[key]
308
+ }
309
+ return env
310
+ }
311
+
312
+ function removeGeneratedGraphJson(graphOut) {
313
+ const outputDir = resolve(graphOut)
314
+ if (outputDir.split(/[\\/]/).at(-1) !== 'graphify-out') {
315
+ throw new Error(`Refusing to clean graphify output outside a graphify-out directory: ${outputDir}`)
316
+ }
317
+ rmSync(join(outputDir, 'graph.json'), { force: true })
318
+ }
319
+
320
+ function wrapWithRtk(command, args) {
321
+ if (RTK_BYPASS_COMMANDS.has(command)) return { command, args, wrapped: false }
322
+ if (!options.useRtk || command === 'rtk' || !commandExists('rtk')) return { command, args, wrapped: false }
323
+ return { command: 'rtk', args: [command, ...args], wrapped: true }
324
+ }
325
+
326
+ function spawnStructured(command, args, options) {
327
+ const direct = resolveDirectWindowsInvocation(command, args)
328
+ if (direct) {
329
+ return spawnSync(direct.command, direct.args, {
330
+ ...options,
331
+ shell: false,
332
+ windowsHide: true,
333
+ })
334
+ }
335
+ const resolved = resolveWindowsCommandShim(resolveCommandPath(command) ?? command)
336
+ if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(resolved)) {
337
+ const commandLine = ['&', powershellQuote(resolved), ...args.map(powershellQuote)].join(' ')
338
+ return spawnSync('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', commandLine], {
339
+ ...options,
340
+ shell: false,
341
+ windowsHide: true,
342
+ })
343
+ }
344
+ return spawnSync(resolved, args, {
345
+ ...options,
346
+ shell: false,
347
+ windowsHide: true,
348
+ })
349
+ }
350
+
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
+ function resolveWindowsCommandShim(command) {
370
+ if (process.platform !== 'win32') return command
371
+ if (!/[\\/]/.test(command) || extname(command)) return command
372
+ for (const extension of ['.cmd', '.exe', '.bat', '.com']) {
373
+ const candidate = `${command}${extension}`
374
+ if (existsSync(candidate)) return candidate
375
+ }
376
+ return command
377
+ }
378
+
379
+ function resolveCommandPath(command) {
380
+ if (/[\\/]/.test(command)) return command
381
+ const lookup = process.platform === 'win32' ? 'where.exe' : 'which'
382
+ const result = spawnSync(lookup, [command], {
383
+ encoding: 'utf8',
384
+ shell: false,
385
+ windowsHide: true,
386
+ })
387
+ if (result.status !== 0) return null
388
+ return String(result.stdout ?? '')
389
+ .split(/\r?\n/)
390
+ .map(line => line.trim())
391
+ .find(Boolean) ?? null
392
+ }
393
+
394
+ function commandExists(command) {
395
+ return Boolean(resolveCommandPath(command))
396
+ }
397
+
398
+ function parseArgs(args) {
399
+ const parsed = {
400
+ skipGbrain: false,
401
+ skipGraphify: false,
402
+ requireGbrain: false,
403
+ requireGraphify: false,
404
+ keepOutput: false,
405
+ keepGbrainPage: false,
406
+ verbose: false,
407
+ useRtk: true,
408
+ noCluster: true,
409
+ semanticExtract: false,
410
+ globalExtract: false,
411
+ globalAdd: false,
412
+ globalTag: 'scale-engine-rehearsal',
413
+ largeProject: repoRoot,
414
+ outDir: undefined,
415
+ reportFile: undefined,
416
+ writeReport: false,
417
+ graphifyBackend: undefined,
418
+ graphifyQuestion: 'Where are SCALE setup, memory provider, and graphify knowledge integration implemented?',
419
+ timeoutMs: 900_000,
420
+ }
421
+ for (let index = 0; index < args.length; index += 1) {
422
+ const arg = args[index]
423
+ if (arg === '--skip-gbrain') parsed.skipGbrain = true
424
+ else if (arg === '--skip-graphify') parsed.skipGraphify = true
425
+ else if (arg === '--require-gbrain') parsed.requireGbrain = true
426
+ else if (arg === '--require-graphify') parsed.requireGraphify = true
427
+ else if (arg === '--keep-output') parsed.keepOutput = true
428
+ else if (arg === '--keep-gbrain-page') parsed.keepGbrainPage = true
429
+ else if (arg === '--verbose') parsed.verbose = true
430
+ else if (arg === '--no-rtk') parsed.useRtk = false
431
+ else if (arg === '--cluster') parsed.noCluster = false
432
+ else if (arg === '--semantic-extract') parsed.semanticExtract = true
433
+ else if (arg === '--global') parsed.globalExtract = true
434
+ else if (arg === '--global-add') parsed.globalAdd = true
435
+ else if (arg === '--global-tag') parsed.globalTag = requireValue(args, ++index, arg)
436
+ else if (arg === '--large-project') parsed.largeProject = requireValue(args, ++index, arg)
437
+ else if (arg === '--out') parsed.outDir = requireValue(args, ++index, arg)
438
+ else if (arg === '--report-file') parsed.reportFile = requireValue(args, ++index, arg)
439
+ else if (arg === '--write-report') parsed.writeReport = true
440
+ else if (arg === '--graphify-backend') parsed.graphifyBackend = requireValue(args, ++index, arg)
441
+ else if (arg === '--graphify-question') parsed.graphifyQuestion = requireValue(args, ++index, arg)
442
+ else if (arg === '--timeout-ms') parsed.timeoutMs = Number.parseInt(requireValue(args, ++index, arg), 10)
443
+ else if (arg === '--help' || arg === '-h') {
444
+ printHelp()
445
+ process.exit(0)
446
+ } else {
447
+ throw new Error(`Unknown argument: ${arg}`)
448
+ }
449
+ }
450
+ if (!Number.isFinite(parsed.timeoutMs) || parsed.timeoutMs <= 0) parsed.timeoutMs = 900_000
451
+ return parsed
452
+ }
453
+
454
+ function requireValue(args, index, flag) {
455
+ const value = args[index]
456
+ if (!value) throw new Error(`${flag} requires a value`)
457
+ return value
458
+ }
459
+
460
+ function printHelp() {
461
+ process.stdout.write(`Usage: node scripts/workflow/provider-rehearsal.mjs [options]
462
+
463
+ Runs real provider checks for:
464
+ - gbrain cross-process write/get/query replay
465
+ - graphify real-project extraction and graph query
466
+
467
+ Default mode records blocked capabilities without failing unless --require-gbrain or --require-graphify is set.
468
+
469
+ Options:
470
+ --require-gbrain Fail when gbrain is missing, unconfigured, or replay fails
471
+ --require-graphify Fail when graphify is missing or graph extraction/query fails
472
+ --skip-gbrain Skip gbrain replay
473
+ --skip-graphify Skip graphify rehearsal
474
+ --large-project PATH Project to extract with graphify, default current repo
475
+ --out PATH Output directory for temporary graphify artifacts
476
+ --keep-output Keep temporary output when --out is not supplied
477
+ --keep-gbrain-page Do not delete the temporary gbrain page
478
+ --semantic-extract Use graphify extract, which may call an LLM; default is graphify update with no LLM
479
+ --graphify-backend ID Pass --backend to graphify extract only with --semantic-extract
480
+ --graphify-question Q Query to ask graphify after graph generation
481
+ --global Pass --global to graphify extract only with --semantic-extract
482
+ --global-add Add generated graph to graphify global store
483
+ --global-tag TAG Tag for --global-add, default scale-engine-rehearsal
484
+ --write-report Write JSON report to .scale/reports
485
+ --report-file PATH Write JSON report to the supplied path
486
+ --timeout-ms N Command timeout, default 900000
487
+ --no-rtk Do not wrap provider CLIs through rtk
488
+ --verbose Print command lines to stderr
489
+ `)
490
+ }
491
+
492
+ function failureLine(value) {
493
+ const lines = value.split(/\r?\n/).map(line => line.trim()).filter(Boolean)
494
+ return lines.find(line => /^error[:\s]/i.test(line))
495
+ ?? lines.find(line => !/^warning[:\s]/i.test(line))
496
+ ?? lines[0]
497
+ ?? ''
498
+ }
499
+
500
+ function formatCommand(command, args) {
501
+ return [command, ...args].map(quoteArg).join(' ')
502
+ }
503
+
504
+ function quoteArg(value) {
505
+ const raw = String(value)
506
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(raw)) return raw
507
+ return `"${raw.replace(/(["\\$`])/g, '\\$1')}"`
508
+ }
509
+
510
+ function powershellQuote(value) {
511
+ return `'${String(value).replace(/'/g, "''")}'`
512
+ }
513
+
514
+ function tail(value, max = 4000) {
515
+ return value.length > max ? value.slice(-max) : value
516
+ }