@hongmaple0820/scale-engine 0.40.0 → 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 (47) hide show
  1. package/dist/api/cli.js +421 -26
  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 +2 -0
  6. package/dist/bootstrap/DependencyBootstrap.js +250 -39
  7. package/dist/bootstrap/DependencyBootstrap.js.map +1 -1
  8. package/dist/bootstrap/DependencyBootstrapRenderer.js +2 -4
  9. package/dist/bootstrap/DependencyBootstrapRenderer.js.map +1 -1
  10. package/dist/capabilities/InstalledSkillsIntegration.js +29 -9
  11. package/dist/capabilities/InstalledSkillsIntegration.js.map +1 -1
  12. package/dist/context/ContextBudget.js +2 -2
  13. package/dist/core/GbrainRuntime.d.ts +25 -0
  14. package/dist/core/GbrainRuntime.js +270 -0
  15. package/dist/core/GbrainRuntime.js.map +1 -0
  16. package/dist/env/EnvironmentDoctor.js +221 -5
  17. package/dist/env/EnvironmentDoctor.js.map +1 -1
  18. package/dist/memory/MemoryProviders.js +38 -91
  19. package/dist/memory/MemoryProviders.js.map +1 -1
  20. package/dist/runtime/ModelUsageLedger.d.ts +53 -2
  21. package/dist/runtime/ModelUsageLedger.js +243 -39
  22. package/dist/runtime/ModelUsageLedger.js.map +1 -1
  23. package/dist/setup/SetupVerification.d.ts +42 -0
  24. package/dist/setup/SetupVerification.js +180 -0
  25. package/dist/setup/SetupVerification.js.map +1 -0
  26. package/dist/setup/SetupWizard.d.ts +3 -0
  27. package/dist/setup/SetupWizard.js +79 -19
  28. package/dist/setup/SetupWizard.js.map +1 -1
  29. package/dist/skills/SkillDoctor.js +2 -2
  30. package/dist/skills/SkillDoctor.js.map +1 -1
  31. package/dist/skills/SkillRepository.js +2 -2
  32. package/dist/skills/SkillRepository.js.map +1 -1
  33. package/dist/tools/ToolCapabilityRegistry.js +12 -2
  34. package/dist/tools/ToolCapabilityRegistry.js.map +1 -1
  35. package/dist/workflow/VerificationProfile.js +1 -1
  36. package/dist/workflow/VerificationProfile.js.map +1 -1
  37. package/docs/CONTEXT_BUDGET.md +12 -2
  38. package/docs/GOVERNANCE_DASHBOARD.md +7 -0
  39. package/docs/THIRD_PARTY_SKILLS.md +12 -4
  40. package/docs/start/README.md +2 -2
  41. package/docs/start/quickstart.md +54 -44
  42. package/docs/start/workflow-upgrade.md +8 -1
  43. package/package.json +3 -2
  44. package/scripts/workflow/lib/gbrain-runtime.mjs +185 -0
  45. package/scripts/workflow/lib/report-output.mjs +107 -0
  46. package/scripts/workflow/provider-rehearsal.mjs +129 -48
  47. package/scripts/workflow/setup-smoke.mjs +142 -8
@@ -32,27 +32,48 @@ cd scale-demo
32
32
  scale init --governance-pack standard
33
33
  ```
34
34
 
35
- 初始化会生成 `.scale/`、`docs/`、`scripts/` 以及对应 Agent 入口文件。已有项目升级不要盲目重跑 `init`,优先使用:
35
+ 初始化会生成 `.scale/`、`docs/`、`scripts/` 以及对应 Agent 入口文件。已有项目升级不要盲目重复 `init`,优先使用升级向导:
36
+
37
+ ```bash
38
+ scale upgrade --dir .
39
+ ```
40
+
41
+ 向导会生成升级计划、写入 HTML 报告,并在安全时询问是否应用。CI 或高级用户仍可使用分步命令:
36
42
 
37
43
  ```bash
38
44
  scale upgrade check --dir . --lang zh
39
45
  scale upgrade plan --dir . --html --lang zh
