@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.
- package/README.md +15 -0
- package/dist/api/cli.js +142 -40
- 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 +22 -1
- package/dist/bootstrap/DependencyBootstrap.js +427 -33
- package/dist/bootstrap/DependencyBootstrap.js.map +1 -1
- package/dist/bootstrap/DependencyBootstrapRenderer.d.ts +3 -0
- package/dist/bootstrap/DependencyBootstrapRenderer.js +140 -0
- package/dist/bootstrap/DependencyBootstrapRenderer.js.map +1 -0
- package/dist/cli/gateStatusCommands.d.ts +1 -0
- package/dist/cli/gateStatusCommands.js +52 -0
- package/dist/cli/gateStatusCommands.js.map +1 -0
- package/dist/cli/phaseCommands.js +15 -3
- package/dist/cli/phaseCommands.js.map +1 -1
- package/dist/cli/promptCommands.d.ts +1 -0
- package/dist/cli/promptCommands.js +57 -0
- package/dist/cli/promptCommands.js.map +1 -0
- package/dist/cli/scoreCommands.d.ts +1 -0
- package/dist/cli/scoreCommands.js +112 -0
- package/dist/cli/scoreCommands.js.map +1 -0
- package/dist/codegraph/CodeIntelligence.js +1 -1
- package/dist/codegraph/CodeIntelligence.js.map +1 -1
- package/dist/context/SessionStartSequence.js +13 -4
- package/dist/context/SessionStartSequence.js.map +1 -1
- package/dist/core/ExternalCommand.js +18 -4
- package/dist/core/ExternalCommand.js.map +1 -1
- package/dist/env/EnvironmentDoctor.d.ts +66 -0
- package/dist/env/EnvironmentDoctor.js +365 -0
- package/dist/env/EnvironmentDoctor.js.map +1 -0
- package/dist/i18n/Language.d.ts +9 -0
- package/dist/i18n/Language.js +38 -0
- package/dist/i18n/Language.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/memory/MemoryProviders.d.ts +8 -0
- package/dist/memory/MemoryProviders.js +217 -16
- package/dist/memory/MemoryProviders.js.map +1 -1
- package/dist/prompts/PromptOptimizer.d.ts +42 -0
- package/dist/prompts/PromptOptimizer.js +309 -0
- package/dist/prompts/PromptOptimizer.js.map +1 -0
- package/dist/setup/SetupWizard.d.ts +42 -0
- package/dist/setup/SetupWizard.js +156 -0
- package/dist/setup/SetupWizard.js.map +1 -0
- package/dist/skills/SkillRepository.js +2 -2
- package/dist/skills/SkillRepository.js.map +1 -1
- package/dist/testing/DiffTestSelector.js +1 -1
- package/dist/testing/DiffTestSelector.js.map +1 -1
- package/dist/tools/ToolCapabilityRegistry.d.ts +4 -0
- package/dist/tools/ToolCapabilityRegistry.js +11 -6
- package/dist/tools/ToolCapabilityRegistry.js.map +1 -1
- package/dist/workflow/CommitDiscipline.js +8 -7
- package/dist/workflow/CommitDiscipline.js.map +1 -1
- package/dist/workflow/CrossRepoOrchestrator.js +15 -7
- package/dist/workflow/CrossRepoOrchestrator.js.map +1 -1
- package/dist/workflow/GateCatalog.d.ts +61 -0
- package/dist/workflow/GateCatalog.js +212 -0
- package/dist/workflow/GateCatalog.js.map +1 -0
- package/dist/workflow/GovernanceTemplatePacks.js +19 -4
- package/dist/workflow/GovernanceTemplatePacks.js.map +1 -1
- package/dist/workflow/SessionPreamble.js +7 -2
- package/dist/workflow/SessionPreamble.js.map +1 -1
- package/dist/workflow/TaskScoreEngine.d.ts +42 -0
- package/dist/workflow/TaskScoreEngine.js +181 -0
- package/dist/workflow/TaskScoreEngine.js.map +1 -0
- package/dist/workflow/WorkspaceTopology.d.ts +3 -0
- package/dist/workflow/WorkspaceTopology.js +40 -3
- package/dist/workflow/WorkspaceTopology.js.map +1 -1
- package/dist/workflow/gates/GateSystem.js +2 -2
- package/dist/workflow/gates/GateSystem.js.map +1 -1
- package/dist/workflow/index.d.ts +2 -0
- package/dist/workflow/index.js +2 -0
- package/dist/workflow/index.js.map +1 -1
- package/docs/CODE_INTELLIGENCE.md +27 -2
- package/docs/MEMORY_FABRIC.md +22 -1
- package/docs/THIRD_PARTY_SKILLS.md +50 -1
- package/docs/guides/GETTING_STARTED.md +24 -0
- package/docs/start/quickstart.md +103 -76
- package/docs/workflow/GATES_AND_SCORE.md +56 -0
- package/docs/workflow/PROMPT_OPTIMIZATION.md +44 -0
- package/docs/workflow/README.md +7 -0
- package/docs/workflow/node-library.md +3 -3
- package/package.json +12 -5
- package/scripts/workflow/provider-rehearsal.mjs +516 -0
- 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
|
+
}
|