@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.
@@ -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
+ - **文档开销不超过开发开销** — 小项目可以粗粒度,不必教条
@@ -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} loomDir — .loom/v{N} 目录(可选,weaver 不需要)
26
+ * @param {string} versionDir — .loom/v{N} 目录(可选,weaver 不需要)
30
27
  * @returns {string} 激活提示词
31
28
  */
32
- export function activateRole(role, loomDir) {
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
- const baselineFile = join(loomRoot, 'meta/BASELINE.md');
49
- parts.push('\n---\n\n## 强制加载:BASELINE\n\n' + readFileSync(baselineFile, 'utf-8'));
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. 项目特定底线(如果有 loomDir
52
- if (loomDir) {
53
- const projectBaseline = join(loomDir, '00_PHILOSOPHY/PROJECT_BASELINE.md');
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
- // 影响 guide 输出语气和 Agent 行为:on 时直接跑,off 时等确认
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
- return { on: true, since };
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
  }
@@ -13,13 +13,13 @@ import { getVerificationHistory, getPendingVerifications, getVerificationContrac
13
13
 
14
14
  /**
15
15
  * 项目健康检查。
16
- * @param {string} loomDir — 当前版本目录
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(loomDir, verificationsDir, philosophyDir) {
22
- const { intents, topo_order } = loadIntentMap(loomDir);
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(loomDir, '04_INTENT_MAP.json'))
71
- ? statSync(join(loomDir, '04_INTENT_MAP.json')).mtimeMs
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} loomDir
180
+ * @param {string} versionDir
147
181
  * @param {string} verificationsDir
148
182
  * @param {string} philosophyDir
149
183
  * @returns {object}
150
184
  */
151
- export function contextSummary(loomDir, verificationsDir, philosophyDir) {
152
- const status = getStatus(loomDir);
153
- const next = getNextIntent(loomDir);
154
- const pending = getPendingVerifications(loomDir, verificationsDir);
155
- const { issues } = doctor(loomDir, verificationsDir, philosophyDir);
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} loomDir
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(loomDir, verificationsDir, philosophyDir, intentId) {
190
- const intent = getIntent(loomDir, intentId);
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
- try { narrative = getNarrative(loomDir, intentId); } catch { /* narrative_ref 可能缺失 */ }
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
- try { acceptance = getVerificationContract(loomDir, intentId); } catch { /* 可能缺失 */ }
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(loomDir);
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} loomDir
284
+ * @param {string} versionDir
245
285
  * @param {string} intentId
246
286
  * @returns {string[]}
247
287
  */
248
- export function reverseDep(loomDir, intentId) {
249
- const { intents } = loadIntentMap(loomDir);
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} loomDir
304
+ * @param {string} versionDir
265
305
  * @param {string} anchor — 如 "PRODUCT_PHILOSOPHY.md#core-belief"
266
306
  * @returns {string[]}
267
307
  */
268
- export function reverseRef(loomDir, anchor) {
269
- const { intents } = loadIntentMap(loomDir);
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
- return content.includes('<!-- LOOM_TEMPLATE -->');
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: `有 ${counts.blocked} 个 Intent 阻塞: ${blockedIds.join(', ')}。需要人工介入。`,
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 {