46
+ scale upgrade apply --dir . --confirm --lang zh
40
47
  ```
41
48
 
42
49
  ## 3. 交互式安装第三方能力
43
50
 
44
- 默认输出语言是中文。需要英文时加 `--lang en`,也可以设置 `SCALE_LANG=en`。
51
+ 默认语言是中文。需要英文时加 `--lang en`,也可以设置 `SCALE_LANG=en`。
45
52
 
46
- 推荐先只查看计划:
53
+ 直接进入交互式安装:
54
+
55
+ ```bash
56
+ scale setup
57
+ ```
58
+
59
+ 交互式安装会询问:
60
+
61
+ - 语言:默认中文。
62
+ - 安装包:标准、前端/UI、AI OS、完整、自定义。
63
+ - 记忆供应商:默认 `gbrain`,也可切换到 `scale-local`。
64
+ - 记忆路由模式:默认 `external-first`。
65
+ - 是否执行安装:可跳过、全量安装、或只安装选中的第三方项。
66
+
67
+ 只查看计划:
47
68
 
48
69
  ```bash
49
70
  scale setup --pack full
50
71
  ```
51
72
 
52
- 确认后安装:
73
+ 确认后执行安装:
53
74
 
54
75
  ```bash
55
- scale setup --pack full --yes
76
+ scale setup --pack full --apply --yes
56
77
  ```
57
78
 
58
79
  机器可读输出:
@@ -61,38 +82,46 @@ scale setup --pack full --yes
61
82
  scale setup --pack full --json
62
83
  ```
63
84
 
64
- `setup` 和 `bootstrap deps` 都会输出 `runtimeChecks`。如果机器缺少 `python`、`bun`、`cargo`、`uv/pipx`、`node/npm/npx`,会先显示缺失项和修复建议,再决定是否执行 `--yes` 或 `--apply`,避免安装中途卡住。
85
+ `setup` 和 `bootstrap deps` 都会输出 `runtimeChecks`。如果机器缺少 `python`、`bun`、`cargo`、`uv/pipx`、`node/npm/npx`,会先显示缺失项和修复建议,再决定是否执行 `--apply`,避免安装中途卡住。
65
86
 
66
- 记忆供应商可以在安装入口直接切换,不需要手改 `.scale/memory-providers.json`:
87
+ 记忆供应商可在安装入口直接切换,不需要手改 `.scale/memory-providers.json`:
67
88
 
68
89
  ```bash
69
90
  scale setup --pack memory --memory-provider scale-local --json
70
91
  scale setup --pack memory --memory-provider gbrain --memory-mode external-first --json
71
92
  ```
72
93
 
73
- 第三方能力的职责边界:
94
+ ## 4. UI Skills 默认策略
95
+
96
+ | 能力 | 默认定位 | 安装方式 | 关键验证 |
97
+ | --- | --- | --- | --- |
98
+ | `awesome-design-md` | 品牌、视觉语言、`DESIGN.md` 来源 | `scale setup --pack ui --include awesome-design-md --apply` | 生成 `~/.agents/skills/awesome-design-md/SKILL.md`,同步 `~/.scale/vendor/awesome-design-md` |
99
+ | `ui-ux-pro-max` | UX、状态、可访问性、响应式验收 | `scale setup --pack ui --include ui-ux-pro-max --apply` | 生成 `~/.agents/skills/ui-ux-pro-max/SKILL.md`,同步 `~/.scale/vendor/ui-ux-pro-max` |
100
+ | `frontend-design` | 可选实现陪跑,不再是 UI 默认必装项 | `scale setup --pack ui --include frontend-design --apply` | 需要时显式安装 |
101
+
102
+ 安装器优先使用 `git clone --depth 1` 同步上游仓库;如果没有 Git 但有 npx,会退回 `npx degit`。缺少两者时不会硬跑失败,会在安装计划里标记为需要人工处理并给出下一步。
103
+
104
+ ## 5. 其他第三方能力边界
74
105
 
75
106
  | 能力 | 默认定位 | 关键验证 |
76
107
  | --- | --- | --- |
77
- | `awesome-design-md` | 品牌、视觉语言、`DESIGN.md` 来源 | 是否同步上游 DESIGN.md catalog |
78
- | `ui-ux-pro-max` | UX、状态、可访问性、响应式验收 | 是否通过官方 `uipro-cli` 安装 |
79
- | `frontend-design` | 可选实现灵感,不再是 UI 默认必装项 | 需要时显式 `--include frontend-design` |
80
108
  | `rtk` | CLI proxy/token 节省能力 | `rtk gain` 和 `rtk init -g --codex` |
