@jennie-shawn/starwork 0.1.0-alpha.3 → 0.1.0-alpha.5
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/cli/README.md +1 -1
- package/cli/doctor-spec.md +70 -4
- package/cli/src/cli.js +371 -6
- package/cli/test/init.test.js +60 -0
- package/docs/README.md +1 -0
- package/docs/doctor-capabilities.html +626 -0
- package/docs/index.html +1 -0
- package/docs/roadmap.html +3 -3
- package/docs/roadmap.md +6 -6
- package/docs/v0.1-plan.md +1 -1
- package/package.json +1 -1
- package/skills/README.md +1 -0
- package/skills/starworkDoctor/SKILL.md +172 -0
- package/skills/starworkDoctor/agents/openai.yaml +7 -0
- package/skills/starworkDoctor-spec.md +291 -0
package/cli/README.md
CHANGED
|
@@ -25,7 +25,7 @@ v0.1 只覆盖最小可用安装和适配能力:
|
|
|
25
25
|
|
|
26
26
|
- `starwork init` 第一版:可以初始化轻量单项目、长期单项目和多项目管理中枢,并通过 Pack 语言配置组装通用工作、内容创作者和中枢管理场景。
|
|
27
27
|
- `starwork spawn` 第一版:可以从健康 Hub 生成 `satellite-starter` / `satellite-matter` 项目工作台,支持 `--blueprint` 定制目录、路径、规则和 seed,并回写 Hub 项目注册表。
|
|
28
|
-
- `starwork doctor` 第一版:可以检查 workspace state、Core 必需角色、Kit 文件、正式事实源、业务工作区和 Pack 落地结果,并支持 `--json`
|
|
28
|
+
- `starwork doctor` 第一版:可以检查 workspace state、Core 必需角色、Kit 文件、正式事实源、业务工作区和 Pack 落地结果,并支持 `--json` 输出;alpha.4 开始可识别历史模板并给出升级建议;alpha.5 开始输出目录 `inventory` 与语义 `signals`,供 `starworkDoctor` skill 判断。
|
|
29
29
|
- `starwork adapt` 第一版:可以为 Codex、Claude Code、Cursor、Trae 生成或登记轻量适配入口。
|
|
30
30
|
- `starwork pack install` 第一版:可以在健康工作台上补装 Pack,并更新路径、规则、模板和 workspace state。
|
|
31
31
|
|
package/cli/doctor-spec.md
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
- 所属模块:StarWork CLI
|
|
7
7
|
- 命令:`starwork doctor`
|
|
8
8
|
- 前置状态:Core v0.1 已封版,`starwork init` 第一版已落地
|
|
9
|
-
- 实现状态:v0.1
|
|
10
|
-
- 目标:检查一个 StarWork
|
|
9
|
+
- 实现状态:v0.1 最小实现已落地;历史模板升级诊断、目录 inventory 和 signals 已进入 alpha
|
|
10
|
+
- 目标:检查一个 StarWork 工作台是否健康,并把结构事实、风险和候选信号暴露出来;对历史模板用户只输出可供 AI 判断的诊断信号,不直接生成行动建议
|
|
11
11
|
|
|
12
12
|
## 一句话定义
|
|
13
13
|
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
它不负责创建工作台,不负责升级工作台,也不默认修复文件。它只做一件事:把 Core、Kit、Pack 和 Agent 适配文件之间的不一致检查出来,用人能看懂的方式报告。
|
|
17
17
|
|
|
18
|
+
从 alpha.4 起,`doctor` 还承担一个轻量探测入口职责:当目录不是标准 StarWork 工作台,但看起来像用户正在使用的历史模板时,它不会只说“不是工作台”,而是输出历史模板识别结果、目录 inventory 和 semantic signals,交给 `starworkDoctor` 做后续判断。
|
|
19
|
+
|
|
18
20
|
## 为什么先做 doctor
|
|
19
21
|
|
|
20
22
|
Core v0.1 已封版后,项目进入 M2:CLI v0.1 最小闭环。
|
|
@@ -90,6 +92,8 @@ starwork doctor --verbose
|
|
|
90
92
|
| `--json` | 输出机器可读 JSON。 |
|
|
91
93
|
| `--strict` | 将部分 warning 视为失败,适合测试和发布前检查。 |
|
|
92
94
|
| `--verbose` | 显示通过项、检查来源和路径解析细节。 |
|
|
95
|
+
| `--inventory-depth <number\|all>` | 控制目录结构探测深度,默认用于保护过大的工作区。 |
|
|
96
|
+
| `--inventory-limit <number>` | 控制最多输出多少个目录和文件条目。 |
|
|
93
97
|
| `--help` | 显示帮助。 |
|
|
94
98
|
|
|
95
99
|
v0.1 暂不提供 `--fix`。修复动作后续可以单独设计为 `starwork doctor --fix` 或 `starwork repair`,但第一版不要混入。
|
|
@@ -142,7 +146,7 @@ v0.1 暂不提供 `--fix`。修复动作后续可以单独设计为 `starwork do
|
|
|
142
146
|
结果:
|
|
143
147
|
|
|
144
148
|
- 找到:使用该目录作为工作区根目录。
|
|
145
|
-
- 未找到但当前目录有 StarWork
|
|
149
|
+
- 未找到但当前目录有 StarWork 或历史模板痕迹:报告缺少 workspace state,同时输出历史模板候选信号。
|
|
146
150
|
- 未找到且无 StarWork 痕迹:报告不是 StarWork 工作台。
|
|
147
151
|
|
|
148
152
|
v0.1 判断标准:
|
|
@@ -150,9 +154,41 @@ v0.1 判断标准:
|
|
|
150
154
|
| 情况 | 结果 |
|
|
151
155
|
|---|---|
|
|
152
156
|
| 找到 `.starwork/workspace.json` | 继续检查。 |
|
|
153
|
-
| 未找到 state,但有 `AGENTS.md` 和 `_系统/` / `_system/` | `fail`:缺少 workspace state
|
|
157
|
+
| 未找到 state,但有 `AGENTS.md` 和 `_系统/` / `_system/` | `fail`:缺少 workspace state,并输出候选信号。 |
|
|
158
|
+
| 未找到 state,但有 `references/outputs` 或 `参考资料/输出` | `fail`:识别为历史模板升级候选。 |
|
|
154
159
|
| 完全没有 StarWork 痕迹 | `fail`:不是 StarWork 工作台。 |
|
|
155
160
|
|
|
161
|
+
### Step 1.5:历史模板升级诊断
|
|
162
|
+
|
|
163
|
+
历史模板诊断只读文件结构,不移动、不复制、不写入任何文件。
|
|
164
|
+
|
|
165
|
+
识别信号:
|
|
166
|
+
|
|
167
|
+
| 信号 | 示例 |
|
|
168
|
+
|---|---|
|
|
169
|
+
| Agent 入口规则 | `AGENTS.md`、`CLAUDE.md`、`.cursorrules` |
|
|
170
|
+
| 系统目录 | `_系统/`、`_system/` |
|
|
171
|
+
| 事项目录 | `事项/`、`matters/` |
|
|
172
|
+
| 参考资料目录 | `参考资料/`、`资料/`、`素材/`、`references/`、`reference/` |
|
|
173
|
+
| 输出目录 | `输出/`、`成果/`、`outputs/`、`output/` |
|
|
174
|
+
| 身份和教训 | `identity/`、`lessons/`、`_系统/身份/`、`_system/identity/` |
|
|
175
|
+
|
|
176
|
+
推断规则:
|
|
177
|
+
|
|
178
|
+
| 推断项 | 规则 |
|
|
179
|
+
|---|---|
|
|
180
|
+
| `language` | 中文路径多则 `zh`,英文路径多则 `en`,不确定时默认为 `zh`。 |
|
|
181
|
+
| `workspace_type` | 存在 `事项/` 或 `matters/` 时推断为 `single-matter`,否则推断为 `single-light`。 |
|
|
182
|
+
输出内容:
|
|
183
|
+
|
|
184
|
+
- `upgrade.candidate: true`
|
|
185
|
+
- `upgrade.source: legacy-template`
|
|
186
|
+
- `upgrade.inferred.language`
|
|
187
|
+
- `upgrade.inferred.workspace_type`
|
|
188
|
+
- 检测到的参考资料目录和输出目录
|
|
189
|
+
|
|
190
|
+
注意:`doctor --json` 不输出 `next_steps`,也不输出 Pack 建议,避免影响 `starworkDoctor` 基于上下文做独立判断。在没有正式 `starwork upgrade` 命令前,`doctor` 只提供事实和信号,不执行迁移。
|
|
191
|
+
|
|
156
192
|
### Step 2:读取 workspace state
|
|
157
193
|
|
|
158
194
|
读取 `.starwork/workspace.json`。
|
|
@@ -388,6 +424,7 @@ Result:
|
|
|
388
424
|
"language": "zh",
|
|
389
425
|
"packs": ["content-creator"]
|
|
390
426
|
},
|
|
427
|
+
"upgrade": null,
|
|
391
428
|
"summary": {
|
|
392
429
|
"pass": 18,
|
|
393
430
|
"info": 1,
|
|
@@ -405,6 +442,29 @@ Result:
|
|
|
405
442
|
}
|
|
406
443
|
```
|
|
407
444
|
|
|
445
|
+
历史模板升级候选的 JSON 示例:
|
|
446
|
+
|
|
447
|
+
```json
|
|
448
|
+
{
|
|
449
|
+
"schema": "starwork.doctor.result.v0.1",
|
|
450
|
+
"ok": false,
|
|
451
|
+
"strict_ok": false,
|
|
452
|
+
"workspace_root": null,
|
|
453
|
+
"workspace": null,
|
|
454
|
+
"upgrade": {
|
|
455
|
+
"candidate": true,
|
|
456
|
+
"source": "legacy-template",
|
|
457
|
+
"confidence": "high",
|
|
458
|
+
"inferred": {
|
|
459
|
+
"language": "zh",
|
|
460
|
+
"workspace_type": "single-matter",
|
|
461
|
+
"references": ["参考资料"],
|
|
462
|
+
"outputs": ["输出"]
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
408
468
|
## 检查 ID 命名
|
|
409
469
|
|
|
410
470
|
检查 ID 应稳定,方便测试、文档和后续自动化使用。
|
|
@@ -419,6 +479,7 @@ capability.*
|
|
|
419
479
|
pack.*
|
|
420
480
|
content.*
|
|
421
481
|
adapter.*
|
|
482
|
+
legacy.*
|
|
422
483
|
```
|
|
423
484
|
|
|
424
485
|
示例:
|
|
@@ -435,6 +496,9 @@ adapter.*
|
|
|
435
496
|
- `pack.paths.exist`
|
|
436
497
|
- `pack.templates.installed`
|
|
437
498
|
- `content.current_work.too_long`
|
|
499
|
+
- `legacy.template.detected`
|
|
500
|
+
- `legacy.references.detected`
|
|
501
|
+
- `legacy.outputs.detected`
|
|
438
502
|
|
|
439
503
|
## 与其他命令的关系
|
|
440
504
|
|
|
@@ -503,6 +567,8 @@ adapter.*
|
|
|
503
567
|
- 删除正式事实源目录后返回 fail。
|
|
504
568
|
- 删除 Pack seed 文件后返回 fail。
|
|
505
569
|
- 非 StarWork 目录返回 fail,并提示先运行 `starwork init`。
|
|
570
|
+
- 对存在 `references/outputs` 的英文历史模板返回 fail,但输出 `upgrade` 候选信号,不输出 `next_steps` 或 Pack 建议。
|
|
571
|
+
- 对存在 `参考资料/输出/事项` 的中文历史模板返回 fail,但推断为 `single-matter` + `zh`。
|
|
506
572
|
- `--json` 输出稳定结构。
|
|
507
573
|
- 没有 fail 时退出码为 `0`,有 fail 时退出码为 `1`。
|
|
508
574
|
|
package/cli/src/cli.js
CHANGED
|
@@ -241,6 +241,10 @@ function parseArgs(argv) {
|
|
|
241
241
|
options.strict = true;
|
|
242
242
|
} else if (arg === "--verbose") {
|
|
243
243
|
options.verbose = true;
|
|
244
|
+
} else if (arg === "--inventory-depth") {
|
|
245
|
+
options.inventoryDepth = readValue(argv, ++i, arg);
|
|
246
|
+
} else if (arg === "--inventory-limit") {
|
|
247
|
+
options.inventoryLimit = readValue(argv, ++i, arg);
|
|
244
248
|
} else if (arg === "--agent") {
|
|
245
249
|
options.agent = readValue(argv, ++i, arg);
|
|
246
250
|
} else if (arg === "--hub") {
|
|
@@ -379,13 +383,23 @@ function doctor(argv) {
|
|
|
379
383
|
return finishDoctor(result, options);
|
|
380
384
|
}
|
|
381
385
|
|
|
386
|
+
result.inventory = collectInventory(targetDir, options);
|
|
387
|
+
result.signals = detectWorkspaceSignals(result.inventory);
|
|
388
|
+
|
|
382
389
|
const workspaceRoot = findWorkspaceRoot(targetDir);
|
|
383
390
|
if (!workspaceRoot) {
|
|
384
|
-
const
|
|
385
|
-
if (
|
|
386
|
-
|
|
391
|
+
const legacy = detectLegacyWorkspace(targetDir, result.signals);
|
|
392
|
+
if (legacy.candidate) {
|
|
393
|
+
result.upgrade = buildLegacySignals(legacy);
|
|
394
|
+
addCheck(result, "workspace.state.exists", "fail", "这是一个可升级的历史模板工作区,但缺少 .starwork/workspace.json。", legacy.primaryTrace);
|
|
395
|
+
addLegacyChecks(result, legacy);
|
|
387
396
|
} else {
|
|
388
|
-
|
|
397
|
+
const trace = findStarWorkTrace(targetDir);
|
|
398
|
+
if (trace) {
|
|
399
|
+
addCheck(result, "workspace.state.exists", "fail", "疑似 StarWork 工作台,但缺少 .starwork/workspace.json。", trace);
|
|
400
|
+
} else {
|
|
401
|
+
addCheck(result, "workspace.state.exists", "fail", "当前目录不是 StarWork 工作台。请先运行 starwork init。");
|
|
402
|
+
}
|
|
389
403
|
}
|
|
390
404
|
return finishDoctor(result, options);
|
|
391
405
|
}
|
|
@@ -461,6 +475,9 @@ function createDoctorResult(targetDir) {
|
|
|
461
475
|
workspace_root: null,
|
|
462
476
|
target: targetDir,
|
|
463
477
|
workspace: null,
|
|
478
|
+
upgrade: null,
|
|
479
|
+
inventory: null,
|
|
480
|
+
signals: null,
|
|
464
481
|
summary: {
|
|
465
482
|
pass: 0,
|
|
466
483
|
info: 0,
|
|
@@ -866,14 +883,352 @@ function normalizeSafeSourcePath(relativePath, sourceRoot, label) {
|
|
|
866
883
|
function findStarWorkTrace(dir) {
|
|
867
884
|
const traces = [
|
|
868
885
|
"AGENTS.md",
|
|
886
|
+
"CLAUDE.md",
|
|
869
887
|
"_系统",
|
|
870
888
|
"_system",
|
|
871
889
|
"事项",
|
|
872
|
-
"matters"
|
|
890
|
+
"matters",
|
|
891
|
+
"参考资料",
|
|
892
|
+
"references",
|
|
893
|
+
"输出",
|
|
894
|
+
"outputs"
|
|
873
895
|
];
|
|
874
896
|
return traces.find((trace) => fs.existsSync(path.join(dir, trace))) || null;
|
|
875
897
|
}
|
|
876
898
|
|
|
899
|
+
function collectInventory(root, options = {}) {
|
|
900
|
+
const maxDepth = parseInventoryDepth(options.inventoryDepth);
|
|
901
|
+
const maxEntries = parseInventoryLimit(options.inventoryLimit);
|
|
902
|
+
const directories = [];
|
|
903
|
+
const files = [];
|
|
904
|
+
const omitted = {
|
|
905
|
+
directories: 0,
|
|
906
|
+
files: 0,
|
|
907
|
+
reason: null
|
|
908
|
+
};
|
|
909
|
+
let totalEntries = 0;
|
|
910
|
+
|
|
911
|
+
function walk(current, depth) {
|
|
912
|
+
if (totalEntries >= maxEntries) {
|
|
913
|
+
omitted.reason = "count_limit";
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
let entries;
|
|
918
|
+
try {
|
|
919
|
+
entries = fs.readdirSync(current, { withFileTypes: true })
|
|
920
|
+
.filter((entry) => !shouldOmitInventoryEntry(entry.name))
|
|
921
|
+
.sort((a, b) => a.name.localeCompare(b.name, "zh-Hans-CN"));
|
|
922
|
+
} catch {
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
for (const entry of entries) {
|
|
927
|
+
if (totalEntries >= maxEntries) {
|
|
928
|
+
if (entry.isDirectory()) omitted.directories += 1;
|
|
929
|
+
else if (entry.isFile()) omitted.files += 1;
|
|
930
|
+
omitted.reason = "count_limit";
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const absolute = path.join(current, entry.name);
|
|
935
|
+
const relativePath = normalizeRelativePath(path.relative(root, absolute));
|
|
936
|
+
if (!relativePath) continue;
|
|
937
|
+
|
|
938
|
+
if (entry.isDirectory()) {
|
|
939
|
+
const childCount = safeChildCount(absolute);
|
|
940
|
+
directories.push({
|
|
941
|
+
path: relativePath,
|
|
942
|
+
depth: depth + 1,
|
|
943
|
+
children_count: childCount
|
|
944
|
+
});
|
|
945
|
+
totalEntries += 1;
|
|
946
|
+
if (depth + 1 < maxDepth) {
|
|
947
|
+
walk(absolute, depth + 1);
|
|
948
|
+
} else if (childCount > 0) {
|
|
949
|
+
omitted.directories += 1;
|
|
950
|
+
omitted.reason = omitted.reason || "depth_limit";
|
|
951
|
+
}
|
|
952
|
+
} else if (entry.isFile()) {
|
|
953
|
+
files.push({
|
|
954
|
+
path: relativePath,
|
|
955
|
+
depth: depth + 1,
|
|
956
|
+
size: safeFileSize(absolute)
|
|
957
|
+
});
|
|
958
|
+
totalEntries += 1;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
walk(root, 0);
|
|
964
|
+
|
|
965
|
+
return {
|
|
966
|
+
root,
|
|
967
|
+
max_depth: Number.isFinite(maxDepth) ? maxDepth : "all",
|
|
968
|
+
max_entries: maxEntries,
|
|
969
|
+
directories,
|
|
970
|
+
files,
|
|
971
|
+
omitted
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function parseInventoryDepth(value) {
|
|
976
|
+
if (!value) return 8;
|
|
977
|
+
if (String(value).toLowerCase() === "all") return Infinity;
|
|
978
|
+
const parsed = Number(value);
|
|
979
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
980
|
+
throw new Error("--inventory-depth 必须是正整数或 all。");
|
|
981
|
+
}
|
|
982
|
+
return parsed;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function parseInventoryLimit(value) {
|
|
986
|
+
if (!value) return 5000;
|
|
987
|
+
const parsed = Number(value);
|
|
988
|
+
if (!Number.isInteger(parsed) || parsed < 100) {
|
|
989
|
+
throw new Error("--inventory-limit 必须是大于等于 100 的整数。");
|
|
990
|
+
}
|
|
991
|
+
return parsed;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function shouldOmitInventoryEntry(name) {
|
|
995
|
+
return [
|
|
996
|
+
".git",
|
|
997
|
+
"node_modules",
|
|
998
|
+
".DS_Store",
|
|
999
|
+
".cache",
|
|
1000
|
+
".next",
|
|
1001
|
+
"dist",
|
|
1002
|
+
"build",
|
|
1003
|
+
"coverage"
|
|
1004
|
+
].includes(name);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function safeChildCount(dir) {
|
|
1008
|
+
try {
|
|
1009
|
+
return fs.readdirSync(dir).filter((name) => !shouldOmitInventoryEntry(name)).length;
|
|
1010
|
+
} catch {
|
|
1011
|
+
return 0;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function safeFileSize(file) {
|
|
1016
|
+
try {
|
|
1017
|
+
return fs.statSync(file).size;
|
|
1018
|
+
} catch {
|
|
1019
|
+
return 0;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function detectWorkspaceSignals(inventory) {
|
|
1024
|
+
const directories = inventory?.directories || [];
|
|
1025
|
+
const files = inventory?.files || [];
|
|
1026
|
+
return {
|
|
1027
|
+
agent_entry: files.filter((file) => isAgentEntryFile(file.path)).map((file) => file.path),
|
|
1028
|
+
system_dirs: directories.filter((dir) => isSystemDirectory(dir.path)).map((dir) => dir.path),
|
|
1029
|
+
matter_dirs: directories.filter((dir) => isMatterDirectory(dir.path)).map((dir) => dir.path),
|
|
1030
|
+
possible_reference_dirs: directories.filter((dir) => isPossibleReferenceDirectory(dir.path)).map((dir) => dir.path),
|
|
1031
|
+
possible_output_dirs: directories.filter((dir) => isPossibleOutputDirectory(dir.path)).map((dir) => dir.path),
|
|
1032
|
+
possible_draft_dirs: directories.filter((dir) => isPossibleDraftDirectory(dir.path)).map((dir) => dir.path),
|
|
1033
|
+
possible_current_work_dirs: directories.filter((dir) => isPossibleCurrentWorkDirectory(dir.path)).map((dir) => dir.path),
|
|
1034
|
+
identity_dirs: directories.filter((dir) => isIdentityDirectory(dir.path)).map((dir) => dir.path),
|
|
1035
|
+
lessons_dirs: directories.filter((dir) => isLessonsDirectory(dir.path)).map((dir) => dir.path),
|
|
1036
|
+
readme_files: files.filter((file) => /^README(\.[a-z0-9]+)?$/i.test(path.basename(file.path))).map((file) => file.path)
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function isAgentEntryFile(relativePath) {
|
|
1041
|
+
return ["AGENTS.md", "CLAUDE.md", ".cursorrules"].includes(relativePath)
|
|
1042
|
+
|| relativePath.endsWith("/AGENTS.md")
|
|
1043
|
+
|| relativePath.endsWith("/CLAUDE.md");
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function isSystemDirectory(relativePath) {
|
|
1047
|
+
const base = basenameLower(relativePath);
|
|
1048
|
+
return ["_系统", "_system", "system"].includes(base);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function isMatterDirectory(relativePath) {
|
|
1052
|
+
const base = basenameLower(relativePath);
|
|
1053
|
+
return includesAny(base, ["事项", "matter", "matters"]);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function isPossibleReferenceDirectory(relativePath) {
|
|
1057
|
+
const base = basenameLower(relativePath);
|
|
1058
|
+
return includesAny(base, ["参考", "资料", "素材", "知识", "reference", "references", "ref", "source", "material", "materials", "knowledge"]);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function isPossibleOutputDirectory(relativePath) {
|
|
1062
|
+
const base = basenameLower(relativePath);
|
|
1063
|
+
return includesAny(base, ["输出", "成果", "成稿", "终稿", "交付", "发布", "确认", "output", "outputs", "final", "deliverable", "deliverables", "published", "release"]);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function isPossibleDraftDirectory(relativePath) {
|
|
1067
|
+
const base = basenameLower(relativePath);
|
|
1068
|
+
return includesAny(base, ["草稿", "初稿", "脚本", "draft", "drafts", "script", "scripts"]);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function isPossibleCurrentWorkDirectory(relativePath) {
|
|
1072
|
+
const base = basenameLower(relativePath);
|
|
1073
|
+
return includesAny(base, ["推进", "当前", "任务", "工作台", "work", "working", "tasks", "todo"]);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function isIdentityDirectory(relativePath) {
|
|
1077
|
+
const base = basenameLower(relativePath);
|
|
1078
|
+
return includesAny(base, ["身份", "identity", "profile", "persona"]);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function isLessonsDirectory(relativePath) {
|
|
1082
|
+
const base = basenameLower(relativePath);
|
|
1083
|
+
return includesAny(base, ["教训", "经验", "复盘", "lessons", "learning", "retrospective"]);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function basenameLower(relativePath) {
|
|
1087
|
+
return path.basename(relativePath).toLowerCase();
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function includesAny(value, needles) {
|
|
1091
|
+
return needles.some((needle) => value.includes(needle));
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function detectLegacyWorkspace(dir, signals = null) {
|
|
1095
|
+
const groups = {
|
|
1096
|
+
entryRules: ["AGENTS.md", "CLAUDE.md", ".cursorrules"],
|
|
1097
|
+
system: ["_系统", "_system", "system"],
|
|
1098
|
+
matters: ["事项", "matters"],
|
|
1099
|
+
referencesZh: ["参考资料", "资料", "资料库", "素材", "素材库", "知识"],
|
|
1100
|
+
referencesEn: ["references", "reference"],
|
|
1101
|
+
outputsZh: ["输出", "成果", "成稿", "终稿", "交付物", "发布记录"],
|
|
1102
|
+
outputsEn: ["outputs", "output"],
|
|
1103
|
+
identityRoot: ["identity"],
|
|
1104
|
+
lessonsRoot: ["lessons"],
|
|
1105
|
+
identitySystemZh: ["_系统/身份"],
|
|
1106
|
+
lessonsSystemZh: ["_系统/教训"],
|
|
1107
|
+
identitySystemEn: ["_system/identity"],
|
|
1108
|
+
lessonsSystemEn: ["_system/lessons"]
|
|
1109
|
+
};
|
|
1110
|
+
const found = {};
|
|
1111
|
+
for (const [key, candidates] of Object.entries(groups)) {
|
|
1112
|
+
found[key] = existingRelativePaths(dir, candidates);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
const references = [...found.referencesZh, ...found.referencesEn];
|
|
1116
|
+
const outputs = [...found.outputsZh, ...found.outputsEn];
|
|
1117
|
+
const signalReferences = signals?.possible_reference_dirs || [];
|
|
1118
|
+
const signalOutputs = signals?.possible_output_dirs || [];
|
|
1119
|
+
const signalMatters = signals?.matter_dirs || [];
|
|
1120
|
+
const signalEntries = signals?.agent_entry || [];
|
|
1121
|
+
const signalSystems = signals?.system_dirs || [];
|
|
1122
|
+
const signalIdentity = signals?.identity_dirs || [];
|
|
1123
|
+
const signalLessons = signals?.lessons_dirs || [];
|
|
1124
|
+
const hasSystem = found.system.length > 0;
|
|
1125
|
+
const hasEntry = found.entryRules.length > 0;
|
|
1126
|
+
const hasMatters = found.matters.length > 0;
|
|
1127
|
+
const hasIdentityOrLessons = [
|
|
1128
|
+
...found.identityRoot,
|
|
1129
|
+
...found.lessonsRoot,
|
|
1130
|
+
...found.identitySystemZh,
|
|
1131
|
+
...found.lessonsSystemZh,
|
|
1132
|
+
...found.identitySystemEn,
|
|
1133
|
+
...found.lessonsSystemEn
|
|
1134
|
+
].length > 0;
|
|
1135
|
+
|
|
1136
|
+
const signalCount = [
|
|
1137
|
+
hasEntry || signalEntries.length > 0,
|
|
1138
|
+
hasSystem || signalSystems.length > 0,
|
|
1139
|
+
hasMatters || signalMatters.length > 0,
|
|
1140
|
+
references.length > 0 || signalReferences.length > 0,
|
|
1141
|
+
outputs.length > 0 || signalOutputs.length > 0,
|
|
1142
|
+
hasIdentityOrLessons || signalIdentity.length > 0 || signalLessons.length > 0
|
|
1143
|
+
].filter(Boolean).length;
|
|
1144
|
+
const candidate = signalCount >= 2 || ((references.length > 0 || signalReferences.length > 0) && (outputs.length > 0 || signalOutputs.length > 0));
|
|
1145
|
+
const language = inferLegacyLanguage(found);
|
|
1146
|
+
const workspaceType = hasMatters || signalMatters.length > 0 ? "single-matter" : "single-light";
|
|
1147
|
+
const primaryTrace = [
|
|
1148
|
+
...found.entryRules,
|
|
1149
|
+
...signalEntries,
|
|
1150
|
+
...found.system,
|
|
1151
|
+
...signalSystems,
|
|
1152
|
+
...found.matters,
|
|
1153
|
+
...signalMatters,
|
|
1154
|
+
...references,
|
|
1155
|
+
...signalReferences,
|
|
1156
|
+
...outputs,
|
|
1157
|
+
...signalOutputs
|
|
1158
|
+
][0] || null;
|
|
1159
|
+
|
|
1160
|
+
return {
|
|
1161
|
+
candidate,
|
|
1162
|
+
confidence: signalCount >= 4 ? "high" : "medium",
|
|
1163
|
+
language,
|
|
1164
|
+
workspaceType,
|
|
1165
|
+
primaryTrace,
|
|
1166
|
+
found,
|
|
1167
|
+
references: uniqueList([...references, ...signalReferences]),
|
|
1168
|
+
outputs: uniqueList([...outputs, ...signalOutputs])
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function uniqueList(items) {
|
|
1173
|
+
return [...new Set(items.filter(Boolean))];
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function existingRelativePaths(root, candidates) {
|
|
1177
|
+
return candidates.filter((relativePath) => fs.existsSync(path.join(root, relativePath)));
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function inferLegacyLanguage(found) {
|
|
1181
|
+
const zhScore = Number(found.system.includes("_系统"))
|
|
1182
|
+
+ Number(found.matters.includes("事项"))
|
|
1183
|
+
+ found.referencesZh.length
|
|
1184
|
+
+ found.outputsZh.length
|
|
1185
|
+
+ found.identitySystemZh.length
|
|
1186
|
+
+ found.lessonsSystemZh.length;
|
|
1187
|
+
const enScore = Number(found.system.includes("_system"))
|
|
1188
|
+
+ Number(found.matters.includes("matters"))
|
|
1189
|
+
+ found.referencesEn.length
|
|
1190
|
+
+ found.outputsEn.length
|
|
1191
|
+
+ found.identitySystemEn.length
|
|
1192
|
+
+ found.lessonsSystemEn.length;
|
|
1193
|
+
return enScore > zhScore ? "en" : "zh";
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function buildLegacySignals(legacy) {
|
|
1197
|
+
return {
|
|
1198
|
+
candidate: true,
|
|
1199
|
+
source: "legacy-template",
|
|
1200
|
+
confidence: legacy.confidence,
|
|
1201
|
+
inferred: {
|
|
1202
|
+
language: legacy.language,
|
|
1203
|
+
workspace_type: legacy.workspaceType,
|
|
1204
|
+
references: legacy.references,
|
|
1205
|
+
outputs: legacy.outputs
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function addLegacyChecks(result, legacy) {
|
|
1211
|
+
addCheck(result, "legacy.template.detected", "info", `检测到历史模板升级候选,置信度:${legacy.confidence}。`, legacy.primaryTrace);
|
|
1212
|
+
addCheck(result, "legacy.language.inferred", "info", `推测语言:${legacy.language}。`);
|
|
1213
|
+
addCheck(result, "legacy.workspace_type.inferred", "info", `推测工作区类型:${legacy.workspaceType}。`);
|
|
1214
|
+
|
|
1215
|
+
if (legacy.references.length) {
|
|
1216
|
+
addCheck(result, "legacy.references.detected", "info", `检测到参考资料目录:${legacy.references.join(", ")}。`, legacy.references[0]);
|
|
1217
|
+
} else {
|
|
1218
|
+
addCheck(result, "legacy.references.detected", "warn", "未检测到常见参考资料目录,升级时可能需要手动指定资料区。");
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
if (legacy.outputs.length) {
|
|
1222
|
+
addCheck(result, "legacy.outputs.detected", "info", `检测到输出目录:${legacy.outputs.join(", ")}。`, legacy.outputs[0]);
|
|
1223
|
+
} else {
|
|
1224
|
+
addCheck(result, "legacy.outputs.detected", "warn", "未检测到常见输出目录,升级时可能需要手动指定成果区。");
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
if (!legacy.found.entryRules.length) {
|
|
1228
|
+
addCheck(result, "legacy.entry_rules.detected", "warn", "未检测到 AGENTS.md、CLAUDE.md 或 Cursor 规则文件,升级后需要补齐 Agent 入口规则。");
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
877
1232
|
function addCheck(result, id, level, message, checkPath) {
|
|
878
1233
|
result.summary[level] += 1;
|
|
879
1234
|
result.checks.push({
|
|
@@ -897,7 +1252,7 @@ function finishDoctor(result, options) {
|
|
|
897
1252
|
}
|
|
898
1253
|
|
|
899
1254
|
function doctorPublicResult(result) {
|
|
900
|
-
const { exitCode,
|
|
1255
|
+
const { exitCode, ...publicResult } = result;
|
|
901
1256
|
return publicResult;
|
|
902
1257
|
}
|
|
903
1258
|
|
|
@@ -934,6 +1289,14 @@ function printDoctorResult(result, options) {
|
|
|
934
1289
|
}
|
|
935
1290
|
}
|
|
936
1291
|
|
|
1292
|
+
if (result.upgrade?.candidate) {
|
|
1293
|
+
console.log("Legacy signals:");
|
|
1294
|
+
console.log(` 检测为:历史模板候选(${result.upgrade.confidence} confidence)`);
|
|
1295
|
+
console.log(` 推测类型:${result.upgrade.inferred.workspace_type}`);
|
|
1296
|
+
console.log(` 推测语言:${result.upgrade.inferred.language}`);
|
|
1297
|
+
console.log("");
|
|
1298
|
+
}
|
|
1299
|
+
|
|
937
1300
|
console.log("Result:");
|
|
938
1301
|
if (result.summary.fail > 0) {
|
|
939
1302
|
console.log(" Workspace has blocking issues.");
|
|
@@ -1914,6 +2277,8 @@ Options:
|
|
|
1914
2277
|
--json
|
|
1915
2278
|
--strict
|
|
1916
2279
|
--verbose
|
|
2280
|
+
--inventory-depth <number|all>
|
|
2281
|
+
--inventory-limit <number>
|
|
1917
2282
|
`);
|
|
1918
2283
|
}
|
|
1919
2284
|
|
package/cli/test/init.test.js
CHANGED
|
@@ -372,6 +372,66 @@ test("doctor fails outside a StarWork workspace", () => {
|
|
|
372
372
|
assert.match(result.stdout, /不是 StarWork 工作台/);
|
|
373
373
|
});
|
|
374
374
|
|
|
375
|
+
test("doctor reports legacy signals for an English legacy template", () => {
|
|
376
|
+
const dir = tempDir();
|
|
377
|
+
fs.writeFileSync(path.join(dir, "AGENTS.md"), "# Legacy Agent Rules\n", "utf8");
|
|
378
|
+
fs.mkdirSync(path.join(dir, "references"), { recursive: true });
|
|
379
|
+
fs.mkdirSync(path.join(dir, "outputs", "drafts"), { recursive: true });
|
|
380
|
+
fs.mkdirSync(path.join(dir, "outputs", "final"), { recursive: true });
|
|
381
|
+
|
|
382
|
+
const result = runDoctor(["--target", dir, "--json"]);
|
|
383
|
+
const report = JSON.parse(result.stdout);
|
|
384
|
+
|
|
385
|
+
assert.equal(result.status, 1);
|
|
386
|
+
assert.equal(report.upgrade.candidate, true);
|
|
387
|
+
assert.equal(report.upgrade.source, "legacy-template");
|
|
388
|
+
assert.equal(report.upgrade.inferred.language, "en");
|
|
389
|
+
assert.equal(report.upgrade.inferred.workspace_type, "single-light");
|
|
390
|
+
assert.equal(Object.hasOwn(report.upgrade.inferred, "pack"), false);
|
|
391
|
+
assert.equal(Object.hasOwn(report.upgrade, "next_steps"), false);
|
|
392
|
+
assert.deepEqual(report.upgrade.inferred.references, ["references"]);
|
|
393
|
+
assert(report.upgrade.inferred.outputs.includes("outputs"));
|
|
394
|
+
assert(report.checks.some((check) => check.id === "legacy.references.detected" && check.level === "info"));
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("doctor exposes inventory and semantic signals for non-standard legacy folders", () => {
|
|
398
|
+
const dir = tempDir();
|
|
399
|
+
fs.writeFileSync(path.join(dir, "README.md"), "# Custom Workspace\n", "utf8");
|
|
400
|
+
fs.mkdirSync(path.join(dir, "资料库", "文章"), { recursive: true });
|
|
401
|
+
fs.mkdirSync(path.join(dir, "成稿"), { recursive: true });
|
|
402
|
+
fs.mkdirSync(path.join(dir, "推进"), { recursive: true });
|
|
403
|
+
|
|
404
|
+
const result = runDoctor(["--target", dir, "--json"]);
|
|
405
|
+
const report = JSON.parse(result.stdout);
|
|
406
|
+
|
|
407
|
+
assert.equal(result.status, 1);
|
|
408
|
+
assert.equal(report.target, path.resolve(dir));
|
|
409
|
+
assert(report.inventory.directories.some((item) => item.path === "资料库"));
|
|
410
|
+
assert(report.inventory.directories.some((item) => item.path === "成稿"));
|
|
411
|
+
assert(report.inventory.files.some((item) => item.path === "README.md"));
|
|
412
|
+
assert(report.signals.possible_reference_dirs.includes("资料库"));
|
|
413
|
+
assert(report.signals.possible_output_dirs.includes("成稿"));
|
|
414
|
+
assert(report.signals.possible_current_work_dirs.includes("推进"));
|
|
415
|
+
assert.equal(report.upgrade.candidate, true);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("doctor reports legacy signals for a Chinese matter legacy template", () => {
|
|
419
|
+
const dir = tempDir();
|
|
420
|
+
fs.mkdirSync(path.join(dir, "_系统", "身份"), { recursive: true });
|
|
421
|
+
fs.mkdirSync(path.join(dir, "事项"), { recursive: true });
|
|
422
|
+
fs.mkdirSync(path.join(dir, "参考资料"), { recursive: true });
|
|
423
|
+
fs.mkdirSync(path.join(dir, "输出", "确认成果"), { recursive: true });
|
|
424
|
+
|
|
425
|
+
const result = runDoctor(["--target", dir]);
|
|
426
|
+
|
|
427
|
+
assert.equal(result.status, 1);
|
|
428
|
+
assert.match(result.stdout, /历史模板候选/);
|
|
429
|
+
assert.match(result.stdout, /推测类型:single-matter/);
|
|
430
|
+
assert.match(result.stdout, /推测语言:zh/);
|
|
431
|
+
assert.doesNotMatch(result.stdout, /--dry-run/);
|
|
432
|
+
assert.doesNotMatch(result.stdout, /下一步/);
|
|
433
|
+
});
|
|
434
|
+
|
|
375
435
|
test("adapt creates a Claude adapter and records it in workspace state", () => {
|
|
376
436
|
const dir = tempDir();
|
|
377
437
|
runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
|
package/docs/README.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
- [index.html](index.html):StarWork HTML 文档中心,统一挂载所有 HTML 阅读稿
|
|
9
9
|
- [hub-management.html](hub-management.html):多项目中枢管理机制 HTML 阅读版,解释 Hub + Satellite、项目注册表和回写边界
|
|
10
10
|
- [cli-capabilities.html](cli-capabilities.html):StarWork CLI v0.1 能力说明 HTML 阅读版
|
|
11
|
+
- [doctor-capabilities.html](doctor-capabilities.html):StarWork Doctor 能力说明 HTML 阅读版,解释标准体检和历史模板升级诊断
|
|
11
12
|
- [product-shape-business-model.html](product-shape-business-model.html):StarWork 产品形态与商业模式 HTML 可视化阅读版
|
|
12
13
|
- [roadmap.md](roadmap.md):StarWork 从当前状态到 v0.1 发布、v0.2 扩展和 v1.0 稳定产品的里程碑
|
|
13
14
|
- [roadmap.html](roadmap.html):里程碑 HTML 阅读版,包含 SVG 总路线图和当前焦点图
|