@hongmaple0820/scale-engine 0.38.0 → 0.39.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 (86) 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 +420 -32
  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.js +38 -5
  39. package/dist/memory/MemoryProviders.js.map +1 -1
  40. package/dist/prompts/PromptOptimizer.d.ts +42 -0
  41. package/dist/prompts/PromptOptimizer.js +309 -0
  42. package/dist/prompts/PromptOptimizer.js.map +1 -0
  43. package/dist/setup/SetupWizard.d.ts +42 -0
  44. package/dist/setup/SetupWizard.js +156 -0
  45. package/dist/setup/SetupWizard.js.map +1 -0
  46. package/dist/skills/SkillRepository.js +2 -2
  47. package/dist/skills/SkillRepository.js.map +1 -1
  48. package/dist/testing/DiffTestSelector.js +1 -1
  49. package/dist/testing/DiffTestSelector.js.map +1 -1
  50. package/dist/tools/ToolCapabilityRegistry.d.ts +4 -0
  51. package/dist/tools/ToolCapabilityRegistry.js +11 -6
  52. package/dist/tools/ToolCapabilityRegistry.js.map +1 -1
  53. package/dist/workflow/CommitDiscipline.js +8 -7
  54. package/dist/workflow/CommitDiscipline.js.map +1 -1
  55. package/dist/workflow/CrossRepoOrchestrator.js +15 -7
  56. package/dist/workflow/CrossRepoOrchestrator.js.map +1 -1
  57. package/dist/workflow/GateCatalog.d.ts +61 -0
  58. package/dist/workflow/GateCatalog.js +212 -0
  59. package/dist/workflow/GateCatalog.js.map +1 -0
  60. package/dist/workflow/GovernanceTemplatePacks.js +19 -4
  61. package/dist/workflow/GovernanceTemplatePacks.js.map +1 -1
  62. package/dist/workflow/SessionPreamble.js +7 -2
  63. package/dist/workflow/SessionPreamble.js.map +1 -1
  64. package/dist/workflow/TaskScoreEngine.d.ts +42 -0
  65. package/dist/workflow/TaskScoreEngine.js +181 -0
  66. package/dist/workflow/TaskScoreEngine.js.map +1 -0
  67. package/dist/workflow/WorkspaceTopology.d.ts +3 -0
  68. package/dist/workflow/WorkspaceTopology.js +40 -3
  69. package/dist/workflow/WorkspaceTopology.js.map +1 -1
  70. package/dist/workflow/gates/GateSystem.js +2 -2
  71. package/dist/workflow/gates/GateSystem.js.map +1 -1
  72. package/dist/workflow/index.d.ts +2 -0
  73. package/dist/workflow/index.js +2 -0
  74. package/dist/workflow/index.js.map +1 -1
  75. package/docs/CODE_INTELLIGENCE.md +27 -2
  76. package/docs/MEMORY_FABRIC.md +22 -1
  77. package/docs/THIRD_PARTY_SKILLS.md +50 -1
  78. package/docs/guides/GETTING_STARTED.md +24 -0
  79. package/docs/start/quickstart.md +103 -76
  80. package/docs/workflow/GATES_AND_SCORE.md +56 -0
  81. package/docs/workflow/PROMPT_OPTIMIZATION.md +44 -0
  82. package/docs/workflow/README.md +7 -0
  83. package/docs/workflow/node-library.md +3 -3
  84. package/package.json +11 -4
  85. package/scripts/workflow/provider-rehearsal.mjs +425 -0
  86. package/scripts/workflow/setup-smoke.mjs +299 -0