81
- | `gbrain` | 默认记忆供应商 | 检查 brain 是否已配置且连接/schema 可用,未初始化会提示 `gbrain init --pglite` |
109
+ | `gbrain` | 默认记忆供应商 | 检查 brain 是否已配置且连接/schema 可用;未初始化会提示 `gbrain init --pglite` |
82
110
  | `graphify` | 知识图谱产物供应商 | `graphify install --platform codex` 和 `graphify-out/graph.json` |
83
111
  | `codegraph` | 代码结构索引供应商 | `codegraph init -i` 和 `.codegraph/` |
84
112
 
85
- 低层命令仍可直接使用:
113
+ 底层命令仍可直接使用:
86
114
 
87
115
  ```bash
88
116
  scale bootstrap deps --profile advanced --governance-pack frontend-app --lang zh
89
117
  scale bootstrap deps --profile advanced --governance-pack frontend-app --apply --lang zh
90
118
  ```
91
119
 
92
- ## 4. 验证闭环
120
+ ## 6. 验证闭环
93
121
 
94
122
  ```bash
95
123
  scale doctor
124
+ scale setup --verify --pack full --json
96
125
  scale preflight --preflight-profile quick
97
126
  scale status
98
127
  scale assets scan --dir .
@@ -114,11 +143,11 @@ npm run smoke:graphify -- --large-project /path/to/large-project
114
143
 
115
144
  验证语义:
116
145
 
117
- - `smoke:gbrain` 会先确认 gbrain 已配置且召回关键健康检查可用,通过后写入一个临时记忆页,再用独立 CLI 进程 `get/query/search` 回放,证明不是本地 mock。
146
+ - `smoke:gbrain` 会先确认 gbrain 已配置且关键健康检查可用,通过后写入一个临时记忆页,再用独立 CLI 进程 `get/query/search` 回放,证明不是本地 mock。
118
147
  - `smoke:graphify` 默认对真实项目执行 `graphify update <project> --no-cluster`,走 AST/Python 无模型路径,检查 `graph.json`,再执行 `graphify query`;只有显式 `--semantic-extract` 才允许语义模型提取。
119
- - `graphify-out/` 是生成产物,不应该提交到 Git;长期知识沉淀应进入经过评审的 `memory/`、docs 或规则文件。
148
+ - `graphify-out/` 是生成产物,不应该提交到 Git;长期知识沉淀应进入经过评审的 `memory/`、`docs` 或规则文件。
120
149
 
121
- ## 5. 建立任务上下文
150
+ ## 7. 建立任务上下文
122
151
 
123
152
  ```bash
124
153
  scale context init --name "Scale Demo"
@@ -139,7 +168,7 @@ scale memory settle --task-id 2026-05-18-oauth-hardening --session-id 2026-05-18
139
168
 
140
169
  `memory settle` 默认只生成学习候选,不会自动把一次会话判断提升成长线规则。存在失败证据时,候选会要求先解决失败,避免把未闭环问题沉淀成经验。
141
170
 
142
- ## 6. MOE/多仓工作区
171
+ ## 8. MOE/多仓工作区
143
172
 
144
173
  多仓项目使用:
145
174
 
@@ -151,31 +180,12 @@ MOE 默认把子工程配置为兄弟仓库或绝对路径,不建议把独立
151
180
 
152
181
  ```json
153
182
  {
154
- "topology": "moe",
155
- "repositories": [
156
- { "name": "root", "path": ".", "role": "root", "required": true },
157
- { "name": "api", "path": "../api", "role": "external", "required": true, "remote": "origin" }
158
- ]
183
+ "workspace": {
184
+ "type": "moe",
185
+ "repositories": [
186
+ { "name": "api", "path": "../api", "role": "service" },
187
+ { "name": "web", "path": "../web", "role": "frontend" }
188
+ ]
189
+ }
159
190
  }
160
191
  ```
