@haaaiawd/loom 0.7.0 → 0.9.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 +31 -19
- package/cli/bin/loom.js +89 -27
- package/cli/help/doctor.md +56 -5
- package/cli/help/preview.md +59 -0
- package/cli/help/workflow.md +18 -12
- package/cli/src/diagnostics.js +156 -12
- package/cli/src/guide.js +17 -15
- package/cli/src/philosophy.js +260 -0
- package/cli/src/preview.js +67 -10
- package/cli/src/verify.js +8 -4
- package/dimensions/PART_DECOMPOSITION.md +203 -0
- package/dimensions/examples/AGENT_SYSTEM/README.md +219 -0
- package/dimensions/examples/CLI_TOOL/README.md +163 -0
- package/dimensions/universal/COLLABORATION_PHILOSOPHY.md +77 -0
- package/dimensions/universal/ENGINEERING_CREED.md +74 -0
- package/dimensions/universal/PRODUCT_PHILOSOPHY.md +70 -0
- package/meta/PHILOSOPHY_WEAVER.md +104 -50
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -57,12 +57,14 @@ Keeper 验证(四维度:意图忠实度 / 哲学一致性 / 底线合规 /
|
|
|
57
57
|
|
|
58
58
|
### 步骤 0:诊断当前阶段
|
|
59
59
|
|
|
60
|
-
```bash
|
|
61
|
-
loom guide
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
```bash
|
|
61
|
+
loom guide
|
|
62
|
+
loom guide --dry-run # 只读诊断,不写 heartbeat
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
guide 检测项目当前在哪个阶段,输出"你在阶段 X,下一步做 Y"。
|
|
66
|
+
Agent 每完成一步都跑 guide 确认下一步。
|
|
67
|
+
如果只是审计或探测,不希望产生任何状态写入,用 `loom guide --dry-run`。
|
|
66
68
|
|
|
67
69
|
### AUTO 模式
|
|
68
70
|
|
|
@@ -145,24 +147,34 @@ loom intent status
|
|
|
145
147
|
|
|
146
148
|
### 步骤 6:人类预览
|
|
147
149
|
|
|
148
|
-
```bash
|
|
149
|
-
loom preview
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
150
|
+
```bash
|
|
151
|
+
loom preview status # 先检查 preview 是否新鲜
|
|
152
|
+
loom preview # 新鲜则打开;过期则提示重新生成
|
|
153
|
+
loom preview --regen # 输出生成提示词,让 Agent 重写 loom-preview.html
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
preview 是人类总览用的只读投影:哲学、愿景、架构、Intent 进度、验证历史。
|
|
157
|
+
`loom preview` 会用 mtime 检查 `.loom/v{N}` 是否比 `loom-preview.html` 更新:
|
|
158
|
+
- 新鲜:直接打开 `loom-preview.html`
|
|
159
|
+
- 过期:不打开旧投影,提示 `loom preview --regen`
|
|
160
|
+
- 强行打开旧投影:`loom preview --stale`
|
|
161
|
+
|
|
162
|
+
Agent 在用户说"看看进度 / 打开 preview / 看全局"时,先跑 `loom preview status`。
|
|
155
163
|
|
|
156
164
|
**CLI 命令一览**:
|
|
157
165
|
|
|
158
166
|
| 命令 | 用途 |
|
|
159
167
|
|---|---|
|
|
160
|
-
| `loom init` | 初始化项目 |
|
|
161
|
-
| `loom guide` | 诊断当前阶段,输出下一步引导 |
|
|
162
|
-
| `loom
|
|
163
|
-
| `loom
|
|
164
|
-
| `loom
|
|
165
|
-
| `loom
|
|
168
|
+
| `loom init` | 初始化项目 |
|
|
169
|
+
| `loom guide` | 诊断当前阶段,输出下一步引导 |
|
|
170
|
+
| `loom guide --dry-run` | 只读诊断当前阶段,不写 heartbeat |
|
|
171
|
+
| `loom auto on\|off\|status` | AUTO 模式开关 |
|
|
172
|
+
| `loom activate <role>` | 输出角色激活提示词 |
|
|
173
|
+
| `loom preview` | 打开新鲜 HTML 预览;过期时提示重新生成 |
|
|
174
|
+
| `loom preview status` | 检查 `loom-preview.html` 是否存在、是否新鲜 |
|
|
175
|
+
| `loom preview --regen` | 输出提示词,让 Agent 重写 HTML 预览 |
|
|
176
|
+
| `loom preview --stale` | 强行打开过期预览 |
|
|
177
|
+
| `loom help <topic>` | 分层指南(workflow\|concepts\|loop\|version\|doctor\|preview) |
|
|
166
178
|
| `loom version list` | 列出所有版本(* 标记当前) |
|
|
167
179
|
| `loom version new` | 创建新版本 + 自动切换(Major 升级) |
|
|
168
180
|
| `loom version use <v>` | 切换当前版本 |
|
package/cli/bin/loom.js
CHANGED
|
@@ -11,7 +11,7 @@ import { findLoomRoot, findVersionDir, readCurrentPointer } from '../src/shared/
|
|
|
11
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
12
|
|
|
13
13
|
import { getNextIntent, getStatus, getDependencyGraph, getIntent, loadIntentMap, updateIntentStatus, getNarrative } from '../src/intent-map.js';
|
|
14
|
-
import { getPhilosophy, listPhilosophyFiles } from '../src/philosophy.js';
|
|
14
|
+
import { getPhilosophy, listPhilosophyFiles, validateInspirationSources, validatePartDecomposition } from '../src/philosophy.js';
|
|
15
15
|
import { writeVerification, getVerificationHistory, getPendingVerifications, listVerifications, getVerificationContract } from '../src/verify.js';
|
|
16
16
|
import { initProject } from '../src/init.js';
|
|
17
17
|
import { activateRole } from '../src/activate.js';
|
|
@@ -20,7 +20,7 @@ import { doctor, contextSummary, traceIntent, reverseDep, reverseRef } from '../
|
|
|
20
20
|
import { getHelpTopic, listHelpTopics } from '../src/help.js';
|
|
21
21
|
import { guideProject } from '../src/guide.js';
|
|
22
22
|
import { isAutoOn, autoOn, autoOff, autoStatus } from '../src/auto.js';
|
|
23
|
-
import { generatePreviewPrompt } from '../src/preview.js';
|
|
23
|
+
import { generatePreviewPrompt, getPreviewStatus } from '../src/preview.js';
|
|
24
24
|
|
|
25
25
|
// ─── 路径解析 ──────────────────────────────────────────
|
|
26
26
|
// findLoomRoot / findVersionDir / readCurrentPointer 已提取到 shared/paths.js
|
|
@@ -180,8 +180,38 @@ try {
|
|
|
180
180
|
case 'list':
|
|
181
181
|
output(listPhilosophyFiles(getPhilosophyDir(versionDir)));
|
|
182
182
|
break;
|
|
183
|
+
case 'check': {
|
|
184
|
+
const philDir = getPhilosophyDir(versionDir);
|
|
185
|
+
const inspiration = validateInspirationSources(philDir);
|
|
186
|
+
const decomposition = validatePartDecomposition(philDir);
|
|
187
|
+
const allIssues = [...inspiration.issues, ...decomposition.issues];
|
|
188
|
+
const allPassed = inspiration.passed && decomposition.passed;
|
|
189
|
+
|
|
190
|
+
if (allPassed) {
|
|
191
|
+
console.log('✓ 哲学文档校验通过');
|
|
192
|
+
console.log(' 灵感来源:');
|
|
193
|
+
for (const { file, sources } of inspiration.sources) {
|
|
194
|
+
console.log(` ${file}: ${sources.length} 个源`);
|
|
195
|
+
}
|
|
196
|
+
console.log(' 实现部分拆解:');
|
|
197
|
+
for (const part of decomposition.parts) {
|
|
198
|
+
console.log(` - ${part}`);
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
const high = allIssues.filter((i) => i.severity === 'high').length;
|
|
202
|
+
const medium = allIssues.filter((i) => i.severity === 'medium').length;
|
|
203
|
+
console.log(`✗ 哲学文档校验未通过(${allIssues.length} 个问题: ${high} high, ${medium} medium)`);
|
|
204
|
+
for (const issue of allIssues) {
|
|
205
|
+
const icon = issue.severity === 'high' ? '⚠' : '·';
|
|
206
|
+
console.log(` ${icon} [${issue.severity}] ${issue.msg}`);
|
|
207
|
+
}
|
|
208
|
+
console.log('\n参见 meta/PHILOSOPHY_WEAVER.md + dimensions/PART_DECOMPOSITION.md + dimensions/SEARCH_METHODOLOGY.md。');
|
|
209
|
+
exit(1);
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
183
213
|
default:
|
|
184
|
-
die(`未知子命令: philosophy ${sub}\n用法: loom philosophy [get <anchor>|list]`);
|
|
214
|
+
die(`未知子命令: philosophy ${sub}\n用法: loom philosophy [get <anchor>|list|check]`);
|
|
185
215
|
}
|
|
186
216
|
break;
|
|
187
217
|
}
|
|
@@ -338,12 +368,16 @@ try {
|
|
|
338
368
|
break;
|
|
339
369
|
}
|
|
340
370
|
|
|
341
|
-
case 'guide': {
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
371
|
+
case 'guide': {
|
|
372
|
+
const dryRun = argv.includes('--dry-run');
|
|
373
|
+
const result = guideProject(cwd(), { dryRun });
|
|
374
|
+
console.log(`阶段 ${result.stage_num}: ${result.stage}`);
|
|
375
|
+
if (dryRun) {
|
|
376
|
+
console.log('诊断: dry-run(不写 heartbeat)');
|
|
377
|
+
}
|
|
378
|
+
if (result.auto) {
|
|
379
|
+
console.log(`模式: AUTO(自动执行,不等确认)`);
|
|
380
|
+
} else {
|
|
347
381
|
console.log(`模式: 手动(每步需用户确认)`);
|
|
348
382
|
}
|
|
349
383
|
console.log(`\n${result.message}`);
|
|
@@ -395,15 +429,39 @@ try {
|
|
|
395
429
|
break;
|
|
396
430
|
}
|
|
397
431
|
|
|
398
|
-
case 'preview': {
|
|
399
|
-
const previewFile = join(cwd(), 'loom-preview.html');
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
432
|
+
case 'preview': {
|
|
433
|
+
const previewFile = join(cwd(), 'loom-preview.html');
|
|
434
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
435
|
+
console.log(`用法:
|
|
436
|
+
loom preview 打开新鲜 preview;过期时提示重新生成
|
|
437
|
+
loom preview --regen 输出生成提示词,让 Agent 重写 loom-preview.html
|
|
438
|
+
loom preview status 检查 preview 是否存在、是否新鲜
|
|
439
|
+
loom preview --stale 强行打开过期 preview
|
|
440
|
+
loom preview --help 显示本帮助`);
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
const status = getPreviewStatus(cwd());
|
|
444
|
+
const hasPreview = status.exists;
|
|
445
|
+
const regenOnly = argv.includes('--regen') || argv.includes('-r');
|
|
446
|
+
const openStale = argv.includes('--stale');
|
|
447
|
+
|
|
448
|
+
if (sub === 'status') {
|
|
449
|
+
output(status);
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// 已有 HTML 且没指定 --regen:直接打开浏览器
|
|
454
|
+
if (hasPreview && !regenOnly) {
|
|
455
|
+
if (!status.fresh && !openStale) {
|
|
456
|
+
console.log('preview 已过期:.loom 源文件比 loom-preview.html 更新。');
|
|
457
|
+
console.log(` preview: ${status.preview_mtime || '未知'}`);
|
|
458
|
+
console.log(` 最新源: ${status.source_latest_mtime || '未知'} ${status.latest_source_file ? `(${status.latest_source_file})` : ''}`);
|
|
459
|
+
console.log('\n下一步: loom preview --regen');
|
|
460
|
+
console.log('强行打开旧 preview: loom preview --stale');
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
const { spawn } = await import('node:child_process');
|
|
464
|
+
const target = previewFile.replace(/\\/g, '/');
|
|
407
465
|
if (process.platform === 'win32') {
|
|
408
466
|
spawn('cmd', ['/c', 'start', target], { detached: true, stdio: 'ignore' }).unref();
|
|
409
467
|
} else if (process.platform === 'darwin') {
|
|
@@ -412,9 +470,9 @@ try {
|
|
|
412
470
|
spawn('xdg-open', [target], { detached: true, stdio: 'ignore' }).unref();
|
|
413
471
|
}
|
|
414
472
|
console.log(`已打开浏览器: ${previewFile}`);
|
|
415
|
-
console.log(`重新生成: loom preview --regen`);
|
|
416
|
-
break;
|
|
417
|
-
}
|
|
473
|
+
console.log(status.fresh ? `重新生成: loom preview --regen` : `已打开旧 preview。重新生成: loom preview --regen`);
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
418
476
|
|
|
419
477
|
// 没有 HTML 或指定 --regen:输出提示词让 AI 生成
|
|
420
478
|
const prompt = generatePreviewPrompt();
|
|
@@ -450,9 +508,10 @@ To Human:
|
|
|
450
508
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
451
509
|
|
|
452
510
|
用法:
|
|
453
|
-
loom init 初始化项目(创建 .loom/v1/ 骨架 + 模板)
|
|
454
|
-
loom guide 诊断当前阶段,输出下一步引导
|
|
455
|
-
loom
|
|
511
|
+
loom init 初始化项目(创建 .loom/v1/ 骨架 + 模板)
|
|
512
|
+
loom guide 诊断当前阶段,输出下一步引导
|
|
513
|
+
loom guide --dry-run 只读诊断当前阶段,不写 heartbeat
|
|
514
|
+
loom auto on|off|status AUTO 模式开关(on 时 Agent 自动连续执行)
|
|
456
515
|
loom activate <role> 输出角色激活提示词(weaver|visionary|architect|forge|keeper)
|
|
457
516
|
|
|
458
517
|
loom version list 列出所有版本(* 标记当前)
|
|
@@ -474,12 +533,15 @@ To Human:
|
|
|
474
533
|
|
|
475
534
|
loom doctor 项目健康检查(一致性+孤儿引用+循环依赖+僵尸)
|
|
476
535
|
loom context 上下文摘要(进度+下一步+待验证+风险)
|
|
477
|
-
loom preview
|
|
478
|
-
loom preview
|
|
479
|
-
loom
|
|
536
|
+
loom preview 打开新鲜 HTML;过期则提示重新生成
|
|
537
|
+
loom preview status 检查 preview 是否存在、是否新鲜
|
|
538
|
+
loom preview --regen 强制重新输出提示词(让 AI 重新生成 HTML)
|
|
539
|
+
loom preview --stale 强行打开过期 preview
|
|
540
|
+
loom help <topic> 分层指南(workflow|concepts|loop|version|doctor|preview)
|
|
480
541
|
|
|
481
542
|
loom philosophy get <anchor> 按锚点加载哲学章节
|
|
482
543
|
loom philosophy list 列出哲学文档文件
|
|
544
|
+
loom philosophy check 校验灵感来源质量(源数量/多样性/理由)
|
|
483
545
|
|
|
484
546
|
loom verify contract <id> 返回某 Intent 的验收契约(解析引用)
|
|
485
547
|
loom verify history <id> 返回某 Intent 验证历史
|
package/cli/help/doctor.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
loom doctor
|
|
7
7
|
\`\`\`
|
|
8
8
|
|
|
9
|
-
检测
|
|
9
|
+
检测 7 类问题:
|
|
10
10
|
|
|
11
11
|
| 问题类型 | 严重度 | 说明 |
|
|
12
12
|
|---|---|---|
|
|
@@ -15,9 +15,41 @@ loom doctor
|
|
|
15
15
|
| orphan_dependency | high | depends_on 引用不存在的 Intent |
|
|
16
16
|
| completed_no_record | high | completed 但无验证记录 |
|
|
17
17
|
| completed_depends_blocked | high | completed 依赖 blocked 的 Intent |
|
|
18
|
+
| inspiration_source | high/medium | 灵感来源质量不达标(源太少/全是 Wikipedia/缺理由) |
|
|
19
|
+
| part_decomposition | high/medium | 缺少实现部分拆解清单(Weaver 跳过了拆解步骤) |
|
|
18
20
|
| in_progress_no_record | medium | in_progress 但无验证记录(可能中断) |
|
|
19
21
|
| zombie | medium | in_progress/blocked 超过 7 天无活动 |
|
|
20
22
|
|
|
23
|
+
## 灵感来源校验
|
|
24
|
+
|
|
25
|
+
\`\`\`bash
|
|
26
|
+
loom philosophy check
|
|
27
|
+
\`\`\`
|
|
28
|
+
|
|
29
|
+
单独校验哲学文档的灵感来源质量。防止 Weaver 从训练数据"背"几个名字就交差。
|
|
30
|
+
|
|
31
|
+
校验规则:
|
|
32
|
+
- 至少 3 个独立源
|
|
33
|
+
- 至少 2 个非 Wikipedia 链接
|
|
34
|
+
- 每个源必须有选取理由(萃取/转译/启发关系)
|
|
35
|
+
- Wikipedia 占比不超过 70%
|
|
36
|
+
|
|
37
|
+
不达标时,需要重新织造哲学——真正走搜索漏斗,找原著、论文、工程博客等深度源。
|
|
38
|
+
|
|
39
|
+
## 实现部分拆解校验
|
|
40
|
+
|
|
41
|
+
\`\`\`bash
|
|
42
|
+
loom philosophy check
|
|
43
|
+
\`\`\`
|
|
44
|
+
|
|
45
|
+
同时校验哲学文档是否包含"实现部分清单"——Weaver 是否按 PART_DECOMPOSITION.md 拆解了项目的实现部分。
|
|
46
|
+
|
|
47
|
+
校验规则:
|
|
48
|
+
- 哲学文档中必须有"实现部分清单"章节(或"部分拆解""Part Decomposition"等)
|
|
49
|
+
- 至少识别到 2 个实现部分(小项目建议 3-5 个,大项目 6-10 个)
|
|
50
|
+
|
|
51
|
+
缺少时,说明 Weaver 跳过了拆解步骤,需要重新织造——按 PART_DECOMPOSITION.md 的方法论识别项目的实现部分。
|
|
52
|
+
|
|
21
53
|
## 上下文摘要
|
|
22
54
|
|
|
23
55
|
\`\`\`bash
|
|
@@ -25,9 +57,28 @@ loom context
|
|
|
25
57
|
\`\`\`
|
|
26
58
|
|
|
27
59
|
一条命令获取:进度 + 下一个 Intent + 待验证 + 不一致项 + 风险。
|
|
28
|
-
Agent 重启后先跑这个,快速知道"我在哪、接下来做什么"。
|
|
29
|
-
|
|
30
|
-
##
|
|
60
|
+
Agent 重启后先跑这个,快速知道"我在哪、接下来做什么"。
|
|
61
|
+
|
|
62
|
+
## 只读阶段诊断
|
|
63
|
+
|
|
64
|
+
\`\`\`bash
|
|
65
|
+
loom guide --dry-run
|
|
66
|
+
\`\`\`
|
|
67
|
+
|
|
68
|
+
用于审计、子代理预演、只读探测。它输出和 `loom guide` 同样的阶段判断,但不写 `.loom/heartbeat.json`。
|
|
69
|
+
|
|
70
|
+
## Preview 新鲜度诊断
|
|
71
|
+
|
|
72
|
+
\`\`\`bash
|
|
73
|
+
loom preview status
|
|
74
|
+
\`\`\`
|
|
75
|
+
|
|
76
|
+
检查 `loom-preview.html` 是否比当前 `.loom/v{N}` 源文件更新。人类要求看 preview 时,Agent 先跑这个命令:
|
|
77
|
+
- `fresh=true`:可以 `loom preview`
|
|
78
|
+
- `fresh=false`:不要打开旧投影,先 `loom preview --regen`
|
|
79
|
+
- 必须看旧投影:`loom preview --stale`
|
|
80
|
+
|
|
81
|
+
## 崩溃恢复
|
|
31
82
|
|
|
32
83
|
### Forge 崩溃(Intent 留在 in_progress)
|
|
33
84
|
|
|
@@ -67,4 +118,4 @@ LOOM 假设项目使用 Git。所有 .loom/ 下的文件都应纳入版本控制
|
|
|
67
118
|
- 误操作 → 从 Git 回滚
|
|
68
119
|
- 变更追溯 → Git log 就是审计日志
|
|
69
120
|
|
|
70
|
-
LOOM 不内置备份、审计、回滚——这些是版本控制的职责。
|
|
121
|
+
LOOM 不内置备份、审计、回滚——这些是版本控制的职责。
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
## Preview 指南
|
|
2
|
+
|
|
3
|
+
preview 是给人看的只读投影。它把 `.loom/v{N}` 的哲学、愿景、架构、Intent Map 和验证记录压缩成 `loom-preview.html`。
|
|
4
|
+
|
|
5
|
+
## 常用命令
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
loom preview status # 只读检查 preview 是否存在、是否新鲜
|
|
9
|
+
loom preview # 新鲜则打开;过期则提示重新生成
|
|
10
|
+
loom preview --regen # 输出生成提示词,让 Agent 重写 loom-preview.html
|
|
11
|
+
loom preview --stale # 强行打开过期 preview
|
|
12
|
+
loom preview --help # 查看 preview 命令用法
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Agent 使用规则
|
|
16
|
+
|
|
17
|
+
当用户说“看看进度”“打开 preview”“让我看全局”时:
|
|
18
|
+
|
|
19
|
+
1. 先运行 `loom preview status`
|
|
20
|
+
2. 如果 `fresh=true`,运行 `loom preview`
|
|
21
|
+
3. 如果 `fresh=false`,不要打开旧页面,运行 `loom preview --regen`
|
|
22
|
+
4. 按提示词读取 `.loom/`,重写 `loom-preview.html`
|
|
23
|
+
5. 再运行 `loom preview` 打开
|
|
24
|
+
|
|
25
|
+
只有用户明确说要看旧版时,才使用 `loom preview --stale`。
|
|
26
|
+
|
|
27
|
+
## 新鲜度判断
|
|
28
|
+
|
|
29
|
+
LOOM 使用最小 mtime 机制,不做 hash:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
preview_mtime = loom-preview.html 修改时间
|
|
33
|
+
source_latest_mtime = 当前 .loom/v{N} 核心源文件最新修改时间
|
|
34
|
+
|
|
35
|
+
preview_mtime >= source_latest_mtime → fresh
|
|
36
|
+
否则 → stale
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
核心源文件包括:
|
|
40
|
+
- `00_PHILOSOPHY/`
|
|
41
|
+
- `01_VISION.md`
|
|
42
|
+
- `02_ARCHITECTURE.md`
|
|
43
|
+
- `03_DECISIONS/`
|
|
44
|
+
- `04_INTENT_MAP.json`
|
|
45
|
+
- `05_VERIFICATION.md`
|
|
46
|
+
- `06_CHANGELOG.md`
|
|
47
|
+
- `verifications/`
|
|
48
|
+
|
|
49
|
+
## 为什么不直接打开旧 preview
|
|
50
|
+
|
|
51
|
+
旧 preview 是假仪表盘风险:`.loom/` 已经进入 blocked、deviated 或完成新 Intent,但 HTML 仍显示旧状态。
|
|
52
|
+
|
|
53
|
+
所以普通 `loom preview` 在 stale 时会停止并提示 `loom preview --regen`。
|
|
54
|
+
|
|
55
|
+
## 生成责任
|
|
56
|
+
|
|
57
|
+
当前 CLI 不直接生成 HTML。`loom preview --regen` 输出提示词,要求 Agent 读取 `.loom/` 后生成 `loom-preview.html`。
|
|
58
|
+
|
|
59
|
+
CLI 负责判断是否过期和打开文件,Agent 负责把信息做成高质量视觉投影。
|
package/cli/help/workflow.md
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
loom guide
|
|
9
9
|
\`\`\`
|
|
10
10
|
|
|
11
|
-
guide 检测项目当前在哪个阶段,输出"你在阶段 X,下一步做 Y"。
|
|
12
|
-
Agent 每完成一步都跑 guide 确认下一步。
|
|
11
|
+
guide 检测项目当前在哪个阶段,输出"你在阶段 X,下一步做 Y"。
|
|
12
|
+
Agent 每完成一步都跑 guide 确认下一步。
|
|
13
|
+
审计、预演、子代理探测时用 `loom guide --dry-run`,避免写 `.loom/heartbeat.json`。
|
|
13
14
|
|
|
14
15
|
## AUTO 模式
|
|
15
16
|
|
|
@@ -71,15 +72,20 @@ loom context # 当前状态摘要
|
|
|
71
72
|
每个 Intent 独立走一圈:选 → 实现 → 验证 → 闭合或修正。
|
|
72
73
|
详细流程见 \`loom help loop\`。
|
|
73
74
|
|
|
74
|
-
## 阶段 5:人类预览
|
|
75
|
-
|
|
76
|
-
\`\`\`bash
|
|
77
|
-
loom preview
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
## 阶段 5:人类预览
|
|
76
|
+
|
|
77
|
+
\`\`\`bash
|
|
78
|
+
loom preview status
|
|
79
|
+
loom preview
|
|
80
|
+
loom preview --regen
|
|
81
|
+
\`\`\`
|
|
82
|
+
|
|
83
|
+
preview 是给人看的只读投影。Agent 应先跑 `loom preview status`:
|
|
84
|
+
- `fresh=true` → 运行 `loom preview` 打开
|
|
85
|
+
- `fresh=false` → 运行 `loom preview --regen`,按提示词重写 `loom-preview.html`,再打开
|
|
86
|
+
- 用户明确要看旧版 → `loom preview --stale`
|
|
87
|
+
|
|
88
|
+
详细规则见 `loom help preview`。
|
|
83
89
|
|
|
84
90
|
## 阶段 6:版本演进(按需)
|
|
85
91
|
|
|
@@ -91,4 +97,4 @@ loom preview
|
|
|
91
97
|
- **哲学是经线,意图是纬线** — 所有角色共享哲学锚点
|
|
92
98
|
- **底线不可协商** — BASELINE 5 条 + 项目特定底线,角色激活时强制加载
|
|
93
99
|
- **意图可回溯** — 每个 Intent 携带叙事,Keeper 独立验证忠实度
|
|
94
|
-
- **文档开销不超过开发开销** — 小项目可以粗粒度,不必教条
|
|
100
|
+
- **文档开销不超过开发开销** — 小项目可以粗粒度,不必教条
|
package/cli/src/diagnostics.js
CHANGED
|
@@ -5,9 +5,103 @@
|
|
|
5
5
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { loadIntentMap, getStatus, getNextIntent, getNarrative, getIntent } from './intent-map.js';
|
|
8
|
-
import { getPhilosophy } from './philosophy.js';
|
|
8
|
+
import { getPhilosophy, validateInspirationSources, validatePartDecomposition } from './philosophy.js';
|
|
9
9
|
import { getVerificationHistory, getPendingVerifications, getVerificationContract } from './verify.js';
|
|
10
10
|
|
|
11
|
+
function readIntentMapRaw(versionDir) {
|
|
12
|
+
const filePath = join(versionDir, '04_INTENT_MAP.json');
|
|
13
|
+
if (!existsSync(filePath)) return null;
|
|
14
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function summarizeRawIntentMap(data) {
|
|
18
|
+
const intents = data?.intents && typeof data.intents === 'object' ? data.intents : {};
|
|
19
|
+
const ids = { pending: [], in_progress: [], completed: [], blocked: [], needs_review: [] };
|
|
20
|
+
const titles = {};
|
|
21
|
+
|
|
22
|
+
for (const [id, intent] of Object.entries(intents)) {
|
|
23
|
+
const status = intent?.status;
|
|
24
|
+
if (ids[status]) ids[status].push(id);
|
|
25
|
+
titles[id] = intent?.title || '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
counts: {
|
|
30
|
+
pending: ids.pending.length,
|
|
31
|
+
in_progress: ids.in_progress.length,
|
|
32
|
+
completed: ids.completed.length,
|
|
33
|
+
blocked: ids.blocked.length,
|
|
34
|
+
needs_review: ids.needs_review.length,
|
|
35
|
+
total: Object.keys(intents).length,
|
|
36
|
+
},
|
|
37
|
+
ids,
|
|
38
|
+
titles,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function intentMapDiagnostics(versionDir) {
|
|
43
|
+
const issues = [];
|
|
44
|
+
let raw = null;
|
|
45
|
+
let valid = null;
|
|
46
|
+
let validMap = null;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
raw = readIntentMapRaw(versionDir);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
issues.push({ id: 'intent_map', type: 'intent_map_unreadable', severity: 'fatal', msg: `Intent Map 无法读取或 JSON 损坏: ${e.message}` });
|
|
52
|
+
return { raw, valid, issues, validMap: null };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!raw) {
|
|
56
|
+
issues.push({ id: 'intent_map', type: 'intent_map_missing', severity: 'fatal', msg: '缺少 04_INTENT_MAP.json' });
|
|
57
|
+
return { raw, valid, issues, validMap: null };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const isTemplate = raw._meta?._template === true;
|
|
61
|
+
if (isTemplate) {
|
|
62
|
+
issues.push({ id: 'intent_map', type: 'intent_map_template', severity: 'high', msg: 'Intent Map 仍是模板,尚未由 Architect 产出真实意图图' });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
validMap = loadIntentMap(versionDir);
|
|
67
|
+
valid = true;
|
|
68
|
+
} catch (e) {
|
|
69
|
+
valid = false;
|
|
70
|
+
// 模板状态下字段缺失是预期的,降级为 high 而非 fatal
|
|
71
|
+
// 非模板状态下字段缺失是真正的损坏,保持 fatal
|
|
72
|
+
issues.push({ id: 'intent_map', type: 'intent_map_invalid', severity: isTemplate ? 'high' : 'fatal', msg: e.message });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { raw, valid, issues, validMap };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getIntentVerificationMethod(intent) {
|
|
79
|
+
return intent.verification_method || intent._optional?.verification_method || null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeVerificationCommand(command) {
|
|
83
|
+
return String(command || '')
|
|
84
|
+
.replace(/^\s*run\s+/i, '')
|
|
85
|
+
.replace(/^\s*exec\s+/i, '')
|
|
86
|
+
.replace(/\s+/g, ' ')
|
|
87
|
+
.trim();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function commandCoversMethod(actualCommand, expectedMethod) {
|
|
91
|
+
const actual = normalizeVerificationCommand(actualCommand);
|
|
92
|
+
const expected = normalizeVerificationCommand(expectedMethod);
|
|
93
|
+
if (!actual || !expected) return false;
|
|
94
|
+
|
|
95
|
+
return expected.split('&&').every((part) => {
|
|
96
|
+
const expectedPart = normalizeVerificationCommand(part);
|
|
97
|
+
if (!expectedPart) return true;
|
|
98
|
+
if (actual.includes(expectedPart)) return true;
|
|
99
|
+
// npm test is an acceptable broader reproduction for node --test based methods.
|
|
100
|
+
if (expectedPart.startsWith('node --test') && actual.includes('npm test')) return true;
|
|
101
|
+
return false;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
11
105
|
// ─── doctor ────────────────────────────────────────────
|
|
12
106
|
// 全面健康检查:一致性 + 孤儿引用 + 循环依赖 + 僵尸 Intent
|
|
13
107
|
|
|
@@ -19,8 +113,15 @@ import { getVerificationHistory, getPendingVerifications, getVerificationContrac
|
|
|
19
113
|
* @returns {{ issues: object[], summary: object }}
|
|
20
114
|
*/
|
|
21
115
|
export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
22
|
-
const
|
|
23
|
-
const issues = [];
|
|
116
|
+
const mapState = intentMapDiagnostics(versionDir);
|
|
117
|
+
const issues = [...mapState.issues];
|
|
118
|
+
|
|
119
|
+
if (!mapState.validMap) {
|
|
120
|
+
appendPhilosophyDiagnostics(issues, philosophyDir);
|
|
121
|
+
return { issues, summary: summarizeIssues(issues) };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const { intents } = mapState.validMap;
|
|
24
125
|
|
|
25
126
|
// 1. 状态一致性:in_progress/completed 但无验证记录
|
|
26
127
|
for (const [id, intent] of Object.entries(intents)) {
|
|
@@ -93,8 +194,8 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
|
93
194
|
// 7. 验证脚本可执行性:检查 verification_method 引用的脚本/目录是否存在
|
|
94
195
|
const projectDir = join(versionDir, '..', '..');
|
|
95
196
|
for (const [id, intent] of Object.entries(intents)) {
|
|
96
|
-
|
|
97
|
-
|
|
197
|
+
const vm = getIntentVerificationMethod(intent);
|
|
198
|
+
if (!vm) continue;
|
|
98
199
|
// 检测 npm test 引用
|
|
99
200
|
if (vm.includes('npm test') || vm.includes('npm run test')) {
|
|
100
201
|
const pkgPath = join(projectDir, 'package.json');
|
|
@@ -124,15 +225,56 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
|
124
225
|
}
|
|
125
226
|
}
|
|
126
227
|
|
|
127
|
-
|
|
228
|
+
// 8. completed Intent 的 verification_method 必须被最新验证记录覆盖,防止契约命令漂移。
|
|
229
|
+
for (const [id, intent] of Object.entries(intents)) {
|
|
230
|
+
if (intent.status !== 'completed') continue;
|
|
231
|
+
const method = getIntentVerificationMethod(intent);
|
|
232
|
+
if (!method) continue;
|
|
233
|
+
|
|
234
|
+
const expected = normalizeVerificationCommand(method);
|
|
235
|
+
if (!expected || expected === 'human_review') continue;
|
|
236
|
+
|
|
237
|
+
const history = getVerificationHistory(verificationsDir, id);
|
|
238
|
+
const latest = history?.records?.[history.records.length - 1];
|
|
239
|
+
const actual = normalizeVerificationCommand(latest?.reproduction_command);
|
|
240
|
+
|
|
241
|
+
if (!latest) {
|
|
242
|
+
issues.push({ id, type: 'verification_method_unverified', severity: 'high', msg: `${id} 声明了 verification_method 但没有验证记录覆盖: ${method}` });
|
|
243
|
+
} else if (!actual) {
|
|
244
|
+
issues.push({ id, type: 'verification_method_unverified', severity: 'high', msg: `${id} 最新验证记录缺少 reproduction_command,无法复现 verification_method: ${method}` });
|
|
245
|
+
} else if (!commandCoversMethod(actual, expected)) {
|
|
246
|
+
issues.push({ id, type: 'verification_method_drift', severity: 'high', msg: `${id} verification_method 未被最新 reproduction_command 覆盖。method="${method}" reproduction_command="${latest.reproduction_command}"` });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
appendPhilosophyDiagnostics(issues, philosophyDir);
|
|
251
|
+
return { issues, summary: summarizeIssues(issues) };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function appendPhilosophyDiagnostics(issues, philosophyDir) {
|
|
255
|
+
// 哲学灵感来源校验(防止 Weaver 从训练数据"背"几个名字就交差)
|
|
256
|
+
if (!existsSync(philosophyDir)) return;
|
|
257
|
+
|
|
258
|
+
const inspirationCheck = validateInspirationSources(philosophyDir);
|
|
259
|
+
for (const issue of inspirationCheck.issues) {
|
|
260
|
+
issues.push({ id: 'philosophy', type: 'inspiration_source', severity: issue.severity, msg: issue.msg });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 实现部分拆解校验(防止 Weaver 跳过拆解步骤)
|
|
264
|
+
const decompositionCheck = validatePartDecomposition(philosophyDir);
|
|
265
|
+
for (const issue of decompositionCheck.issues) {
|
|
266
|
+
issues.push({ id: 'philosophy', type: 'part_decomposition', severity: issue.severity, msg: issue.msg });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function summarizeIssues(issues) {
|
|
271
|
+
return {
|
|
128
272
|
total_issues: issues.length,
|
|
129
273
|
fatal: issues.filter((i) => i.severity === 'fatal').length,
|
|
130
274
|
high: issues.filter((i) => i.severity === 'high').length,
|
|
131
275
|
medium: issues.filter((i) => i.severity === 'medium').length,
|
|
132
276
|
healthy: issues.length === 0,
|
|
133
277
|
};
|
|
134
|
-
|
|
135
|
-
return { issues, summary };
|
|
136
278
|
}
|
|
137
279
|
|
|
138
280
|
/**
|
|
@@ -183,19 +325,21 @@ function detectCycles(intents) {
|
|
|
183
325
|
* @returns {object}
|
|
184
326
|
*/
|
|
185
327
|
export function contextSummary(versionDir, verificationsDir, philosophyDir) {
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const
|
|
328
|
+
const mapState = intentMapDiagnostics(versionDir);
|
|
329
|
+
const status = mapState.valid ? getStatus(versionDir) : summarizeRawIntentMap(mapState.raw);
|
|
330
|
+
const next = mapState.valid ? getNextIntent(versionDir) : null;
|
|
331
|
+
const pending = mapState.valid ? getPendingVerifications(versionDir, verificationsDir) : [];
|
|
189
332
|
const { issues } = doctor(versionDir, verificationsDir, philosophyDir);
|
|
190
333
|
|
|
191
334
|
const risks = [];
|
|
192
335
|
const fatalCount = issues.filter((i) => i.severity === 'fatal').length;
|
|
193
336
|
const highCount = issues.filter((i) => i.severity === 'high').length;
|
|
194
|
-
if (fatalCount > 0) risks.push(`${fatalCount}
|
|
337
|
+
if (fatalCount > 0) risks.push(`${fatalCount} 个致命问题(Intent Map 损坏/循环依赖)`);
|
|
195
338
|
if (highCount > 0) risks.push(`${highCount} 个高严重度问题(状态不一致/孤儿引用)`);
|
|
196
339
|
if (status.counts.blocked > 0) risks.push(`${status.counts.blocked} 个阻塞 Intent`);
|
|
197
340
|
|
|
198
341
|
return {
|
|
342
|
+
intent_map_valid: mapState.valid === true,
|
|
199
343
|
progress: {
|
|
200
344
|
completed: status.counts.completed,
|
|
201
345
|
total: status.counts.total,
|