@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.
- package/dist/api/cli.js +421 -26
- 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 +2 -0
- package/dist/bootstrap/DependencyBootstrap.js +250 -39
- package/dist/bootstrap/DependencyBootstrap.js.map +1 -1
- package/dist/bootstrap/DependencyBootstrapRenderer.js +2 -4
- package/dist/bootstrap/DependencyBootstrapRenderer.js.map +1 -1
- package/dist/capabilities/InstalledSkillsIntegration.js +29 -9
- package/dist/capabilities/InstalledSkillsIntegration.js.map +1 -1
- package/dist/context/ContextBudget.js +2 -2
- package/dist/core/GbrainRuntime.d.ts +25 -0
- package/dist/core/GbrainRuntime.js +270 -0
- package/dist/core/GbrainRuntime.js.map +1 -0
- package/dist/env/EnvironmentDoctor.js +221 -5
- package/dist/env/EnvironmentDoctor.js.map +1 -1
- package/dist/memory/MemoryProviders.js +38 -91
- package/dist/memory/MemoryProviders.js.map +1 -1
- package/dist/runtime/ModelUsageLedger.d.ts +53 -2
- package/dist/runtime/ModelUsageLedger.js +243 -39
- package/dist/runtime/ModelUsageLedger.js.map +1 -1
- package/dist/setup/SetupVerification.d.ts +42 -0
- package/dist/setup/SetupVerification.js +180 -0
- package/dist/setup/SetupVerification.js.map +1 -0
- package/dist/setup/SetupWizard.d.ts +3 -0
- package/dist/setup/SetupWizard.js +79 -19
- package/dist/setup/SetupWizard.js.map +1 -1
- package/dist/skills/SkillDoctor.js +2 -2
- package/dist/skills/SkillDoctor.js.map +1 -1
- package/dist/skills/SkillRepository.js +2 -2
- package/dist/skills/SkillRepository.js.map +1 -1
- package/dist/tools/ToolCapabilityRegistry.js +12 -2
- package/dist/tools/ToolCapabilityRegistry.js.map +1 -1
- package/dist/workflow/VerificationProfile.js +1 -1
- package/dist/workflow/VerificationProfile.js.map +1 -1
- package/docs/CONTEXT_BUDGET.md +12 -2
- package/docs/GOVERNANCE_DASHBOARD.md +7 -0
- package/docs/THIRD_PARTY_SKILLS.md +12 -4
- package/docs/start/README.md +2 -2
- package/docs/start/quickstart.md +54 -44
- package/docs/start/workflow-upgrade.md +8 -1
- package/package.json +3 -2
- package/scripts/workflow/lib/gbrain-runtime.mjs +185 -0
- package/scripts/workflow/lib/report-output.mjs +107 -0
- package/scripts/workflow/provider-rehearsal.mjs +129 -48
- package/scripts/workflow/setup-smoke.mjs +142 -8
package/docs/start/quickstart.md
CHANGED
|
@@ -32,27 +32,48 @@ cd scale-demo
|
|
|
32
32
|
scale init --governance-pack standard
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
初始化会生成 `.scale/`、`docs/`、`scripts/` 以及对应 Agent
|
|
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
|
-
|
|
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`,会先显示缺失项和修复建议,再决定是否执行 `--
|
|
85
|
+
`setup` 和 `bootstrap deps` 都会输出 `runtimeChecks`。如果机器缺少 `python`、`bun`、`cargo`、`uv/pipx`、`node/npm/npx`,会先显示缺失项和修复建议,再决定是否执行 `--apply`,避免安装中途卡住。
|
|
65
86
|
|
|
66
|
-
|
|
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
|
|
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
|
-
##
|
|
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
|
|
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
|
|
148
|
+
- `graphify-out/` 是生成产物,不应该提交到 Git;长期知识沉淀应进入经过评审的 `memory/`、`docs` 或规则文件。
|
|
120
149
|
|
|
121
|
-
##
|
|
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
|
-
##
|
|
171
|
+
## 8. MOE/多仓工作区
|
|
143
172
|
|
|
144
173
|
多仓项目使用:
|
|
145
174
|
|
|
@@ -151,31 +180,12 @@ MOE 默认把子工程配置为兄弟仓库或绝对路径,不建议把独立
|
|
|
151
180
|
|
|
152
181
|
```json
|
|
153
182
|
{
|
|
154
|
-
"
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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.
|
|
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
|
+
}
|