161
-
162
- 这样可以避免子工程 Git 状态、分支、提交和主工程互相污染。
163
-
164
- ## 7. 安装烟测
165
-
166
- 仓库开发和发版前可以一键验证安装入口:
167
-
168
- ```bash
169
- npm run smoke:setup
170
- npm run smoke:providers
171
- make setup-smoke
172
- ```
173
-
174
- `smoke:setup` 只验证安装计划、双语输出、运行时依赖诊断、记忆供应商切换和 CodeGraph/Graphify 状态路径,不会执行真实第三方安装。
175
- `smoke:providers` 会执行真实供应商回放;未配置 gbrain 或 graphify 时输出 `blocked` 和修复命令,只有 `smoke:gbrain`/`smoke:graphify` 或显式 `--require-*` 才会失败退出。
176
-
177
- 遇到跨系统命令兼容、PATH 或运行时依赖问题时,先导出环境诊断:
178
-
179
- ```bash
180
- scale doctor env --json
181
- ```
@@ -51,7 +51,14 @@ scale init --governance-pack frontend-app
51
51
 
52
52
  ## 更新已有工作流
53
53
 
54
- 按这个顺序运行:
54
+ 默认使用升级向导:
55
+
56
+ ```bash
57
+ scale upgrade --dir .
58
+ scale preflight --dir . --service all --preflight-profile quick
59
+ ```
60
+
61
+ 向导会生成计划和 HTML 报告;如果计划可安全应用,会在交互式终端询问是否立即应用。需要 CI 或精确控制时,再使用分步命令:
55
62
 