@@ -0,0 +1,425 @@
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
+
15
+ mkdirSync(workRoot, { recursive: true })
16
+
17
+ try {
18
+ const gbrain = options.skipGbrain ? skipped('gbrain') : runGbrainReplay()
19
+ const graphify = options.skipGraphify ? skipped('graphify') : runGraphifyRehearsal()
20
+ const report = buildReport([gbrain, graphify])
21
+ if (options.writeReport || options.reportFile) writeReport(report)
22
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
23
+ if (!report.ok) process.exitCode = 1
24
+ } finally {
25
+ if (!options.keepOutput && !options.outDir) rmSync(workRoot, { recursive: true, force: true })
26
+ }
27
+
28
+ function runGbrainReplay() {
29
+ const doctor = runCommand('gbrain-doctor', 'gbrain', ['doctor', '--json'], { timeoutMs: 30_000 })
30
+ if (doctor.exitCode !== 0) {
31
+ return capability('gbrain', options.requireGbrain ? 'failed' : 'blocked', {
32
+ reason: failureLine(`${doctor.stdout}\n${doctor.stderr}`) || 'gbrain doctor failed',
33
+ required: options.requireGbrain,
34
+ commands: [doctor],
35
+ nextCommands: [
36
+ 'gbrain init --supabase',
37
+ 'gbrain init --url <remote-gbrain-url>',
38
+ 'gbrain doctor --json',
39
+ 'npm run smoke:gbrain',
40
+ ],
41
+ })
42
+ }
43
+
44
+ const slug = `scale-rehearsal-${Date.now()}-${Math.random().toString(16).slice(2)}`
45
+ const sentinel = `scale-engine-gbrain-replay-${Date.now()}-${Math.random().toString(16).slice(2)}`
46
+ const body = [
47
+ `# ${slug}`,
48
+ '',
49
+ `Sentinel: ${sentinel}`,
50
+ '',
51
+ 'This page verifies that SCALE can write memory in one process and read/query it in later processes.',
52
+ ].join('\n')
53
+
54
+ const put = runCommand('gbrain-put', 'gbrain', ['put', slug], { input: body, timeoutMs: 60_000 })
55
+ const get = runCommand('gbrain-get', 'gbrain', ['get', slug], { timeoutMs: 60_000 })
56
+ const query = runCommand('gbrain-query', 'gbrain', ['query', sentinel], { timeoutMs: 60_000 })
57
+ const search = query.exitCode === 0 ? undefined : runCommand('gbrain-search', 'gbrain', ['search', sentinel], { timeoutMs: 60_000 })
58
+ const cleanup = options.keepGbrainPage ? undefined : runCommand('gbrain-delete', 'gbrain', ['delete', slug], { timeoutMs: 60_000 })
59
+
60
+ const recallOutput = `${query.stdout}\n${query.stderr}\n${search?.stdout ?? ''}\n${search?.stderr ?? ''}`
61
+ const replayPassed = put.exitCode === 0
62
+ && get.exitCode === 0
63
+ && get.stdout.includes(sentinel)
64
+ && (query.exitCode === 0 || search?.exitCode === 0)
65
+ && (recallOutput.includes(sentinel) || recallOutput.includes(slug))
66
+
67
+ return capability('gbrain', replayPassed ? 'passed' : 'failed', {
68
+ reason: replayPassed
69
+ ? 'remote gbrain write/get/query replay passed across separate CLI processes'
70
+ : 'gbrain was configured, but write/get/query replay did not prove recall',
71
+ required: options.requireGbrain,
72
+ sentinel,
73
+ slug,
74
+ commands: [doctor, put, get, query, search, cleanup].filter(Boolean),
75
+ nextCommands: replayPassed ? [] : ['gbrain doctor --json', `gbrain get ${slug}`, `gbrain query ${sentinel}`],
76
+ })
77
+ }
78
+
79
+ function runGraphifyRehearsal() {
80
+ const help = runCommand('graphify-help', 'graphify', ['--help'], { timeoutMs: 30_000 })
81
+ if (help.exitCode !== 0) {
82
+ return capability('graphify', options.requireGraphify ? 'failed' : 'blocked', {
83
+ reason: failureLine(`${help.stdout}\n${help.stderr}`) || 'graphify CLI is not available',
84
+ required: options.requireGraphify,
85
+ commands: [help],
86
+ nextCommands: [
87
+ 'uv tool install graphify',
88
+ 'graphify install --platform codex',
89
+ 'npm run smoke:graphify',
90
+ ],
91
+ })
92
+ }
93
+
94
+ const graphOut = resolve(workRoot, 'graphify-out')
95
+ mkdirSync(graphOut, { recursive: true })
96
+ const extractArgs = ['extract', resolve(options.largeProject), '--out', graphOut]
97
+ if (options.graphifyBackend) extractArgs.push('--backend', options.graphifyBackend)
98
+ if (options.noCluster) extractArgs.push('--no-cluster')
99
+ if (options.globalExtract) extractArgs.push('--global')
100
+
101
+ const extract = runCommand('graphify-extract', 'graphify', extractArgs, { timeoutMs: options.timeoutMs })
102
+ if (extract.exitCode !== 0) {
103
+ return capability('graphify', options.requireGraphify ? 'failed' : 'blocked', {
104
+ reason: failureLine(`${extract.stdout}\n${extract.stderr}`) || 'graphify extract failed',
105
+ required: options.requireGraphify,
106
+ commands: [help, extract],
107
+ nextCommands: [
108
+ 'graphify install --platform codex',
109
+ 'graphify hook status',
110
+ 'Set one LLM key: GEMINI_API_KEY, GOOGLE_API_KEY, MOONSHOT_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY, or DEEPSEEK_API_KEY',
111
+ `graphify extract ${quoteArg(resolve(options.largeProject))} --out ${quoteArg(graphOut)} --no-cluster`,
112
+ ],
113
+ })
114
+ }
115
+
116
+ const graphPath = findGraphJson(graphOut)
117
+ if (!graphPath) {
118
+ return capability('graphify', options.requireGraphify ? 'failed' : 'blocked', {
119
+ reason: `graphify extract completed but graph.json was not found under ${graphOut}`,
120
+ required: options.requireGraphify,
121
+ commands: [help, extract],
122
+ nextCommands: [`Get-ChildItem -Recurse ${quoteArg(graphOut)}`],
123
+ })
124
+ }
125
+
126
+ const stats = parseGraphStats(graphPath)
127
+ const query = runCommand('graphify-query', 'graphify', [
128
+ 'query',
129
+ options.graphifyQuestion,
130
+ '--graph',
131
+ graphPath,
132
+ ], { timeoutMs: 120_000 })
133
+ const benchmark = runCommand('graphify-benchmark', 'graphify', ['benchmark', graphPath], { timeoutMs: 120_000 })
134
+ const globalAdd = options.globalAdd
135
+ ? runCommand('graphify-global-add', 'graphify', ['global', 'add', graphPath, '--as', options.globalTag], { timeoutMs: 120_000 })
136
+ : undefined
137
+
138
+ const passed = stats.ok && query.exitCode === 0
139
+ return capability('graphify', passed ? 'passed' : options.requireGraphify ? 'failed' : 'blocked', {
140
+ reason: passed
141
+ ? 'graphify extracted a real project graph and answered a graph query'
142
+ : 'graphify generated an artifact but graph stats or query validation failed',
143
+ required: options.requireGraphify,
144
+ project: resolve(options.largeProject),
145
+ graphPath,
146
+ stats,
147
+ commands: [help, extract, query, benchmark, globalAdd].filter(Boolean),
148
+ nextCommands: passed ? [] : [`graphify query ${quoteArg(options.graphifyQuestion)} --graph ${quoteArg(graphPath)}`],
149
+ })
150
+ }
151
+
152
+ function parseGraphStats(graphPath) {
153
+ try {
154
+ const graph = JSON.parse(readFileSync(graphPath, 'utf8'))
155
+ const nodes = firstArray(graph.nodes, graph.graph?.nodes, graph.data?.nodes)
156
+ const edges = firstArray(graph.edges, graph.links, graph.graph?.edges, graph.graph?.links, graph.data?.edges, graph.data?.links)
157
+ return {
158
+ ok: nodes.length > 0,
159
+ nodes: nodes.length,
160
+ edges: edges.length,
161
+ }
162
+ } catch (error) {
163
+ return {
164
+ ok: false,
165
+ nodes: 0,
166
+ edges: 0,
167
+ error: String(error.message ?? error),
168
+ }
169
+ }
170
+ }
171
+
172
+ function firstArray(...values) {
173
+ return values.find(Array.isArray) ?? []
174
+ }
175
+
176
+ function findGraphJson(root) {
177
+ const direct = [
178
+ join(root, 'graph.json'),
179
+ join(root, 'graphify-out', 'graph.json'),
180
+ join(root, 'graph', 'graph.json'),
181
+ ].find(candidate => existsSync(candidate))
182
+ if (direct) return direct
183
+
184
+ const scan = runCommand('find-graph-json', process.platform === 'win32' ? 'powershell' : 'find', process.platform === 'win32'
185
+ ? ['-NoProfile', '-Command', `Get-ChildItem -Path ${quoteArg(root)} -Recurse -Filter graph.json | Select-Object -First 1 -ExpandProperty FullName`]
186
+ : [root, '-name', 'graph.json', '-print', '-quit'], { timeoutMs: 30_000, wrapRtk: false })
187
+ const found = scan.stdout.trim().split(/\r?\n/).find(Boolean)
188
+ return found && existsSync(found) ? found : null
189
+ }
190
+
191
+ function skipped(id) {
192
+ return capability(id, 'skipped', { reason: `${id} rehearsal was skipped`, required: false, commands: [], nextCommands: [] })
193
+ }
194
+
195
+ function capability(id, status, details) {
196
+ return {
197
+ id,
198
+ status,
199
+ required: Boolean(details.required),
200
+ reason: details.reason,
201
+ nextCommands: details.nextCommands ?? [],
202
+ ...Object.fromEntries(Object.entries(details).filter(([key]) => !['required', 'reason', 'nextCommands'].includes(key))),
203
+ }
204
+ }
205
+
206
+ function buildReport(capabilities) {
207
+ const failedRequired = capabilities.filter(item => item.required && item.status !== 'passed' && item.status !== 'skipped')
208
+ return {
209
+ version: 1,
210
+ ok: failedRequired.length === 0,
211
+ status: failedRequired.length === 0 ? 'completed' : 'failed',
212
+ runId,
213
+ generatedAt: new Date().toISOString(),
214
+ repoRoot,
215
+ workRoot,
216
+ rtkWrapped: options.useRtk && commandExists('rtk'),
217
+ capabilities,
218
+ results,
219
+ }
220
+ }
221
+
222
+ function writeReport(report) {
223
+ const target = options.reportFile
224
+ ? resolve(options.reportFile)
225
+ : join(repoRoot, '.scale', 'reports', `${runId}.json`)
226
+ mkdirSync(dirname(target), { recursive: true })
227
+ report.reportFile = target
228
+ writeFileSync(target, `${JSON.stringify(report, null, 2)}\n`, 'utf8')
229
+ }
230
+
231
+ function runCommand(name, command, args, opts = {}) {
232
+ const invocation = opts.wrapRtk === false
233
+ ? { command, args, wrapped: false }
234
+ : wrapWithRtk(command, args)
235
+ const startedAt = new Date().toISOString()
236
+ const commandLine = formatCommand(invocation.command, invocation.args)
237
+ if (options.verbose) process.stderr.write(`[RUN] ${commandLine}\n`)
238
+ const result = spawnStructured(invocation.command, invocation.args, {
239
+ cwd: repoRoot,
240
+ env: process.env,
241
+ input: opts.input,
242
+ encoding: 'utf8',
243
+ timeout: opts.timeoutMs ?? options.timeoutMs,
244
+ maxBuffer: 80 * 1024 * 1024,
245
+ })
246
+ const stdout = String(result.stdout ?? '')
247
+ const stderr = String(result.stderr ?? '') + (result.error ? `\n${result.error.message}` : '')
248
+ const exitCode = typeof result.status === 'number' ? result.status : 1
249
+ const entry = {
250
+ name,
251
+ command: commandLine,
252
+ wrappedByRtk: invocation.wrapped,
253
+ exitCode,
254
+ startedAt,
255
+ endedAt: new Date().toISOString(),
256
+ stdoutTail: tail(stdout),
257
+ stderrTail: tail(stderr),
258
+ }
259
+ results.push(entry)
260
+ return { ...entry, stdout, stderr }
261
+ }
262
+
263
+ function wrapWithRtk(command, args) {
264
+ if (!options.useRtk || command === 'rtk' || !commandExists('rtk')) return { command, args, wrapped: false }
265
+ return { command: 'rtk', args: [command, ...args], wrapped: true }
266
+ }
267
+
268
+ function spawnStructured(command, args, options) {
269
+ const resolved = resolveWindowsCommandShim(resolveCommandPath(command) ?? command)
270
+ if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(resolved)) {
271
+ const comspec = process.env.ComSpec || 'cmd.exe'
272
+ return spawnSync(comspec, ['/d', '/c', 'call', resolved, ...args], {
273
+ ...options,
274
+ shell: false,
275
+ windowsHide: true,
276
+ })
277
+ }
278
+ return spawnSync(resolved, args, {
279
+ ...options,
280
+ shell: false,
281
+ windowsHide: true,
282
+ })
283
+ }
284
+
285
+ function resolveWindowsCommandShim(command) {
286
+ if (process.platform !== 'win32') return command
287
+ if (!/[\\/]/.test(command) || extname(command)) return command
288
+ for (const extension of ['.cmd', '.exe', '.bat', '.com']) {
289
+ const candidate = `${command}${extension}`
290
+ if (existsSync(candidate)) return candidate
291
+ }
292
+ return command
293
+ }
294
+
295
+ function resolveCommandPath(command) {
296
+ if (/[\\/]/.test(command)) return command
297
+ const lookup = process.platform === 'win32' ? 'where.exe' : 'which'
298
+ const result = spawnSync(lookup, [command], {
299
+ encoding: 'utf8',
300
+ shell: false,
301
+ windowsHide: true,
302
+ })
303
+ if (result.status !== 0) return null
304
+ return String(result.stdout ?? '')
305
+ .split(/\r?\n/)
306
+ .map(line => line.trim())
307
+ .find(Boolean) ?? null
308
+ }
309
+
310
+ function commandExists(command) {
311
+ return Boolean(resolveCommandPath(command))
312
+ }
313
+
314
+ function parseArgs(args) {
315
+ const parsed = {
316
+ skipGbrain: false,
317
+ skipGraphify: false,
318
+ requireGbrain: false,
319
+ requireGraphify: false,
320
+ keepOutput: false,
321
+ keepGbrainPage: false,
322
+ verbose: false,
323
+ useRtk: true,
324
+ noCluster: true,
325
+ globalExtract: false,
326
+ globalAdd: false,
327
+ globalTag: 'scale-engine-rehearsal',
328
+ largeProject: repoRoot,
329
+ outDir: undefined,
330
+ reportFile: undefined,
331
+ writeReport: false,
332
+ graphifyBackend: undefined,
333
+ graphifyQuestion: 'Where are SCALE setup, memory provider, and graphify knowledge integration implemented?',
334
+ timeoutMs: 900_000,
335
+ }
336
+ for (let index = 0; index < args.length; index += 1) {
337
+ const arg = args[index]
338
+ if (arg === '--skip-gbrain') parsed.skipGbrain = true
339
+ else if (arg === '--skip-graphify') parsed.skipGraphify = true
340
+ else if (arg === '--require-gbrain') parsed.requireGbrain = true
341
+ else if (arg === '--require-graphify') parsed.requireGraphify = true
342
+ else if (arg === '--keep-output') parsed.keepOutput = true
343
+ else if (arg === '--keep-gbrain-page') parsed.keepGbrainPage = true
344
+ else if (arg === '--verbose') parsed.verbose = true
345
+ else if (arg === '--no-rtk') parsed.useRtk = false
346
+ else if (arg === '--cluster') parsed.noCluster = false
347
+ else if (arg === '--global') parsed.globalExtract = true
348
+ else if (arg === '--global-add') parsed.globalAdd = true
349
+ else if (arg === '--global-tag') parsed.globalTag = requireValue(args, ++index, arg)
350
+ else if (arg === '--large-project') parsed.largeProject = requireValue(args, ++index, arg)
351
+ else if (arg === '--out') parsed.outDir = requireValue(args, ++index, arg)
352
+ else if (arg === '--report-file') parsed.reportFile = requireValue(args, ++index, arg)
353
+ else if (arg === '--write-report') parsed.writeReport = true
354
+ else if (arg === '--graphify-backend') parsed.graphifyBackend = requireValue(args, ++index, arg)
355
+ else if (arg === '--graphify-question') parsed.graphifyQuestion = requireValue(args, ++index, arg)
356
+ else if (arg === '--timeout-ms') parsed.timeoutMs = Number.parseInt(requireValue(args, ++index, arg), 10)
357
+ else if (arg === '--help' || arg === '-h') {
358
+ printHelp()
359
+ process.exit(0)
360
+ } else {
361
+ throw new Error(`Unknown argument: ${arg}`)
362
+ }
363
+ }
364
+ if (!Number.isFinite(parsed.timeoutMs) || parsed.timeoutMs <= 0) parsed.timeoutMs = 900_000
365
+ return parsed
366
+ }
367
+
368
+ function requireValue(args, index, flag) {
369
+ const value = args[index]
370
+ if (!value) throw new Error(`${flag} requires a value`)
371
+ return value
372
+ }
373
+
374
+ function printHelp() {
375
+ process.stdout.write(`Usage: node scripts/workflow/provider-rehearsal.mjs [options]
376
+
377
+ Runs real provider checks for:
378
+ - gbrain cross-process write/get/query replay
379
+ - graphify real-project extraction and graph query
380
+
381
+ Default mode records blocked capabilities without failing unless --require-gbrain or --require-graphify is set.
382
+
383
+ Options:
384
+ --require-gbrain Fail when gbrain is missing, unconfigured, or replay fails
385
+ --require-graphify Fail when graphify is missing or graph extraction/query fails
386
+ --skip-gbrain Skip gbrain replay
387
+ --skip-graphify Skip graphify rehearsal
388
+ --large-project PATH Project to extract with graphify, default current repo
389
+ --out PATH Output directory for temporary graphify artifacts
390
+ --keep-output Keep temporary output when --out is not supplied
391
+ --keep-gbrain-page Do not delete the temporary gbrain page
392
+ --graphify-backend ID Pass --backend to graphify extract
393
+ --graphify-question Q Query to ask graphify after extraction
394
+ --global Pass --global to graphify extract
395
+ --global-add Add generated graph to graphify global store
396
+ --global-tag TAG Tag for --global-add, default scale-engine-rehearsal
397
+ --write-report Write JSON report to .scale/reports
398
+ --report-file PATH Write JSON report to the supplied path
399
+ --timeout-ms N Command timeout, default 900000
400
+ --no-rtk Do not wrap provider CLIs through rtk
401
+ --verbose Print command lines to stderr
402
+ `)
403
+ }
404
+
405
+ function failureLine(value) {
406
+ const lines = value.split(/\r?\n/).map(line => line.trim()).filter(Boolean)
407
+ return lines.find(line => /^error[:\s]/i.test(line))
408
+ ?? lines.find(line => !/^warning[:\s]/i.test(line))
409
+ ?? lines[0]
410
+ ?? ''
411
+ }
412
+
413
+ function formatCommand(command, args) {
414
+ return [command, ...args].map(quoteArg).join(' ')
415
+ }
416
+
417
+ function quoteArg(value) {
418
+ const raw = String(value)
419
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(raw)) return raw
420
+ return `"${raw.replace(/(["\\$`])/g, '\\$1')}"`
421
+ }
422
+
423
+ function tail(value, max = 4000) {
424
+ return value.length > max ? value.slice(-max) : value
425
+ }