@haaaiawd/loom 0.1.0 → 0.7.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/cli/bin/loom.js +106 -71
- package/cli/help/concepts.md +72 -0
- package/cli/help/doctor.md +70 -0
- package/cli/help/loop.md +135 -0
- package/cli/help/version.md +60 -0
- package/cli/help/workflow.md +94 -0
- package/cli/src/activate.js +21 -12
- package/cli/src/auto.js +54 -5
- package/cli/src/diagnostics.js +63 -23
- package/cli/src/guide.js +74 -5
- package/cli/src/help.js +29 -398
- package/cli/src/init.js +42 -8
- package/cli/src/intent-map.js +42 -85
- package/cli/src/philosophy.js +2 -73
- package/cli/src/preview-prompt.md +1 -0
- package/cli/src/shared/md-utils.js +125 -0
- package/cli/src/shared/paths.js +73 -0
- package/cli/src/verify.js +62 -70
- package/cli/src/version.js +1 -1
- package/meta/INTENT_LOOP.md +159 -18
- package/package.json +2 -1
- package/roles/architect.md +12 -0
- package/roles/keeper.md +28 -1
- package/templates/INTENT_MAP_TEMPLATE.json +4 -1
- package/templates/VISION_TEMPLATE.md +4 -2
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
## LOOM 工作流
|
|
2
|
+
|
|
3
|
+
从零到交付的完整流程。每个阶段有明确的产出和验收标准。
|
|
4
|
+
|
|
5
|
+
## 第一步:诊断当前阶段
|
|
6
|
+
|
|
7
|
+
\`\`\`bash
|
|
8
|
+
loom guide
|
|
9
|
+
\`\`\`
|
|
10
|
+
|
|
11
|
+
guide 检测项目当前在哪个阶段,输出"你在阶段 X,下一步做 Y"。
|
|
12
|
+
Agent 每完成一步都跑 guide 确认下一步。
|
|
13
|
+
|
|
14
|
+
## AUTO 模式
|
|
15
|
+
|
|
16
|
+
\`\`\`bash
|
|
17
|
+
loom auto on # 开启:Agent 自动连续执行,不等确认
|
|
18
|
+
loom auto off # 关闭:每步需要用户确认
|
|
19
|
+
\`\`\`
|
|
20
|
+
|
|
21
|
+
AUTO on 时 Agent 一路跑到底,跑完生成 preview 给人看。
|
|
22
|
+
AUTO off 时每步停下等用户说继续。
|
|
23
|
+
|
|
24
|
+
## 阶段 1:织造哲学(Weaver)
|
|
25
|
+
|
|
26
|
+
\`\`\`bash
|
|
27
|
+
loom activate weaver
|
|
28
|
+
\`\`\`
|
|
29
|
+
|
|
30
|
+
Weaver 根据项目特征从真实思想体系织造定制化哲学。产出:
|
|
31
|
+
- PRODUCT_PHILOSOPHY.md — 产品价值观、反模式清单、决策取舍规则
|
|
32
|
+
- ENGINEERING_CREED.md — 工程原则(按需)
|
|
33
|
+
- DECISION_RUBRIC.md — 冲突时的优先级(按需)
|
|
34
|
+
- PROJECT_BASELINE.md — 项目特定底线(按需)
|
|
35
|
+
|
|
36
|
+
**验收**:哲学有北极星、有反模式、有决策标准。全是空话就重做。
|
|
37
|
+
|
|
38
|
+
## 阶段 2:定义愿景(Visionary)
|
|
39
|
+
|
|
40
|
+
\`\`\`bash
|
|
41
|
+
loom activate visionary
|
|
42
|
+
\`\`\`
|
|
43
|
+
|
|
44
|
+
基于哲学定义产品愿景,为每个 Intent 写意图叙事("为什么存在")。产出:
|
|
45
|
+
- 01_VISION.md — 北极星 + 意图叙事列表
|
|
46
|
+
|
|
47
|
+
**验收**:叙事是"为什么"不是"做什么"。写成功能列表就重做。
|
|
48
|
+
|
|
49
|
+
## 阶段 3:设计系统(Architect)
|
|
50
|
+
|
|
51
|
+
\`\`\`bash
|
|
52
|
+
loom activate architect
|
|
53
|
+
\`\`\`
|
|
54
|
+
|
|
55
|
+
基于愿景设计系统结构,绘制 Intent Map。产出:
|
|
56
|
+
- 02_ARCHITECTURE.md — 系统设计
|
|
57
|
+
- 04_INTENT_MAP.json — Intent 依赖图 + 验收契约 + 哲学锚点
|
|
58
|
+
|
|
59
|
+
**验收**:验收契约具体到可验证,依赖无环,每个 Intent 有叙事引用。
|
|
60
|
+
跑 \`loom intent validate\` 校验结构,跑 \`loom doctor\` 检查完整性。
|
|
61
|
+
|
|
62
|
+
## 阶段 4:Intent Loop
|
|
63
|
+
|
|
64
|
+
\`\`\`bash
|
|
65
|
+
loom activate keeper # Keeper 选 Intent、验证
|
|
66
|
+
loom activate forge # Forge 实现
|
|
67
|
+
loom intent next # 下一个可执行 Intent
|
|
68
|
+
loom context # 当前状态摘要
|
|
69
|
+
\`\`\`
|
|
70
|
+
|
|
71
|
+
每个 Intent 独立走一圈:选 → 实现 → 验证 → 闭合或修正。
|
|
72
|
+
详细流程见 \`loom help loop\`。
|
|
73
|
+
|
|
74
|
+
## 阶段 5:人类预览
|
|
75
|
+
|
|
76
|
+
\`\`\`bash
|
|
77
|
+
loom preview
|
|
78
|
+
\`\`\`
|
|
79
|
+
|
|
80
|
+
输出提示词,Agent 按提示词读 .loom/ 文件、拆解信息、生成 HTML 可视化预览。
|
|
81
|
+
人类用浏览器打开 HTML 看全局——哲学、愿景、架构、Intent 进度、验证历史。
|
|
82
|
+
这是只读投影,修改请编辑源文件后重新生成。
|
|
83
|
+
|
|
84
|
+
## 阶段 6:版本演进(按需)
|
|
85
|
+
|
|
86
|
+
当哲学前提/愿景北极星/架构边界变了,需要 Major 升级。
|
|
87
|
+
详细流程见 \`loom help version\`。
|
|
88
|
+
|
|
89
|
+
## 核心原则
|
|
90
|
+
|
|
91
|
+
- **哲学是经线,意图是纬线** — 所有角色共享哲学锚点
|
|
92
|
+
- **底线不可协商** — BASELINE 5 条 + 项目特定底线,角色激活时强制加载
|
|
93
|
+
- **意图可回溯** — 每个 Intent 携带叙事,Keeper 独立验证忠实度
|
|
94
|
+
- **文档开销不超过开发开销** — 小项目可以粗粒度,不必教条
|
package/cli/src/activate.js
CHANGED
|
@@ -4,13 +4,10 @@
|
|
|
4
4
|
import { readFileSync, existsSync } from 'node:fs';
|
|
5
5
|
import { join, resolve, dirname } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { getLoomRoot } from './shared/paths.js';
|
|
7
8
|
|
|
8
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
|
|
10
|
-
function getLoomRoot() {
|
|
11
|
-
return resolve(__dirname, '..', '..');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
11
|
/** 合法角色名 */
|
|
15
12
|
const VALID_ROLES = ['weaver', 'visionary', 'architect', 'forge', 'keeper'];
|
|
16
13
|
|
|
@@ -26,10 +23,10 @@ const ROLE_FILES = {
|
|
|
26
23
|
/**
|
|
27
24
|
* 输出角色激活提示词。
|
|
28
25
|
* @param {string} role — 角色名
|
|
29
|
-
* @param {string}
|
|
26
|
+
* @param {string} versionDir — .loom/v{N} 目录(可选,weaver 不需要)
|
|
30
27
|
* @returns {string} 激活提示词
|
|
31
28
|
*/
|
|
32
|
-
export function activateRole(role,
|
|
29
|
+
export function activateRole(role, versionDir) {
|
|
33
30
|
if (!VALID_ROLES.includes(role)) {
|
|
34
31
|
throw new Error(`未知角色: ${role}\n合法角色: ${VALID_ROLES.join(', ')}`);
|
|
35
32
|
}
|
|
@@ -44,13 +41,25 @@ export function activateRole(role, loomDir) {
|
|
|
44
41
|
}
|
|
45
42
|
parts.push(readFileSync(roleFile, 'utf-8'));
|
|
46
43
|
|
|
47
|
-
// 2. BASELINE
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
// 2. BASELINE 摘要(不重复全文——全文见 meta/BASELINE.md)
|
|
45
|
+
// 5 条底线压缩成摘要,角色需要知道底线存在 + 一句话内容。
|
|
46
|
+
// 如果角色需要底线细节(如 Weaver 织造哲学时),自行 readFileSync 全文。
|
|
47
|
+
parts.push('\n---\n\n## 强制加载:BASELINE 摘要\n\n');
|
|
48
|
+
parts.push('> 完整底线见 `meta/BASELINE.md`。以下是 5 条底线的摘要——\n');
|
|
49
|
+
parts.push('> 角色激活时必须知道这些底线存在,违反任何一条必须立即停止。\n');
|
|
50
|
+
parts.push('> Philosophy Weaver 织造哲学时必须读取完整 BASELINE.md 作为硬约束输入。\n\n');
|
|
51
|
+
parts.push('| 编号 | 底线 | 一句话 |\n');
|
|
52
|
+
parts.push('|------|------|--------|\n');
|
|
53
|
+
parts.push('| B1 | 必须有结构设计 | 编码前必须有明确的目录结构 + 模块职责边界 + 显式依赖关系 |\n');
|
|
54
|
+
parts.push('| B2 | 禁止硬编码 | 密钥/配置/环境特定值/魔法数字不进代码,用环境变量或集中配置 |\n');
|
|
55
|
+
parts.push('| B3 | 接口契约必须显式 | API/CLI/配置/错误语义/跨系统协议必须有显式定义,变更可追溯 |\n');
|
|
56
|
+
parts.push('| B4 | 决策必须可追溯 | 影响架构/接口/技术栈/依赖的决策必须记录(ADR 或等效格式) |\n');
|
|
57
|
+
parts.push('| B5 | 意图必须可回溯 | 每个实现单元有意图叙事("为什么存在"),可被 Keeper 引用对照 |\n');
|
|
58
|
+
parts.push('\n> 底线不可被哲学覆盖。如果织造的哲学与底线冲突,底线优先。\n');
|
|
50
59
|
|
|
51
|
-
// 3. 项目特定底线(如果有
|
|
52
|
-
if (
|
|
53
|
-
const projectBaseline = join(
|
|
60
|
+
// 3. 项目特定底线(如果有 versionDir)
|
|
61
|
+
if (versionDir) {
|
|
62
|
+
const projectBaseline = join(versionDir, '00_PHILOSOPHY/PROJECT_BASELINE.md');
|
|
54
63
|
if (existsSync(projectBaseline)) {
|
|
55
64
|
parts.push('\n---\n\n## 强制加载:项目特定底线\n\n' + readFileSync(projectBaseline, 'utf-8'));
|
|
56
65
|
}
|
package/cli/src/auto.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
// auto — AUTO 模式开关
|
|
1
|
+
// auto — AUTO 模式开关 + 心跳机制
|
|
2
2
|
// 存储机制:.loom/auto 文件存在 = on,不存在 = off
|
|
3
|
-
//
|
|
3
|
+
// 心跳:每次 guide 调用时写 .loom/heartbeat.json(时间戳 + stage + next_command)
|
|
4
|
+
// AUTO on(默认):stage 4+(Intent Loop)自动跑,stage 1-3(哲学/愿景/架构)仍需人类 review
|
|
5
|
+
// AUTO off:所有阶段都需人类确认,每步拆得更碎
|
|
4
6
|
|
|
5
7
|
import { existsSync, writeFileSync, unlinkSync, readFileSync } from 'node:fs';
|
|
6
8
|
import { join } from 'node:path';
|
|
@@ -34,11 +36,58 @@ export function autoOff(loomRoot) {
|
|
|
34
36
|
/**
|
|
35
37
|
* 获取 AUTO 状态描述。
|
|
36
38
|
* @param {string} loomRoot — .loom 目录路径
|
|
37
|
-
* @returns {{ on: boolean, since: string|null }}
|
|
39
|
+
* @returns {{ on: boolean, since: string|null, heartbeat: object|null }}
|
|
38
40
|
*/
|
|
39
41
|
export function autoStatus(loomRoot) {
|
|
40
42
|
const path = join(loomRoot, 'auto');
|
|
41
|
-
if (!existsSync(path)) return { on: false, since: null };
|
|
43
|
+
if (!existsSync(path)) return { on: false, since: null, heartbeat: null };
|
|
42
44
|
const since = readFileSync(path, 'utf-8').trim();
|
|
43
|
-
|
|
45
|
+
const heartbeat = readHeartbeat(loomRoot);
|
|
46
|
+
return { on: true, since, heartbeat };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 写入心跳——每次 guide 调用时记录当前状态。
|
|
51
|
+
* @param {string} loomRoot — .loom 目录路径
|
|
52
|
+
* @param {{ stage: string, stage_num: number, next_command: string, next_action: string }} info
|
|
53
|
+
*/
|
|
54
|
+
export function writeHeartbeat(loomRoot, info) {
|
|
55
|
+
const heartbeat = {
|
|
56
|
+
timestamp: new Date().toISOString(),
|
|
57
|
+
stage: info.stage,
|
|
58
|
+
stage_num: info.stage_num,
|
|
59
|
+
next_command: info.next_command,
|
|
60
|
+
next_action: info.next_action,
|
|
61
|
+
};
|
|
62
|
+
writeFileSync(join(loomRoot, 'heartbeat.json'), JSON.stringify(heartbeat, null, 2), 'utf-8');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 读取心跳。
|
|
67
|
+
* @param {string} loomRoot — .loom 目录路径
|
|
68
|
+
* @returns {object|null}
|
|
69
|
+
*/
|
|
70
|
+
export function readHeartbeat(loomRoot) {
|
|
71
|
+
const path = join(loomRoot, 'heartbeat.json');
|
|
72
|
+
if (!existsSync(path)) return null;
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 判断当前阶段是否需要人类 review。
|
|
82
|
+
* AUTO on 时:stage 1-3(哲学/愿景/架构)需要人类 review,stage 4+ 自动跑
|
|
83
|
+
* AUTO off 时:所有阶段都需要人类 review
|
|
84
|
+
* @param {string} loomRoot — .loom 目录路径
|
|
85
|
+
* @param {number} stageNum — 阶段号
|
|
86
|
+
* @returns {boolean} 是否需要人类 review
|
|
87
|
+
*/
|
|
88
|
+
export function needsHumanReview(loomRoot, stageNum) {
|
|
89
|
+
const autoOn = isAutoOn(loomRoot);
|
|
90
|
+
if (!autoOn) return true; // AUTO off:所有阶段都需人类 review
|
|
91
|
+
// AUTO on:stage 1-3 需人类 review,stage 4+ 自动
|
|
92
|
+
return stageNum > 0 && stageNum < 4;
|
|
44
93
|
}
|
package/cli/src/diagnostics.js
CHANGED
|
@@ -13,13 +13,13 @@ import { getVerificationHistory, getPendingVerifications, getVerificationContrac
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* 项目健康检查。
|
|
16
|
-
* @param {string}
|
|
16
|
+
* @param {string} versionDir — 当前版本目录
|
|
17
17
|
* @param {string} verificationsDir — 验证记录目录
|
|
18
18
|
* @param {string} philosophyDir — 哲学目录
|
|
19
19
|
* @returns {{ issues: object[], summary: object }}
|
|
20
20
|
*/
|
|
21
|
-
export function doctor(
|
|
22
|
-
const { intents, topo_order } = loadIntentMap(
|
|
21
|
+
export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
22
|
+
const { intents, topo_order } = loadIntentMap(versionDir);
|
|
23
23
|
const issues = [];
|
|
24
24
|
|
|
25
25
|
// 1. 状态一致性:in_progress/completed 但无验证记录
|
|
@@ -67,8 +67,8 @@ export function doctor(loomDir, verificationsDir, philosophyDir) {
|
|
|
67
67
|
for (const [id, intent] of Object.entries(intents)) {
|
|
68
68
|
if (intent.status !== 'in_progress' && intent.status !== 'blocked') continue;
|
|
69
69
|
const recordPath = join(verificationsDir, `${id}.json`);
|
|
70
|
-
let lastActivity = existsSync(join(
|
|
71
|
-
? statSync(join(
|
|
70
|
+
let lastActivity = existsSync(join(versionDir, '04_INTENT_MAP.json'))
|
|
71
|
+
? statSync(join(versionDir, '04_INTENT_MAP.json')).mtimeMs
|
|
72
72
|
: now;
|
|
73
73
|
if (existsSync(recordPath)) {
|
|
74
74
|
lastActivity = Math.max(lastActivity, statSync(recordPath).mtimeMs);
|
|
@@ -90,6 +90,40 @@ export function doctor(loomDir, verificationsDir, philosophyDir) {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// 7. 验证脚本可执行性:检查 verification_method 引用的脚本/目录是否存在
|
|
94
|
+
const projectDir = join(versionDir, '..', '..');
|
|
95
|
+
for (const [id, intent] of Object.entries(intents)) {
|
|
96
|
+
if (!intent.verification_method) continue;
|
|
97
|
+
const vm = intent.verification_method;
|
|
98
|
+
// 检测 npm test 引用
|
|
99
|
+
if (vm.includes('npm test') || vm.includes('npm run test')) {
|
|
100
|
+
const pkgPath = join(projectDir, 'package.json');
|
|
101
|
+
if (!existsSync(pkgPath)) {
|
|
102
|
+
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 npm test 但项目根没有 package.json` });
|
|
103
|
+
} else {
|
|
104
|
+
try {
|
|
105
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
106
|
+
const testScript = pkg.scripts && pkg.scripts.test;
|
|
107
|
+
if (!testScript) {
|
|
108
|
+
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 npm test 但 package.json 没有 test 脚本` });
|
|
109
|
+
} else {
|
|
110
|
+
// 检查 test 脚本引用的目录/文件是否存在
|
|
111
|
+
// 常见模式: "node --test test/" / "mocha test/" / "jest" 等
|
|
112
|
+
const testDirMatch = testScript.match(/(?:--test|test)\s+(\S+)/);
|
|
113
|
+
if (testDirMatch) {
|
|
114
|
+
const testTarget = testDirMatch[1].replace(/['"]/g, '');
|
|
115
|
+
if (!existsSync(join(projectDir, testTarget))) {
|
|
116
|
+
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 npm test 但 test 脚本引用的 ${testTarget} 不存在` });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// package.json 解析失败,不报——不是 doctor 的职责
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
93
127
|
const summary = {
|
|
94
128
|
total_issues: issues.length,
|
|
95
129
|
fatal: issues.filter((i) => i.severity === 'fatal').length,
|
|
@@ -143,16 +177,16 @@ function detectCycles(intents) {
|
|
|
143
177
|
|
|
144
178
|
/**
|
|
145
179
|
* 项目上下文摘要——Agent 重启后一条命令获取"我在哪"。
|
|
146
|
-
* @param {string}
|
|
180
|
+
* @param {string} versionDir
|
|
147
181
|
* @param {string} verificationsDir
|
|
148
182
|
* @param {string} philosophyDir
|
|
149
183
|
* @returns {object}
|
|
150
184
|
*/
|
|
151
|
-
export function contextSummary(
|
|
152
|
-
const status = getStatus(
|
|
153
|
-
const next = getNextIntent(
|
|
154
|
-
const pending = getPendingVerifications(
|
|
155
|
-
const { issues } = doctor(
|
|
185
|
+
export function contextSummary(versionDir, verificationsDir, philosophyDir) {
|
|
186
|
+
const status = getStatus(versionDir);
|
|
187
|
+
const next = getNextIntent(versionDir);
|
|
188
|
+
const pending = getPendingVerifications(versionDir, verificationsDir);
|
|
189
|
+
const { issues } = doctor(versionDir, verificationsDir, philosophyDir);
|
|
156
190
|
|
|
157
191
|
const risks = [];
|
|
158
192
|
const fatalCount = issues.filter((i) => i.severity === 'fatal').length;
|
|
@@ -180,23 +214,27 @@ export function contextSummary(loomDir, verificationsDir, philosophyDir) {
|
|
|
180
214
|
|
|
181
215
|
/**
|
|
182
216
|
* 返回某个 Intent 的完整追溯链。
|
|
183
|
-
* @param {string}
|
|
217
|
+
* @param {string} versionDir
|
|
184
218
|
* @param {string} verificationsDir
|
|
185
219
|
* @param {string} philosophyDir
|
|
186
220
|
* @param {string} intentId
|
|
187
221
|
* @returns {object}
|
|
188
222
|
*/
|
|
189
|
-
export function traceIntent(
|
|
190
|
-
const intent = getIntent(
|
|
223
|
+
export function traceIntent(versionDir, verificationsDir, philosophyDir, intentId) {
|
|
224
|
+
const intent = getIntent(versionDir, intentId);
|
|
191
225
|
if (!intent) throw new Error(`Intent 不存在: ${intentId}`);
|
|
192
226
|
|
|
193
227
|
// 意图叙事
|
|
194
228
|
let narrative = null;
|
|
195
|
-
|
|
229
|
+
let narrativeError = null;
|
|
230
|
+
try { narrative = getNarrative(versionDir, intentId); }
|
|
231
|
+
catch (e) { narrativeError = e.message; /* narrative_ref 可能缺失或解析失败 */ }
|
|
196
232
|
|
|
197
233
|
// 验收契约
|
|
198
234
|
let acceptance = null;
|
|
199
|
-
|
|
235
|
+
let acceptanceError = null;
|
|
236
|
+
try { acceptance = getVerificationContract(versionDir, intentId); }
|
|
237
|
+
catch (e) { acceptanceError = e.message; /* 引用可能缺失或解析失败 */ }
|
|
200
238
|
|
|
201
239
|
// 验证历史
|
|
202
240
|
const verificationHistory = getVerificationHistory(verificationsDir, intentId);
|
|
@@ -214,7 +252,7 @@ export function traceIntent(loomDir, verificationsDir, philosophyDir, intentId)
|
|
|
214
252
|
}
|
|
215
253
|
|
|
216
254
|
// 依赖链(递归向上)
|
|
217
|
-
const { intents } = loadIntentMap(
|
|
255
|
+
const { intents } = loadIntentMap(versionDir);
|
|
218
256
|
const dependencyChain = [];
|
|
219
257
|
function walkDeps(id, depth) {
|
|
220
258
|
const node = intents[id];
|
|
@@ -229,7 +267,9 @@ export function traceIntent(loomDir, verificationsDir, philosophyDir, intentId)
|
|
|
229
267
|
return {
|
|
230
268
|
intent,
|
|
231
269
|
narrative,
|
|
270
|
+
narrative_error: narrativeError,
|
|
232
271
|
acceptance,
|
|
272
|
+
acceptance_error: acceptanceError,
|
|
233
273
|
verification_history: verificationHistory,
|
|
234
274
|
philosophy_anchors_content: philosophyContent,
|
|
235
275
|
dependency_chain: dependencyChain,
|
|
@@ -241,12 +281,12 @@ export function traceIntent(loomDir, verificationsDir, philosophyDir, intentId)
|
|
|
241
281
|
|
|
242
282
|
/**
|
|
243
283
|
* 返回依赖指定 Intent 的所有 Intent。
|
|
244
|
-
* @param {string}
|
|
284
|
+
* @param {string} versionDir
|
|
245
285
|
* @param {string} intentId
|
|
246
286
|
* @returns {string[]}
|
|
247
287
|
*/
|
|
248
|
-
export function reverseDep(
|
|
249
|
-
const { intents } = loadIntentMap(
|
|
288
|
+
export function reverseDep(versionDir, intentId) {
|
|
289
|
+
const { intents } = loadIntentMap(versionDir);
|
|
250
290
|
const result = [];
|
|
251
291
|
for (const [id, intent] of Object.entries(intents)) {
|
|
252
292
|
if (intent.depends_on?.includes(intentId)) {
|
|
@@ -261,12 +301,12 @@ export function reverseDep(loomDir, intentId) {
|
|
|
261
301
|
|
|
262
302
|
/**
|
|
263
303
|
* 返回引用指定哲学锚点的所有 Intent。
|
|
264
|
-
* @param {string}
|
|
304
|
+
* @param {string} versionDir
|
|
265
305
|
* @param {string} anchor — 如 "PRODUCT_PHILOSOPHY.md#core-belief"
|
|
266
306
|
* @returns {string[]}
|
|
267
307
|
*/
|
|
268
|
-
export function reverseRef(
|
|
269
|
-
const { intents } = loadIntentMap(
|
|
308
|
+
export function reverseRef(versionDir, anchor) {
|
|
309
|
+
const { intents } = loadIntentMap(versionDir);
|
|
270
310
|
const result = [];
|
|
271
311
|
for (const [id, intent] of Object.entries(intents)) {
|
|
272
312
|
if (intent.philosophy_anchors?.includes(anchor)) {
|
package/cli/src/guide.js
CHANGED
|
@@ -6,11 +6,12 @@ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { readCurrentPointer } from './version.js';
|
|
8
8
|
import { loadIntentMap } from './intent-map.js';
|
|
9
|
-
import { isAutoOn } from './auto.js';
|
|
9
|
+
import { isAutoOn, writeHeartbeat, needsHumanReview } from './auto.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* 检测文件是否还是模板(未填充真实内容)。
|
|
13
|
-
* MD 文件检查 <!-- LOOM_TEMPLATE -->
|
|
13
|
+
* MD 文件检查 <!-- LOOM_TEMPLATE --> 标记——但如果文件已经超过模板大小(>2KB),
|
|
14
|
+
* 认为用户已经填充了真实内容只是忘了删标记,忽略标记。
|
|
14
15
|
* JSON 文件检查 _meta._template 字段。
|
|
15
16
|
*/
|
|
16
17
|
function isTemplate(filePath) {
|
|
@@ -24,18 +25,53 @@ function isTemplate(filePath) {
|
|
|
24
25
|
return false; // 损坏文件不算模板
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
+
// MD 文件:有 LOOM_TEMPLATE 标记 且 文件较小(<2KB)才算模板。
|
|
29
|
+
// 如果文件已经 >2KB,说明用户填充了内容只是忘了删标记——忽略标记。
|
|
30
|
+
if (content.includes('<!-- LOOM_TEMPLATE -->')) {
|
|
31
|
+
return content.length < 2048;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
/**
|
|
31
37
|
* 诊断项目当前阶段。
|
|
32
38
|
* @param {string} projectDir — 项目根目录
|
|
33
|
-
* @returns {{ stage: string, stage_num: number, details: object, auto: boolean, next_action: string, next_command: string, message: string }}
|
|
39
|
+
* @returns {{ stage: string, stage_num: number, details: object, auto: boolean, next_action: string, next_command: string, message: string, needs_human_review: boolean }}
|
|
34
40
|
*/
|
|
35
41
|
export function guideProject(projectDir) {
|
|
36
42
|
const cwd = projectDir || process.cwd();
|
|
37
43
|
const loomRoot = join(cwd, '.loom');
|
|
38
44
|
const auto = isAutoOn(loomRoot);
|
|
45
|
+
const result = diagnoseStage(cwd, loomRoot, auto);
|
|
46
|
+
// 统一后处理:写心跳 + 加 AUTO 提示词 + 判断是否需要人类 review
|
|
47
|
+
if (existsSync(loomRoot)) {
|
|
48
|
+
try {
|
|
49
|
+
writeHeartbeat(loomRoot, {
|
|
50
|
+
stage: result.stage,
|
|
51
|
+
stage_num: result.stage_num,
|
|
52
|
+
next_command: result.next_command,
|
|
53
|
+
next_action: result.next_action,
|
|
54
|
+
});
|
|
55
|
+
} catch { /* 心跳写入失败不阻塞 guide */ }
|
|
56
|
+
}
|
|
57
|
+
result.needs_human_review = needsHumanReview(loomRoot, result.stage_num);
|
|
58
|
+
// 在 message 末尾加 AUTO 提示
|
|
59
|
+
if (result.stage_num > 0 && result.stage_num < 4) {
|
|
60
|
+
result.message += auto
|
|
61
|
+
? '\n\n> ⚠ 这是设计阶段(哲学/愿景/架构),即使 AUTO 模式开启也需要人类 review 后再继续。'
|
|
62
|
+
: '\n\n> ⚠ AUTO 模式关闭——每步都需要人类确认后再继续。';
|
|
63
|
+
} else if (result.stage_num >= 4 && !auto) {
|
|
64
|
+
result.message += '\n\n> ⚠ AUTO 模式关闭——执行 next_command 后等人类确认再继续。';
|
|
65
|
+
} else if (result.stage_num >= 4 && auto) {
|
|
66
|
+
result.message += '\n\n> AUTO 模式开启——直接执行 next_command,无需人类确认。';
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 内部函数:诊断阶段(不含心跳和 AUTO 提示词)。
|
|
73
|
+
*/
|
|
74
|
+
function diagnoseStage(cwd, loomRoot, auto) {
|
|
39
75
|
|
|
40
76
|
// 状态 0: 没有 .loom/
|
|
41
77
|
if (!existsSync(loomRoot)) {
|
|
@@ -137,6 +173,9 @@ export function guideProject(projectDir) {
|
|
|
137
173
|
// 状态 7: 有 blocked(优先报告)
|
|
138
174
|
if (counts.blocked > 0) {
|
|
139
175
|
const blockedIds = allIntents.filter((i) => i.status === 'blocked').map((i) => i.id);
|
|
176
|
+
const msg = auto
|
|
177
|
+
? `有 ${counts.blocked} 个 Intent 阻塞: ${blockedIds.join(', ')}。AUTO 模式下这是唯一允许停下的情况——需要人工介入解决阻塞后才能继续。`
|
|
178
|
+
: `有 ${counts.blocked} 个 Intent 阻塞: ${blockedIds.join(', ')}。需要人工介入。`;
|
|
140
179
|
return {
|
|
141
180
|
stage: 'blocked',
|
|
142
181
|
stage_num: 7,
|
|
@@ -144,7 +183,7 @@ export function guideProject(projectDir) {
|
|
|
144
183
|
auto,
|
|
145
184
|
next_action: '人工介入解决阻塞',
|
|
146
185
|
next_command: 'loom intent get ' + blockedIds[0],
|
|
147
|
-
message:
|
|
186
|
+
message: msg,
|
|
148
187
|
};
|
|
149
188
|
}
|
|
150
189
|
|
|
@@ -175,6 +214,36 @@ export function guideProject(projectDir) {
|
|
|
175
214
|
};
|
|
176
215
|
}
|
|
177
216
|
|
|
217
|
+
// 状态 5.5: 有 needs_review(收敛趟)——已完成但需要重新验证的 Intent
|
|
218
|
+
if (counts.needs_review > 0) {
|
|
219
|
+
const reviewIds = allIntents.filter((i) => i.status === 'needs_review').map((i) => i.id);
|
|
220
|
+
// 读 _meta.pass_count 收敛趟计数(最大 3 趟)
|
|
221
|
+
const passCount = intents._meta?.pass_count || 1;
|
|
222
|
+
const MAX_PASSES = 3;
|
|
223
|
+
const isOverLimit = passCount > MAX_PASSES;
|
|
224
|
+
const passMsg = ` [Pass ${passCount}/${MAX_PASSES}]`;
|
|
225
|
+
if (isOverLimit) {
|
|
226
|
+
return {
|
|
227
|
+
stage: 'cannot_converge',
|
|
228
|
+
stage_num: 7,
|
|
229
|
+
details: { version: current, counts, needs_review_ids: reviewIds, pass_count: passCount },
|
|
230
|
+
auto,
|
|
231
|
+
next_action: '收敛失败——超过最大趟数,需 Architect 介入',
|
|
232
|
+
next_command: 'loom intent update ' + reviewIds[0] + ' --status blocked',
|
|
233
|
+
message: `当前版本 ${current}:收敛失败,已超过最大 ${MAX_PASSES} 趟限制(当前 Pass ${passCount})。${counts.needs_review} 个 Intent 仍需重新验证(${reviewIds.join(', ')})。这是系统性问题——需 Architect 介入重新设计。`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
stage: 'converging',
|
|
238
|
+
stage_num: 5.5,
|
|
239
|
+
details: { version: current, counts, needs_review_ids: reviewIds, pass_count: passCount },
|
|
240
|
+
auto,
|
|
241
|
+
next_action: `进入收敛趟 Pass ${passCount}——重验 needs_review 的 Intent`,
|
|
242
|
+
next_command: 'loom intent update ' + reviewIds[0] + ' --status in_progress',
|
|
243
|
+
message: `当前版本 ${current}${passMsg}:${counts.needs_review} 个 Intent 需要重新验证(${reviewIds.join(', ')})。这是不动点收敛的第 ${passCount} 趟——重验这些 Intent,通过则 completed,偏离则修正。一趟无新 needs_review 即收敛达成。最大 ${MAX_PASSES} 趟,超过判定为系统性问题。`,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
178
247
|
// 状态 4: 有 pending,进入 Intent Loop
|
|
179
248
|
if (counts.pending > 0) {
|
|
180
249
|
return {
|