56
63
  ```bash
57
64
  scale upgrade check --dir .
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hongmaple0820/scale-engine",
3
- "version": "0.40.0",
3
+ "version": "0.40.2",
4
4
  "description": "Executable AI agent governance with workflow gates, evidence, skill/tool orchestration, and traceable HTML artifacts",
5
5
  "repository": {
6
6
  "type": "git",
@@ -48,6 +48,7 @@
48
48
  "docs/workflow",
49
49
  "image",
50
50
  "examples/demo-projects/agent-governance-demo",
51
+ "scripts/workflow/lib",
51
52
  "scripts/workflow/setup-smoke.mjs",
52
53
  "scripts/workflow/provider-rehearsal.mjs"
53
54
  ],
@@ -65,7 +66,7 @@
65
66
  "smoke:providers": "node scripts/workflow/provider-rehearsal.mjs",
66
67
  "smoke:gbrain": "node scripts/workflow/provider-rehearsal.mjs --skip-graphify --require-gbrain",
67
68
  "smoke:graphify": "node scripts/workflow/provider-rehearsal.mjs --skip-gbrain --require-graphify",
68
- "release:check": "npm run typecheck && npm run lint && npm test && npm run smoke:setup && npm run build && npm audit --omit=dev && git diff --check && npm pack --dry-run",
69
+ "release:check": "npm run typecheck && npm run lint && npm test && npm run smoke:setup && npm run smoke:providers -- --write-report && npm run build && npm audit --omit=dev && git diff --check && npm pack --dry-run",
69
70
  "mcp": "node dist/api/mcp.js",
70
71
  "serve": "node dist/api/http.js"
71
72
  },
@@ -0,0 +1,185 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { cpSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import { dirname, join, resolve } from 'node:path'
5
+
6
+ const MIRROR_META_FILE = '.scale-gbrain-runtime.json'
7
+ const GBRAIN_TIMEOUT_RECOVERY_COMMANDS = new Set(['--version', 'version', 'doctor', 'list', 'get', 'query', 'search'])
8
+
9
+ export function resolveDirectWindowsGbrainInvocation(command, args, resolveCommandPath) {
10
+ if (process.platform !== 'win32' || command !== 'gbrain') return null
11
+ const gbrainShim = resolveCommandPath('gbrain')
12
+ if (!gbrainShim || !/\.cmd$/i.test(gbrainShim) || !existsSync(gbrainShim)) return null
13
+ try {
14
+ const content = readFileSync(gbrainShim, 'utf8')
15
+ const match = content.match(/call\s+"([^"]*bun\.cmd)"\s+"([^"]*src[\\/]cli\.ts)"/i)
16
+ const cliPath = match?.[2]
17
+ const bunShim = match?.[1] ?? resolveCommandPath('bun')
18
+ const bunExe = bunShim ? join(dirname(bunShim), 'node_modules', 'bun', 'bin', 'bun.exe') : ''
19
+ if (cliPath && bunExe && existsSync(bunExe)) {
20
+ return {
21
+ command: bunExe,
22
+ args: [cliPath, ...args],
23
+ cwd: dirname(dirname(cliPath)),
24
+ cliPath,
25
+ }
26
+ }
27
+ } catch {
28
+ return null
29
+ }
30
+ return null
31
+ }
32
+
33
+ export function shouldRetryWithMirroredGbrain(invocation, result) {
34
+ if (!invocation?.cliPath) return false
35
+ if (result.status === 0) return false
36
+ const output = `${String(result.stdout ?? '')}\n${String(result.stderr ?? '')}\n${String(result.error?.message ?? '')}`
37
+ if (!/EPERM reading/i.test(output)) return false
38
+ return normalizeForCompare(output).includes(normalizeForCompare(invocation.cliPath))
39
+ }
40
+
41
+ export function ensureMirroredGbrainInvocation(invocation) {
42
+ const packageRoot = invocation.cwd
43
+ const cliMtimeMs = statSync(invocation.cliPath).mtimeMs
44
+ const version = readPackageVersion(packageRoot)
45
+ const mirrorKey = `${resolve(packageRoot)}|${version ?? ''}|${cliMtimeMs}`
46
+ const mirrorRoot = join(
47
+ tmpdir(),
48
+ 'scale-engine',
49
+ 'gbrain-runtime',
50
+ createHash('sha1').update(mirrorKey).digest('hex').slice(0, 16),
51
+ )
52
+ const cliRelativePath = invocation.cliPath.slice(packageRoot.length + 1)
53
+ const mirrorCliPath = join(mirrorRoot, cliRelativePath)
54
+ let selectedRoot = mirrorRoot
55
+ if (!isMirrorFresh(mirrorRoot, packageRoot, version, cliMtimeMs, mirrorCliPath)) {
56
+ const stagedRoot = `${mirrorRoot}-staging-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`
57
+ rmSync(stagedRoot, { recursive: true, force: true })
58
+ mkdirSync(dirname(mirrorRoot), { recursive: true })
59
+ cpSync(packageRoot, stagedRoot, { recursive: true, force: true })
60
+ writeFileSync(join(stagedRoot, MIRROR_META_FILE), JSON.stringify({
61
+ sourceRoot: packageRoot,
62
+ version,
63
+ cliMtimeMs,
64
+ }, null, 2), 'utf8')
65
+ const stagedCliPath = join(stagedRoot, cliRelativePath)
66
+ if (!isMirrorFresh(mirrorRoot, packageRoot, version, cliMtimeMs, mirrorCliPath)) {
67
+ try {
68
+ renameSync(stagedRoot, mirrorRoot)
69
+ } catch {
70
+ if (isMirrorFresh(mirrorRoot, packageRoot, version, cliMtimeMs, mirrorCliPath)) {
71
+ rmSync(stagedRoot, { recursive: true, force: true })
72
+ } else {
73
+ selectedRoot = stagedRoot
74
+ }
75
+ }
76
+ } else {
77
+ rmSync(stagedRoot, { recursive: true, force: true })
78
+ }
79
+ if (selectedRoot === mirrorRoot && !existsSync(mirrorCliPath) && existsSync(stagedCliPath)) selectedRoot = stagedRoot
80
+ }
81
+ return {
82
+ ...invocation,
83
+ args: [join(selectedRoot, cliRelativePath), ...invocation.args.slice(1)],
84
+ cwd: selectedRoot,
85
+ cliPath: join(selectedRoot, cliRelativePath),
86
+ }
87
+ }
88
+
89
+ export function normalizeGbrainSpawnResult(args, result) {
90
+ const stdout = String(result.stdout ?? '')
91
+ const stderr = `${String(result.stderr ?? '')}${result.error ? `\n${result.error.message}` : ''}`.trim()
92
+ const exitCode = typeof result.status === 'number' ? result.status : 1
93
+ const timedOut = /ETIMEDOUT/i.test(String(result.error?.message ?? ''))
94
+ const recoveredTimeout = shouldRecoverGbrainTimeout(args, stdout, stderr, exitCode, timedOut)
95
+ return {
96
+ stdout,
97
+ stderr: recoveredTimeout ? stripRecoverableTimeoutNoise(stderr) : stderr,
98
+ exitCode: recoveredTimeout ? 0 : exitCode,
99
+ timedOut: recoveredTimeout ? false : timedOut,
100
+ recoveredTimeout,
101
+ }
102
+ }
103
+
104
+ function isMirrorFresh(mirrorRoot, sourceRoot, version, cliMtimeMs, mirrorCliPath) {
105
+ if (!existsSync(mirrorCliPath)) return false
106
+ const metadataPath = join(mirrorRoot, MIRROR_META_FILE)
107
+ if (!existsSync(metadataPath)) return false
108
+ try {
109
+ const parsed = JSON.parse(readFileSync(metadataPath, 'utf8'))
110
+ return parsed.sourceRoot === sourceRoot
111
+ && parsed.version === version
112
+ && parsed.cliMtimeMs === cliMtimeMs
113
+ } catch {
114
+ return false
115
+ }
116
+ }
117
+
118
+ function readPackageVersion(packageRoot) {
119
+ try {
120
+ const parsed = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'))
121
+ return typeof parsed.version === 'string' ? parsed.version : undefined
122
+ } catch {
123
+ return undefined
124
+ }
125
+ }
126
+
127
+ function normalizeForCompare(value) {
128
+ return String(value).replace(/\//g, '\\').toLowerCase()
129
+ }
130
+
131
+ function shouldRecoverGbrainTimeout(args, stdout, stderr, exitCode, timedOut) {
132
+ if (!timedOut || exitCode === 0) return false
133
+ const command = String(args?.[0] ?? '').trim().toLowerCase()
134
+ if (!command || !GBRAIN_TIMEOUT_RECOVERY_COMMANDS.has(command)) return false
135
+ const cleanedStderr = stripRecoverableTimeoutNoise(stderr)
136
+ if (cleanedStderr) return false
137
+ return hasRecoverableGbrainOutput(command, stdout.trim())
138
+ }
139
+
140
+ function hasRecoverableGbrainOutput(command, stdout) {
141
+ if (!stdout) return false
142
+ switch (command) {
143
+ case '--version':
144
+ case 'version':
145
+ return /\bgbrain\b/i.test(stdout) || /\d+\.\d+\.\d+/.test(stdout)
146
+ case 'doctor':
147
+ return looksLikeJsonOutput(stdout)
148
+ case 'list':
149
+ return /no pages found/i.test(stdout)
150
+ || /^\d+\.\s+\S+/m.test(stdout)
151
+ || /^\[\d+(?:\.\d+)?\]\s+/m.test(stdout)
152
+ case 'get':
153
+ return /^---$/m.test(stdout)
154
+ || /^#\s+\S+/m.test(stdout)
155
+ || looksLikeJsonOutput(stdout)
156
+ case 'query':
157
+ case 'search':
158
+ return looksLikeJsonOutput(stdout)
159
+ || /no (pages|results) found/i.test(stdout)
160
+ || /^\[\d+(?:\.\d+)?\]\s+/m.test(stdout)
161
+ || /^\d+\.\s+\S+/m.test(stdout)
162
+ default:
163
+ return false
164
+ }
165
+ }
166
+
167
+ function looksLikeJsonOutput(stdout) {
168
+ const trimmed = String(stdout ?? '').trim()
169
+ if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('['))) return false
170
+ try {
171
+ JSON.parse(trimmed)
172
+ return true
173
+ } catch {
174
+ return false
175
+ }
176
+ }
177
+
178
+ function stripRecoverableTimeoutNoise(stderr) {
179
+ return String(stderr ?? '')
180
+ .replace(/\n?spawnSync .*?\bETIMEDOUT\b\s*/gim, '\n')
181
+ .replace(/\n?Command failed: .*?\bETIMEDOUT\b\s*/gim, '\n')
182
+ .replace(/\n?timed out after .*$/gim, '\n')
183
+ .replace(/\n{3,}/g, '\n\n')
184
+ .trim()
185
+ }
@@ -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
+ }