@haaaiawd/loom 0.1.0 → 0.8.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 +182 -85
- package/cli/help/concepts.md +72 -0
- package/cli/help/doctor.md +121 -0
- package/cli/help/loop.md +135 -0
- package/cli/help/preview.md +59 -0
- package/cli/help/version.md +60 -0
- package/cli/help/workflow.md +100 -0
- package/cli/src/activate.js +21 -12
- package/cli/src/auto.js +54 -5
- package/cli/src/diagnostics.js +229 -48
- package/cli/src/guide.js +83 -12
- 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 +226 -69
- package/cli/src/preview-prompt.md +1 -0
- package/cli/src/preview.js +67 -10
- package/cli/src/shared/md-utils.js +125 -0
- package/cli/src/shared/paths.js +73 -0
- package/cli/src/verify.js +70 -74
- package/cli/src/version.js +1 -1
- 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/INTENT_LOOP.md +159 -18
- package/meta/PHILOSOPHY_WEAVER.md +104 -50
- package/package.json +5 -4
- 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
package/cli/src/diagnostics.js
CHANGED
|
@@ -2,25 +2,123 @@
|
|
|
2
2
|
// 提供 doctor / context / trace / reverse-dep / reverse-ref 五个聚合命令。
|
|
3
3
|
// 全部是只读的数据聚合,不做决策、不修改文件。
|
|
4
4
|
|
|
5
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
6
|
-
import { join } from 'node:path';
|
|
7
|
-
import { loadIntentMap, getStatus, getNextIntent, getNarrative, getIntent } from './intent-map.js';
|
|
8
|
-
import { getPhilosophy } from './philosophy.js';
|
|
9
|
-
import { getVerificationHistory, getPendingVerifications, getVerificationContract } from './verify.js';
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { loadIntentMap, getStatus, getNextIntent, getNarrative, getIntent } from './intent-map.js';
|
|
8
|
+
import { getPhilosophy, validateInspirationSources, validatePartDecomposition } from './philosophy.js';
|
|
9
|
+
import { getVerificationHistory, getPendingVerifications, getVerificationContract } from './verify.js';
|
|
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
|
+
if (raw._meta?._template === true) {
|
|
61
|
+
issues.push({ id: 'intent_map', type: 'intent_map_template', severity: 'high', msg: 'Intent Map 仍是模板,尚未由 Architect 产出真实意图图' });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
validMap = loadIntentMap(versionDir);
|
|
66
|
+
valid = true;
|
|
67
|
+
} catch (e) {
|
|
68
|
+
valid = false;
|
|
69
|
+
issues.push({ id: 'intent_map', type: 'intent_map_invalid', severity: 'fatal', msg: e.message });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { raw, valid, issues, validMap };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getIntentVerificationMethod(intent) {
|
|
76
|
+
return intent.verification_method || intent._optional?.verification_method || null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeVerificationCommand(command) {
|
|
80
|
+
return String(command || '')
|
|
81
|
+
.replace(/^\s*run\s+/i, '')
|
|
82
|
+
.replace(/^\s*exec\s+/i, '')
|
|
83
|
+
.replace(/\s+/g, ' ')
|
|
84
|
+
.trim();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function commandCoversMethod(actualCommand, expectedMethod) {
|
|
88
|
+
const actual = normalizeVerificationCommand(actualCommand);
|
|
89
|
+
const expected = normalizeVerificationCommand(expectedMethod);
|
|
90
|
+
if (!actual || !expected) return false;
|
|
91
|
+
|
|
92
|
+
return expected.split('&&').every((part) => {
|
|
93
|
+
const expectedPart = normalizeVerificationCommand(part);
|
|
94
|
+
if (!expectedPart) return true;
|
|
95
|
+
if (actual.includes(expectedPart)) return true;
|
|
96
|
+
// npm test is an acceptable broader reproduction for node --test based methods.
|
|
97
|
+
if (expectedPart.startsWith('node --test') && actual.includes('npm test')) return true;
|
|
98
|
+
return false;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
10
101
|
|
|
11
102
|
// ─── doctor ────────────────────────────────────────────
|
|
12
103
|
// 全面健康检查:一致性 + 孤儿引用 + 循环依赖 + 僵尸 Intent
|
|
13
104
|
|
|
14
105
|
/**
|
|
15
106
|
* 项目健康检查。
|
|
16
|
-
* @param {string}
|
|
107
|
+
* @param {string} versionDir — 当前版本目录
|
|
17
108
|
* @param {string} verificationsDir — 验证记录目录
|
|
18
109
|
* @param {string} philosophyDir — 哲学目录
|
|
19
110
|
* @returns {{ issues: object[], summary: object }}
|
|
20
111
|
*/
|
|
21
|
-
export function doctor(
|
|
22
|
-
const
|
|
23
|
-
const issues = [];
|
|
112
|
+
export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
113
|
+
const mapState = intentMapDiagnostics(versionDir);
|
|
114
|
+
const issues = [...mapState.issues];
|
|
115
|
+
|
|
116
|
+
if (!mapState.validMap) {
|
|
117
|
+
appendPhilosophyDiagnostics(issues, philosophyDir);
|
|
118
|
+
return { issues, summary: summarizeIssues(issues) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const { intents } = mapState.validMap;
|
|
24
122
|
|
|
25
123
|
// 1. 状态一致性:in_progress/completed 但无验证记录
|
|
26
124
|
for (const [id, intent] of Object.entries(intents)) {
|
|
@@ -67,8 +165,8 @@ export function doctor(loomDir, verificationsDir, philosophyDir) {
|
|
|
67
165
|
for (const [id, intent] of Object.entries(intents)) {
|
|
68
166
|
if (intent.status !== 'in_progress' && intent.status !== 'blocked') continue;
|
|
69
167
|
const recordPath = join(verificationsDir, `${id}.json`);
|
|
70
|
-
let lastActivity = existsSync(join(
|
|
71
|
-
? statSync(join(
|
|
168
|
+
let lastActivity = existsSync(join(versionDir, '04_INTENT_MAP.json'))
|
|
169
|
+
? statSync(join(versionDir, '04_INTENT_MAP.json')).mtimeMs
|
|
72
170
|
: now;
|
|
73
171
|
if (existsSync(recordPath)) {
|
|
74
172
|
lastActivity = Math.max(lastActivity, statSync(recordPath).mtimeMs);
|
|
@@ -90,16 +188,91 @@ export function doctor(loomDir, verificationsDir, philosophyDir) {
|
|
|
90
188
|
}
|
|
91
189
|
}
|
|
92
190
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
191
|
+
// 7. 验证脚本可执行性:检查 verification_method 引用的脚本/目录是否存在
|
|
192
|
+
const projectDir = join(versionDir, '..', '..');
|
|
193
|
+
for (const [id, intent] of Object.entries(intents)) {
|
|
194
|
+
const vm = getIntentVerificationMethod(intent);
|
|
195
|
+
if (!vm) continue;
|
|
196
|
+
// 检测 npm test 引用
|
|
197
|
+
if (vm.includes('npm test') || vm.includes('npm run test')) {
|
|
198
|
+
const pkgPath = join(projectDir, 'package.json');
|
|
199
|
+
if (!existsSync(pkgPath)) {
|
|
200
|
+
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 npm test 但项目根没有 package.json` });
|
|
201
|
+
} else {
|
|
202
|
+
try {
|
|
203
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
204
|
+
const testScript = pkg.scripts && pkg.scripts.test;
|
|
205
|
+
if (!testScript) {
|
|
206
|
+
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 npm test 但 package.json 没有 test 脚本` });
|
|
207
|
+
} else {
|
|
208
|
+
// 检查 test 脚本引用的目录/文件是否存在
|
|
209
|
+
// 常见模式: "node --test test/" / "mocha test/" / "jest" 等
|
|
210
|
+
const testDirMatch = testScript.match(/(?:--test|test)\s+(\S+)/);
|
|
211
|
+
if (testDirMatch) {
|
|
212
|
+
const testTarget = testDirMatch[1].replace(/['"]/g, '');
|
|
213
|
+
if (!existsSync(join(projectDir, testTarget))) {
|
|
214
|
+
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 npm test 但 test 脚本引用的 ${testTarget} 不存在` });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
// package.json 解析失败,不报——不是 doctor 的职责
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 8. completed Intent 的 verification_method 必须被最新验证记录覆盖,防止契约命令漂移。
|
|
226
|
+
for (const [id, intent] of Object.entries(intents)) {
|
|
227
|
+
if (intent.status !== 'completed') continue;
|
|
228
|
+
const method = getIntentVerificationMethod(intent);
|
|
229
|
+
if (!method) continue;
|
|
230
|
+
|
|
231
|
+
const expected = normalizeVerificationCommand(method);
|
|
232
|
+
if (!expected || expected === 'human_review') continue;
|
|
233
|
+
|
|
234
|
+
const history = getVerificationHistory(verificationsDir, id);
|
|
235
|
+
const latest = history?.records?.[history.records.length - 1];
|
|
236
|
+
const actual = normalizeVerificationCommand(latest?.reproduction_command);
|
|
237
|
+
|
|
238
|
+
if (!latest) {
|
|
239
|
+
issues.push({ id, type: 'verification_method_unverified', severity: 'high', msg: `${id} 声明了 verification_method 但没有验证记录覆盖: ${method}` });
|
|
240
|
+
} else if (!actual) {
|
|
241
|
+
issues.push({ id, type: 'verification_method_unverified', severity: 'high', msg: `${id} 最新验证记录缺少 reproduction_command,无法复现 verification_method: ${method}` });
|
|
242
|
+
} else if (!commandCoversMethod(actual, expected)) {
|
|
243
|
+
issues.push({ id, type: 'verification_method_drift', severity: 'high', msg: `${id} verification_method 未被最新 reproduction_command 覆盖。method="${method}" reproduction_command="${latest.reproduction_command}"` });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
appendPhilosophyDiagnostics(issues, philosophyDir);
|
|
248
|
+
return { issues, summary: summarizeIssues(issues) };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function appendPhilosophyDiagnostics(issues, philosophyDir) {
|
|
252
|
+
// 哲学灵感来源校验(防止 Weaver 从训练数据"背"几个名字就交差)
|
|
253
|
+
if (!existsSync(philosophyDir)) return;
|
|
254
|
+
|
|
255
|
+
const inspirationCheck = validateInspirationSources(philosophyDir);
|
|
256
|
+
for (const issue of inspirationCheck.issues) {
|
|
257
|
+
issues.push({ id: 'philosophy', type: 'inspiration_source', severity: issue.severity, msg: issue.msg });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 实现部分拆解校验(防止 Weaver 跳过拆解步骤)
|
|
261
|
+
const decompositionCheck = validatePartDecomposition(philosophyDir);
|
|
262
|
+
for (const issue of decompositionCheck.issues) {
|
|
263
|
+
issues.push({ id: 'philosophy', type: 'part_decomposition', severity: issue.severity, msg: issue.msg });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function summarizeIssues(issues) {
|
|
268
|
+
return {
|
|
269
|
+
total_issues: issues.length,
|
|
270
|
+
fatal: issues.filter((i) => i.severity === 'fatal').length,
|
|
271
|
+
high: issues.filter((i) => i.severity === 'high').length,
|
|
272
|
+
medium: issues.filter((i) => i.severity === 'medium').length,
|
|
273
|
+
healthy: issues.length === 0,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
103
276
|
|
|
104
277
|
/**
|
|
105
278
|
* DFS 检测循环依赖。
|
|
@@ -143,29 +316,31 @@ function detectCycles(intents) {
|
|
|
143
316
|
|
|
144
317
|
/**
|
|
145
318
|
* 项目上下文摘要——Agent 重启后一条命令获取"我在哪"。
|
|
146
|
-
* @param {string}
|
|
319
|
+
* @param {string} versionDir
|
|
147
320
|
* @param {string} verificationsDir
|
|
148
321
|
* @param {string} philosophyDir
|
|
149
322
|
* @returns {object}
|
|
150
323
|
*/
|
|
151
|
-
export function contextSummary(
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
const
|
|
324
|
+
export function contextSummary(versionDir, verificationsDir, philosophyDir) {
|
|
325
|
+
const mapState = intentMapDiagnostics(versionDir);
|
|
326
|
+
const status = mapState.valid ? getStatus(versionDir) : summarizeRawIntentMap(mapState.raw);
|
|
327
|
+
const next = mapState.valid ? getNextIntent(versionDir) : null;
|
|
328
|
+
const pending = mapState.valid ? getPendingVerifications(versionDir, verificationsDir) : [];
|
|
329
|
+
const { issues } = doctor(versionDir, verificationsDir, philosophyDir);
|
|
156
330
|
|
|
157
331
|
const risks = [];
|
|
158
332
|
const fatalCount = issues.filter((i) => i.severity === 'fatal').length;
|
|
159
333
|
const highCount = issues.filter((i) => i.severity === 'high').length;
|
|
160
|
-
if (fatalCount > 0) risks.push(`${fatalCount}
|
|
161
|
-
if (highCount > 0) risks.push(`${highCount} 个高严重度问题(状态不一致/孤儿引用)`);
|
|
162
|
-
if (status.counts.blocked > 0) risks.push(`${status.counts.blocked} 个阻塞 Intent`);
|
|
163
|
-
|
|
164
|
-
return {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
334
|
+
if (fatalCount > 0) risks.push(`${fatalCount} 个致命问题(Intent Map 损坏/循环依赖)`);
|
|
335
|
+
if (highCount > 0) risks.push(`${highCount} 个高严重度问题(状态不一致/孤儿引用)`);
|
|
336
|
+
if (status.counts.blocked > 0) risks.push(`${status.counts.blocked} 个阻塞 Intent`);
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
intent_map_valid: mapState.valid === true,
|
|
340
|
+
progress: {
|
|
341
|
+
completed: status.counts.completed,
|
|
342
|
+
total: status.counts.total,
|
|
343
|
+
rate: `${status.counts.completed}/${status.counts.total}`,
|
|
169
344
|
},
|
|
170
345
|
next_intent: next ? next.id : null,
|
|
171
346
|
pending_verifications: pending,
|
|
@@ -180,23 +355,27 @@ export function contextSummary(loomDir, verificationsDir, philosophyDir) {
|
|
|
180
355
|
|
|
181
356
|
/**
|
|
182
357
|
* 返回某个 Intent 的完整追溯链。
|
|
183
|
-
* @param {string}
|
|
358
|
+
* @param {string} versionDir
|
|
184
359
|
* @param {string} verificationsDir
|
|
185
360
|
* @param {string} philosophyDir
|
|
186
361
|
* @param {string} intentId
|
|
187
362
|
* @returns {object}
|
|
188
363
|
*/
|
|
189
|
-
export function traceIntent(
|
|
190
|
-
const intent = getIntent(
|
|
364
|
+
export function traceIntent(versionDir, verificationsDir, philosophyDir, intentId) {
|
|
365
|
+
const intent = getIntent(versionDir, intentId);
|
|
191
366
|
if (!intent) throw new Error(`Intent 不存在: ${intentId}`);
|
|
192
367
|
|
|
193
368
|
// 意图叙事
|
|
194
369
|
let narrative = null;
|
|
195
|
-
|
|
370
|
+
let narrativeError = null;
|
|
371
|
+
try { narrative = getNarrative(versionDir, intentId); }
|
|
372
|
+
catch (e) { narrativeError = e.message; /* narrative_ref 可能缺失或解析失败 */ }
|
|
196
373
|
|
|
197
374
|
// 验收契约
|
|
198
375
|
let acceptance = null;
|
|
199
|
-
|
|
376
|
+
let acceptanceError = null;
|
|
377
|
+
try { acceptance = getVerificationContract(versionDir, intentId); }
|
|
378
|
+
catch (e) { acceptanceError = e.message; /* 引用可能缺失或解析失败 */ }
|
|
200
379
|
|
|
201
380
|
// 验证历史
|
|
202
381
|
const verificationHistory = getVerificationHistory(verificationsDir, intentId);
|
|
@@ -214,7 +393,7 @@ export function traceIntent(loomDir, verificationsDir, philosophyDir, intentId)
|
|
|
214
393
|
}
|
|
215
394
|
|
|
216
395
|
// 依赖链(递归向上)
|
|
217
|
-
const { intents } = loadIntentMap(
|
|
396
|
+
const { intents } = loadIntentMap(versionDir);
|
|
218
397
|
const dependencyChain = [];
|
|
219
398
|
function walkDeps(id, depth) {
|
|
220
399
|
const node = intents[id];
|
|
@@ -229,7 +408,9 @@ export function traceIntent(loomDir, verificationsDir, philosophyDir, intentId)
|
|
|
229
408
|
return {
|
|
230
409
|
intent,
|
|
231
410
|
narrative,
|
|
411
|
+
narrative_error: narrativeError,
|
|
232
412
|
acceptance,
|
|
413
|
+
acceptance_error: acceptanceError,
|
|
233
414
|
verification_history: verificationHistory,
|
|
234
415
|
philosophy_anchors_content: philosophyContent,
|
|
235
416
|
dependency_chain: dependencyChain,
|
|
@@ -241,12 +422,12 @@ export function traceIntent(loomDir, verificationsDir, philosophyDir, intentId)
|
|
|
241
422
|
|
|
242
423
|
/**
|
|
243
424
|
* 返回依赖指定 Intent 的所有 Intent。
|
|
244
|
-
* @param {string}
|
|
425
|
+
* @param {string} versionDir
|
|
245
426
|
* @param {string} intentId
|
|
246
427
|
* @returns {string[]}
|
|
247
428
|
*/
|
|
248
|
-
export function reverseDep(
|
|
249
|
-
const { intents } = loadIntentMap(
|
|
429
|
+
export function reverseDep(versionDir, intentId) {
|
|
430
|
+
const { intents } = loadIntentMap(versionDir);
|
|
250
431
|
const result = [];
|
|
251
432
|
for (const [id, intent] of Object.entries(intents)) {
|
|
252
433
|
if (intent.depends_on?.includes(intentId)) {
|
|
@@ -261,12 +442,12 @@ export function reverseDep(loomDir, intentId) {
|
|
|
261
442
|
|
|
262
443
|
/**
|
|
263
444
|
* 返回引用指定哲学锚点的所有 Intent。
|
|
264
|
-
* @param {string}
|
|
445
|
+
* @param {string} versionDir
|
|
265
446
|
* @param {string} anchor — 如 "PRODUCT_PHILOSOPHY.md#core-belief"
|
|
266
447
|
* @returns {string[]}
|
|
267
448
|
*/
|
|
268
|
-
export function reverseRef(
|
|
269
|
-
const { intents } = loadIntentMap(
|
|
449
|
+
export function reverseRef(versionDir, anchor) {
|
|
450
|
+
const { intents } = loadIntentMap(versionDir);
|
|
270
451
|
const result = [];
|
|
271
452
|
for (const [id, intent] of Object.entries(intents)) {
|
|
272
453
|
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 }}
|
|
40
|
+
*/
|
|
41
|
+
export function guideProject(projectDir, options = {}) {
|
|
42
|
+
const cwd = projectDir || process.cwd();
|
|
43
|
+
const loomRoot = join(cwd, '.loom');
|
|
44
|
+
const auto = isAutoOn(loomRoot);
|
|
45
|
+
const result = diagnoseStage(cwd, loomRoot, auto);
|
|
46
|
+
// 统一后处理:写心跳 + 加 AUTO 提示词 + 判断是否需要人类 review
|
|
47
|
+
if (existsSync(loomRoot) && !options.dryRun) {
|
|
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 提示词)。
|
|
34
73
|
*/
|
|
35
|
-
|
|
36
|
-
const cwd = projectDir || process.cwd();
|
|
37
|
-
const loomRoot = join(cwd, '.loom');
|
|
38
|
-
const auto = isAutoOn(loomRoot);
|
|
74
|
+
function diagnoseStage(cwd, loomRoot, auto) {
|
|
39
75
|
|
|
40
76
|
// 状态 0: 没有 .loom/
|
|
41
77
|
if (!existsSync(loomRoot)) {
|
|
@@ -109,9 +145,11 @@ export function guideProject(projectDir) {
|
|
|
109
145
|
}
|
|
110
146
|
|
|
111
147
|
// 状态 4-7: Intent Map 已设计,根据 Intent 状态判断
|
|
112
|
-
let
|
|
113
|
-
|
|
114
|
-
|
|
148
|
+
let intentMap;
|
|
149
|
+
let intents;
|
|
150
|
+
try {
|
|
151
|
+
intentMap = loadIntentMap(versionDir);
|
|
152
|
+
intents = intentMap.intents;
|
|
115
153
|
} catch (e) {
|
|
116
154
|
return {
|
|
117
155
|
stage: 'intent_map_broken',
|
|
@@ -137,6 +175,9 @@ export function guideProject(projectDir) {
|
|
|
137
175
|
// 状态 7: 有 blocked(优先报告)
|
|
138
176
|
if (counts.blocked > 0) {
|
|
139
177
|
const blockedIds = allIntents.filter((i) => i.status === 'blocked').map((i) => i.id);
|
|
178
|
+
const msg = auto
|
|
179
|
+
? `有 ${counts.blocked} 个 Intent 阻塞: ${blockedIds.join(', ')}。AUTO 模式下这是唯一允许停下的情况——需要人工介入解决阻塞后才能继续。`
|
|
180
|
+
: `有 ${counts.blocked} 个 Intent 阻塞: ${blockedIds.join(', ')}。需要人工介入。`;
|
|
140
181
|
return {
|
|
141
182
|
stage: 'blocked',
|
|
142
183
|
stage_num: 7,
|
|
@@ -144,7 +185,7 @@ export function guideProject(projectDir) {
|
|
|
144
185
|
auto,
|
|
145
186
|
next_action: '人工介入解决阻塞',
|
|
146
187
|
next_command: 'loom intent get ' + blockedIds[0],
|
|
147
|
-
message:
|
|
188
|
+
message: msg,
|
|
148
189
|
};
|
|
149
190
|
}
|
|
150
191
|
|
|
@@ -175,6 +216,36 @@ export function guideProject(projectDir) {
|
|
|
175
216
|
};
|
|
176
217
|
}
|
|
177
218
|
|
|
219
|
+
// 状态 5.5: 有 needs_review(收敛趟)——已完成但需要重新验证的 Intent
|
|
220
|
+
if (counts.needs_review > 0) {
|
|
221
|
+
const reviewIds = allIntents.filter((i) => i.status === 'needs_review').map((i) => i.id);
|
|
222
|
+
// 读 _meta.pass_count 收敛趟计数(最大 3 趟)
|
|
223
|
+
const passCount = intentMap._meta?.pass_count || 1;
|
|
224
|
+
const MAX_PASSES = 3;
|
|
225
|
+
const isOverLimit = passCount > MAX_PASSES;
|
|
226
|
+
const passMsg = ` [Pass ${passCount}/${MAX_PASSES}]`;
|
|
227
|
+
if (isOverLimit) {
|
|
228
|
+
return {
|
|
229
|
+
stage: 'cannot_converge',
|
|
230
|
+
stage_num: 7,
|
|
231
|
+
details: { version: current, counts, needs_review_ids: reviewIds, pass_count: passCount },
|
|
232
|
+
auto,
|
|
233
|
+
next_action: '收敛失败——超过最大趟数,需 Architect 介入',
|
|
234
|
+
next_command: 'loom intent update ' + reviewIds[0] + ' --status blocked',
|
|
235
|
+
message: `当前版本 ${current}:收敛失败,已超过最大 ${MAX_PASSES} 趟限制(当前 Pass ${passCount})。${counts.needs_review} 个 Intent 仍需重新验证(${reviewIds.join(', ')})。这是系统性问题——需 Architect 介入重新设计。`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
stage: 'converging',
|
|
240
|
+
stage_num: 5.5,
|
|
241
|
+
details: { version: current, counts, needs_review_ids: reviewIds, pass_count: passCount },
|
|
242
|
+
auto,
|
|
243
|
+
next_action: `进入收敛趟 Pass ${passCount}——重验 needs_review 的 Intent`,
|
|
244
|
+
next_command: 'loom intent update ' + reviewIds[0] + ' --status in_progress',
|
|
245
|
+
message: `当前版本 ${current}${passMsg}:${counts.needs_review} 个 Intent 需要重新验证(${reviewIds.join(', ')})。这是不动点收敛的第 ${passCount} 趟——重验这些 Intent,通过则 completed,偏离则修正。一趟无新 needs_review 即收敛达成。最大 ${MAX_PASSES} 趟,超过判定为系统性问题。`,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
178
249
|
// 状态 4: 有 pending,进入 Intent Loop
|
|
179
250
|
if (counts.pending > 0) {
|
|
180
251
|
return {
|