@haaaiawd/loom 0.9.0 → 0.10.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 +190 -87
- package/cli/src/auto.js +41 -18
- package/cli/src/diagnostics.js +90 -14
- package/cli/src/guide.js +92 -25
- package/cli/src/philosophy.js +4 -2
- package/cli/src/verify.js +38 -8
- package/package.json +1 -1
package/cli/bin/loom.js
CHANGED
|
@@ -12,15 +12,15 @@ 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
14
|
import { getPhilosophy, listPhilosophyFiles, validateInspirationSources, validatePartDecomposition } from '../src/philosophy.js';
|
|
15
|
-
import { writeVerification, getVerificationHistory, getPendingVerifications, listVerifications, getVerificationContract } from '../src/verify.js';
|
|
15
|
+
import { writeVerification, createQuickVerification, getVerificationHistory, getPendingVerifications, listVerifications, getVerificationContract } from '../src/verify.js';
|
|
16
16
|
import { initProject } from '../src/init.js';
|
|
17
17
|
import { activateRole } from '../src/activate.js';
|
|
18
18
|
import { listVersions, newVersion, useVersion, diffVersions } from '../src/version.js';
|
|
19
19
|
import { doctor, contextSummary, traceIntent, reverseDep, reverseRef } from '../src/diagnostics.js';
|
|
20
20
|
import { getHelpTopic, listHelpTopics } from '../src/help.js';
|
|
21
21
|
import { guideProject } from '../src/guide.js';
|
|
22
|
-
import { isAutoOn, autoOn, autoOff, autoStatus } from '../src/auto.js';
|
|
23
|
-
import { generatePreviewPrompt, getPreviewStatus } from '../src/preview.js';
|
|
22
|
+
import { isAutoOn, autoOn, autoOff, autoStatus, getAutoMode } from '../src/auto.js';
|
|
23
|
+
import { generatePreviewPrompt, getPreviewStatus } from '../src/preview.js';
|
|
24
24
|
|
|
25
25
|
// ─── 路径解析 ──────────────────────────────────────────
|
|
26
26
|
// findLoomRoot / findVersionDir / readCurrentPointer 已提取到 shared/paths.js
|
|
@@ -126,8 +126,37 @@ try {
|
|
|
126
126
|
console.log(`${id} status 已更新为 ${newStatus}`);
|
|
127
127
|
break;
|
|
128
128
|
}
|
|
129
|
+
case 'done': {
|
|
130
|
+
// loom intent done <id> — 自动走 pending→in_progress→completed,检查验证记录
|
|
131
|
+
const id = rest[0];
|
|
132
|
+
if (!id) die('用法: loom intent done <id>');
|
|
133
|
+
const verificationsDir = getVerificationsDir(versionDir);
|
|
134
|
+
// 检查有没有验证记录
|
|
135
|
+
const history = getVerificationHistory(verificationsDir, id);
|
|
136
|
+
if (!history || history.records.length === 0) {
|
|
137
|
+
die(`${id} 没有验证记录。先跑: loom verify pass ${id} --summary "..."`);
|
|
138
|
+
}
|
|
139
|
+
// 检查最新验证记录是否 passed
|
|
140
|
+
const latest = history.records[history.records.length - 1];
|
|
141
|
+
if (latest.verdict !== 'passed') {
|
|
142
|
+
die(`${id} 最新验证记录是 ${latest.verdict}(非 passed)。只有 passed 的 Intent 才能 done。`);
|
|
143
|
+
}
|
|
144
|
+
// 获取当前状态,自动走两步
|
|
145
|
+
const intent = getIntent(versionDir, id);
|
|
146
|
+
const currentStatus = intent.status;
|
|
147
|
+
if (currentStatus === 'completed') {
|
|
148
|
+
console.log(`${id} 已经是 completed,无需操作`);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
if (currentStatus === 'pending') {
|
|
152
|
+
updateIntentStatus(versionDir, id, 'in_progress');
|
|
153
|
+
}
|
|
154
|
+
updateIntentStatus(versionDir, id, 'completed');
|
|
155
|
+
console.log(`${id} 已完成(${currentStatus} → completed)`);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
129
158
|
default:
|
|
130
|
-
die(`未知子命令: intent ${sub}\n用法: loom intent [next|status|graph|get <id>|narrative <id>|validate|trace <id>|reverse-dep <id>|reverse-ref <anchor>|update <id> --status
|
|
159
|
+
die(`未知子命令: intent ${sub}\n用法: loom intent [next|status|graph|get <id>|narrative <id>|validate|trace <id>|reverse-dep <id>|reverse-ref <anchor>|update <id> --status <...>|done <id>]`);
|
|
131
160
|
}
|
|
132
161
|
break;
|
|
133
162
|
}
|
|
@@ -135,11 +164,10 @@ try {
|
|
|
135
164
|
case 'init': {
|
|
136
165
|
const result = initProject(cwd());
|
|
137
166
|
console.log('LOOM 项目已初始化');
|
|
138
|
-
console.log(`
|
|
139
|
-
for (const
|
|
140
|
-
if (result.skipped.length) {
|
|
141
|
-
console.log(
|
|
142
|
-
for (const s of result.skipped) console.log(` - ${s}`);
|
|
167
|
+
for (const c of result.created) console.log(` [created] ${c}`);
|
|
168
|
+
for (const s of result.skipped) console.log(` [skipped] ${s} (already exists)`);
|
|
169
|
+
if (result.created.length === 0 && result.skipped.length > 0) {
|
|
170
|
+
console.log(' 所有文件已存在,无需操作。');
|
|
143
171
|
}
|
|
144
172
|
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
145
173
|
console.log('To Agent: 运行 loom guide 诊断当前阶段,按引导执行');
|
|
@@ -187,6 +215,17 @@ try {
|
|
|
187
215
|
const allIssues = [...inspiration.issues, ...decomposition.issues];
|
|
188
216
|
const allPassed = inspiration.passed && decomposition.passed;
|
|
189
217
|
|
|
218
|
+
// --json: 结构化输出,供 Agent 程序化消费
|
|
219
|
+
if (argv.includes('--json')) {
|
|
220
|
+
output({
|
|
221
|
+
passed: allPassed,
|
|
222
|
+
issues: allIssues,
|
|
223
|
+
sources: inspiration.sources.map(({ file, sources }) => ({ file, count: sources.length })),
|
|
224
|
+
parts: decomposition.parts,
|
|
225
|
+
});
|
|
226
|
+
exit(allPassed ? 0 : 1);
|
|
227
|
+
}
|
|
228
|
+
|
|
190
229
|
if (allPassed) {
|
|
191
230
|
console.log('✓ 哲学文档校验通过');
|
|
192
231
|
console.log(' 灵感来源:');
|
|
@@ -271,8 +310,34 @@ try {
|
|
|
271
310
|
}
|
|
272
311
|
break;
|
|
273
312
|
}
|
|
313
|
+
case 'pass':
|
|
314
|
+
case 'fail': {
|
|
315
|
+
// loom verify pass <id> --summary "..." [--reproduction-command "..."]
|
|
316
|
+
// loom verify fail <id> --summary "..." [--deviation "..."] [--reproduction-command "..."]
|
|
317
|
+
const id = rest[0];
|
|
318
|
+
if (!id) die(`用法: loom verify ${sub} <id> --summary "..." [--reproduction-command "..."]${sub === 'fail' ? ' [--deviation "..."]' : ''}`);
|
|
319
|
+
const summaryIdx = argv.indexOf('--summary');
|
|
320
|
+
const reproIdx = argv.indexOf('--reproduction-command');
|
|
321
|
+
const deviationIdx = argv.indexOf('--deviation');
|
|
322
|
+
const summary = summaryIdx !== -1 ? argv[summaryIdx + 1] : null;
|
|
323
|
+
if (!summary) die(`缺少 --summary: loom verify ${sub} ${id} --summary "..."`);
|
|
324
|
+
const extras = {};
|
|
325
|
+
if (reproIdx !== -1 && argv[reproIdx + 1]) extras.reproduction_command = argv[reproIdx + 1];
|
|
326
|
+
if (sub === 'fail' && deviationIdx !== -1 && argv[deviationIdx + 1]) extras.deviation_detail = argv[deviationIdx + 1];
|
|
327
|
+
const verdict = sub === 'pass' ? 'passed' : 'deviated';
|
|
328
|
+
const result = createQuickVerification(verificationsDir, id, verdict, summary, extras);
|
|
329
|
+
console.log(`验证记录已写入: ${result.filePath}`);
|
|
330
|
+
console.log(` 轮次: ${result.round}, verdict: ${verdict}`);
|
|
331
|
+
if (verdict === 'deviated') {
|
|
332
|
+
console.log(` deviated 累计: ${result.deviated_count} 轮`);
|
|
333
|
+
if (result.should_escalate) {
|
|
334
|
+
console.log(` ⚠ 达到 3 轮上限,应升级为 blocked——Keeper 应执行: loom intent update ${id} --status blocked`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
274
339
|
default:
|
|
275
|
-
die(`未知子命令: verify ${sub}\n用法: loom verify [contract <id>|history <id>|pending|list|write --json-file <path>|--json <string>]`);
|
|
340
|
+
die(`未知子命令: verify ${sub}\n用法: loom verify [contract <id>|history <id>|pending|list|write --json-file <path>|--json <string>|pass <id> --summary "..."|fail <id> --summary "..."]`);
|
|
276
341
|
}
|
|
277
342
|
break;
|
|
278
343
|
}
|
|
@@ -337,7 +402,11 @@ try {
|
|
|
337
402
|
for (const issue of issues) {
|
|
338
403
|
const icon = issue.severity === 'fatal' ? '☠' : issue.severity === 'high' ? '⚠' : '·';
|
|
339
404
|
console.log(` ${icon} [${issue.severity}] ${issue.type}: ${issue.msg}`);
|
|
405
|
+
if (issue.fix_hint) {
|
|
406
|
+
console.log(` → 修复: ${issue.fix_hint}`);
|
|
407
|
+
}
|
|
340
408
|
}
|
|
409
|
+
console.log(`\n参见 meta/PHILOSOPHY_WEAVER.md + dimensions/PART_DECOMPOSITION.md + dimensions/SEARCH_METHODOLOGY.md。`);
|
|
341
410
|
}
|
|
342
411
|
break;
|
|
343
412
|
}
|
|
@@ -368,22 +437,39 @@ try {
|
|
|
368
437
|
break;
|
|
369
438
|
}
|
|
370
439
|
|
|
371
|
-
case 'guide': {
|
|
372
|
-
const dryRun = argv.includes('--dry-run');
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
if (
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
console.log(
|
|
440
|
+
case 'guide': {
|
|
441
|
+
const dryRun = argv.includes('--dry-run');
|
|
442
|
+
const jsonOut = argv.includes('--json');
|
|
443
|
+
const result = guideProject(cwd(), { dryRun });
|
|
444
|
+
if (jsonOut) {
|
|
445
|
+
output(result);
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
console.log(`阶段 ${result.stage_num}: ${result.stage}`);
|
|
449
|
+
if (dryRun) {
|
|
450
|
+
console.log('诊断: dry-run(不写 heartbeat)');
|
|
382
451
|
}
|
|
452
|
+
const modeDesc = {
|
|
453
|
+
'manual': '手动(每步需确认)',
|
|
454
|
+
'auto-loop': 'AUTO auto-loop(设计阶段需 review,Intent Loop 自动)',
|
|
455
|
+
'auto-design': 'AUTO auto-design(全部自动)',
|
|
456
|
+
};
|
|
457
|
+
console.log(`模式: ${modeDesc[result.auto_mode] || result.auto_mode}`);
|
|
383
458
|
console.log(`\n${result.message}`);
|
|
384
459
|
console.log(`\n下一步: ${result.next_action}`);
|
|
385
460
|
console.log(` → ${result.next_command}`);
|
|
386
|
-
if (
|
|
461
|
+
if (result.inputs && result.inputs.length > 0) {
|
|
462
|
+
console.log(`\n需要读取:`);
|
|
463
|
+
for (const f of result.inputs) console.log(` - ${f}`);
|
|
464
|
+
}
|
|
465
|
+
if (result.outputs && result.outputs.length > 0) {
|
|
466
|
+
console.log(`\n需要产出:`);
|
|
467
|
+
for (const f of result.outputs) console.log(` - ${f}`);
|
|
468
|
+
}
|
|
469
|
+
if (result.verify_command) {
|
|
470
|
+
console.log(`\n完成后校验: ${result.verify_command}`);
|
|
471
|
+
}
|
|
472
|
+
if (result.auto_mode === 'manual' && result.stage_num > 0 && result.stage_num < 6) {
|
|
387
473
|
console.log(`\n提示: 开启 AUTO 模式可自动连续执行 — loom auto on`);
|
|
388
474
|
}
|
|
389
475
|
break;
|
|
@@ -392,76 +478,93 @@ try {
|
|
|
392
478
|
case 'auto': {
|
|
393
479
|
const loomRoot = findLoomRoot();
|
|
394
480
|
switch (sub) {
|
|
395
|
-
case 'on':
|
|
396
|
-
|
|
397
|
-
|
|
481
|
+
case 'on': {
|
|
482
|
+
// loom auto on → auto-loop(默认)
|
|
483
|
+
// loom auto on --design → auto-design
|
|
484
|
+
const wantDesign = argv.includes('--design');
|
|
485
|
+
const mode = wantDesign ? 'auto-design' : 'auto-loop';
|
|
486
|
+
autoOn(loomRoot, mode);
|
|
487
|
+
if (mode === 'auto-design') {
|
|
488
|
+
console.log('AUTO 模式: auto-design(全部自动,含哲学/愿景/架构)');
|
|
489
|
+
console.log('Agent 自动连续执行所有阶段,不等人类确认。');
|
|
490
|
+
} else {
|
|
491
|
+
console.log('AUTO 模式: auto-loop(Intent Loop 自动,设计阶段需 review)');
|
|
492
|
+
console.log(' - stage 1-3(哲学/愿景/架构):自动执行但需人类 review');
|
|
493
|
+
console.log(' - stage 4+(Intent Loop):自动连续执行');
|
|
494
|
+
}
|
|
398
495
|
console.log('核心契约: 持续运行,除非出意外否则不允许私自停止。');
|
|
399
496
|
console.log(' - L3 human_review 由 Keeper 自主判定,不停下等人类');
|
|
400
497
|
console.log(' - 唯一允许停下的情况: blocked(依赖阻塞/契约无法判定/连续 3 轮 deviated 升级)');
|
|
401
|
-
console.log('
|
|
498
|
+
console.log('切换: loom auto on --design | loom auto off');
|
|
402
499
|
break;
|
|
500
|
+
}
|
|
403
501
|
case 'off':
|
|
404
502
|
autoOff(loomRoot);
|
|
405
|
-
console.log('AUTO
|
|
503
|
+
console.log('AUTO 模式: manual(每步需人类确认)');
|
|
406
504
|
break;
|
|
407
505
|
case 'status': {
|
|
408
506
|
const status = autoStatus(loomRoot);
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
console.log(
|
|
507
|
+
const modeDesc = {
|
|
508
|
+
'manual': 'manual(每步需人类确认)',
|
|
509
|
+
'auto-loop': 'auto-loop(Intent Loop 自动,设计阶段需 review)',
|
|
510
|
+
'auto-design': 'auto-design(全部自动,含哲学/愿景/架构)',
|
|
511
|
+
};
|
|
512
|
+
console.log(`AUTO 模式: ${modeDesc[status.mode] || status.mode}`);
|
|
513
|
+
if (status.mode === 'auto-loop') {
|
|
514
|
+
console.log(' 规则: stage 1-3 需人类 review,stage 4+ 自动执行');
|
|
515
|
+
} else if (status.mode === 'auto-design') {
|
|
516
|
+
console.log(' 规则: 全部阶段自动执行,不需人类 review');
|
|
517
|
+
}
|
|
518
|
+
if (status.heartbeat) {
|
|
519
|
+
const hb = status.heartbeat;
|
|
520
|
+
console.log(` 心跳: ${hb.timestamp}`);
|
|
521
|
+
console.log(` 阶段: ${hb.stage} (stage ${hb.stage_num})`);
|
|
522
|
+
console.log(` 下一步: ${hb.next_action}`);
|
|
523
|
+
console.log(` 命令: ${hb.next_command}`);
|
|
524
|
+
} else if (status.mode !== 'manual') {
|
|
525
|
+
console.log(' 心跳: 尚未记录(运行 loom guide 后生成)');
|
|
423
526
|
}
|
|
424
527
|
break;
|
|
425
528
|
}
|
|
426
529
|
default:
|
|
427
|
-
die(`未知子命令: auto ${sub}\n用法: loom auto [on|off|status]`);
|
|
530
|
+
die(`未知子命令: auto ${sub}\n用法: loom auto [on [--design]|off|status]`);
|
|
428
531
|
}
|
|
429
532
|
break;
|
|
430
533
|
}
|
|
431
534
|
|
|
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, '/');
|
|
535
|
+
case 'preview': {
|
|
536
|
+
const previewFile = join(cwd(), 'loom-preview.html');
|
|
537
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
538
|
+
console.log(`用法:
|
|
539
|
+
loom preview 打开新鲜 preview;过期时提示重新生成
|
|
540
|
+
loom preview --regen 输出生成提示词,让 Agent 重写 loom-preview.html
|
|
541
|
+
loom preview status 检查 preview 是否存在、是否新鲜
|
|
542
|
+
loom preview --stale 强行打开过期 preview
|
|
543
|
+
loom preview --help 显示本帮助`);
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
const status = getPreviewStatus(cwd());
|
|
547
|
+
const hasPreview = status.exists;
|
|
548
|
+
const regenOnly = argv.includes('--regen') || argv.includes('-r');
|
|
549
|
+
const openStale = argv.includes('--stale');
|
|
550
|
+
|
|
551
|
+
if (sub === 'status') {
|
|
552
|
+
output(status);
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// 已有 HTML 且没指定 --regen:直接打开浏览器
|
|
557
|
+
if (hasPreview && !regenOnly) {
|
|
558
|
+
if (!status.fresh && !openStale) {
|
|
559
|
+
console.log('preview 已过期:.loom 源文件比 loom-preview.html 更新。');
|
|
560
|
+
console.log(` preview: ${status.preview_mtime || '未知'}`);
|
|
561
|
+
console.log(` 最新源: ${status.source_latest_mtime || '未知'} ${status.latest_source_file ? `(${status.latest_source_file})` : ''}`);
|
|
562
|
+
console.log('\n下一步: loom preview --regen');
|
|
563
|
+
console.log('强行打开旧 preview: loom preview --stale');
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
const { spawn } = await import('node:child_process');
|
|
567
|
+
const target = previewFile.replace(/\\/g, '/');
|
|
465
568
|
if (process.platform === 'win32') {
|
|
466
569
|
spawn('cmd', ['/c', 'start', target], { detached: true, stdio: 'ignore' }).unref();
|
|
467
570
|
} else if (process.platform === 'darwin') {
|
|
@@ -470,9 +573,9 @@ try {
|
|
|
470
573
|
spawn('xdg-open', [target], { detached: true, stdio: 'ignore' }).unref();
|
|
471
574
|
}
|
|
472
575
|
console.log(`已打开浏览器: ${previewFile}`);
|
|
473
|
-
console.log(status.fresh ? `重新生成: loom preview --regen` : `已打开旧 preview。重新生成: loom preview --regen`);
|
|
474
|
-
break;
|
|
475
|
-
}
|
|
576
|
+
console.log(status.fresh ? `重新生成: loom preview --regen` : `已打开旧 preview。重新生成: loom preview --regen`);
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
476
579
|
|
|
477
580
|
// 没有 HTML 或指定 --regen:输出提示词让 AI 生成
|
|
478
581
|
const prompt = generatePreviewPrompt();
|
|
@@ -508,10 +611,10 @@ To Human:
|
|
|
508
611
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
509
612
|
|
|
510
613
|
用法:
|
|
511
|
-
loom init 初始化项目(创建 .loom/v1/ 骨架 + 模板)
|
|
512
|
-
loom guide 诊断当前阶段,输出下一步引导
|
|
513
|
-
loom guide --dry-run 只读诊断当前阶段,不写 heartbeat
|
|
514
|
-
loom auto on|off|status AUTO 模式开关(on 时 Agent 自动连续执行)
|
|
614
|
+
loom init 初始化项目(创建 .loom/v1/ 骨架 + 模板)
|
|
615
|
+
loom guide 诊断当前阶段,输出下一步引导
|
|
616
|
+
loom guide --dry-run 只读诊断当前阶段,不写 heartbeat
|
|
617
|
+
loom auto on|off|status AUTO 模式开关(on 时 Agent 自动连续执行)
|
|
515
618
|
loom activate <role> 输出角色激活提示词(weaver|visionary|architect|forge|keeper)
|
|
516
619
|
|
|
517
620
|
loom version list 列出所有版本(* 标记当前)
|
|
@@ -533,11 +636,11 @@ To Human:
|
|
|
533
636
|
|
|
534
637
|
loom doctor 项目健康检查(一致性+孤儿引用+循环依赖+僵尸)
|
|
535
638
|
loom context 上下文摘要(进度+下一步+待验证+风险)
|
|
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)
|
|
639
|
+
loom preview 打开新鲜 HTML;过期则提示重新生成
|
|
640
|
+
loom preview status 检查 preview 是否存在、是否新鲜
|
|
641
|
+
loom preview --regen 强制重新输出提示词(让 AI 重新生成 HTML)
|
|
642
|
+
loom preview --stale 强行打开过期 preview
|
|
643
|
+
loom help <topic> 分层指南(workflow|concepts|loop|version|doctor|preview)
|
|
541
644
|
|
|
542
645
|
loom philosophy get <anchor> 按锚点加载哲学章节
|
|
543
646
|
loom philosophy list 列出哲学文档文件
|
package/cli/src/auto.js
CHANGED
|
@@ -1,31 +1,54 @@
|
|
|
1
1
|
// auto — AUTO 模式开关 + 心跳机制
|
|
2
|
-
// 存储机制:.loom/auto
|
|
2
|
+
// 存储机制:.loom/auto 文件内容 = 模式名(auto-loop / auto-design),不存在 = manual
|
|
3
3
|
// 心跳:每次 guide 调用时写 .loom/heartbeat.json(时间戳 + stage + next_command)
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
//
|
|
5
|
+
// 三种模式:
|
|
6
|
+
// manual — 每步停,所有阶段需人类 review
|
|
7
|
+
// auto-loop — 只 Intent Loop(stage 4+)自动,stage 1-3 仍需人类 review(默认)
|
|
8
|
+
// auto-design — 哲学/愿景/架构也自动,全部阶段不需人类 review
|
|
6
9
|
|
|
7
10
|
import { existsSync, writeFileSync, unlinkSync, readFileSync } from 'node:fs';
|
|
8
11
|
import { join } from 'node:path';
|
|
9
12
|
|
|
13
|
+
const VALID_MODES = ['auto-loop', 'auto-design'];
|
|
14
|
+
|
|
10
15
|
/**
|
|
11
|
-
*
|
|
16
|
+
* 读取 .loom/auto 文件内容,返回模式名。
|
|
17
|
+
* 向后兼容:空文件 / 旧时间戳 / 未知内容 → auto-loop
|
|
18
|
+
* @param {string} loomRoot — .loom 目录路径
|
|
19
|
+
* @returns {string} 'manual' | 'auto-loop' | 'auto-design'
|
|
20
|
+
*/
|
|
21
|
+
export function getAutoMode(loomRoot) {
|
|
22
|
+
const path = join(loomRoot, 'auto');
|
|
23
|
+
if (!existsSync(path)) return 'manual';
|
|
24
|
+
const content = readFileSync(path, 'utf-8').trim();
|
|
25
|
+
if (VALID_MODES.includes(content)) return content;
|
|
26
|
+
return 'auto-loop'; // 旧格式兼容
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 检查 AUTO 模式是否开启(非 manual)。
|
|
12
31
|
* @param {string} loomRoot — .loom 目录路径
|
|
13
32
|
* @returns {boolean}
|
|
14
33
|
*/
|
|
15
34
|
export function isAutoOn(loomRoot) {
|
|
16
|
-
return
|
|
35
|
+
return getAutoMode(loomRoot) !== 'manual';
|
|
17
36
|
}
|
|
18
37
|
|
|
19
38
|
/**
|
|
20
39
|
* 开启 AUTO 模式。
|
|
21
40
|
* @param {string} loomRoot — .loom 目录路径
|
|
41
|
+
* @param {string} mode — 'auto-loop' | 'auto-design'
|
|
22
42
|
*/
|
|
23
|
-
export function autoOn(loomRoot) {
|
|
24
|
-
|
|
43
|
+
export function autoOn(loomRoot, mode = 'auto-loop') {
|
|
44
|
+
if (!VALID_MODES.includes(mode)) {
|
|
45
|
+
throw new Error(`非法 AUTO 模式: "${mode}" (合法: ${VALID_MODES.join(' | ')})`);
|
|
46
|
+
}
|
|
47
|
+
writeFileSync(join(loomRoot, 'auto'), mode, 'utf-8');
|
|
25
48
|
}
|
|
26
49
|
|
|
27
50
|
/**
|
|
28
|
-
* 关闭 AUTO
|
|
51
|
+
* 关闭 AUTO 模式(切换到 manual)。
|
|
29
52
|
* @param {string} loomRoot — .loom 目录路径
|
|
30
53
|
*/
|
|
31
54
|
export function autoOff(loomRoot) {
|
|
@@ -36,14 +59,12 @@ export function autoOff(loomRoot) {
|
|
|
36
59
|
/**
|
|
37
60
|
* 获取 AUTO 状态描述。
|
|
38
61
|
* @param {string} loomRoot — .loom 目录路径
|
|
39
|
-
* @returns {{
|
|
62
|
+
* @returns {{ mode: string, heartbeat: object|null }}
|
|
40
63
|
*/
|
|
41
64
|
export function autoStatus(loomRoot) {
|
|
42
|
-
const
|
|
43
|
-
if (!existsSync(path)) return { on: false, since: null, heartbeat: null };
|
|
44
|
-
const since = readFileSync(path, 'utf-8').trim();
|
|
65
|
+
const mode = getAutoMode(loomRoot);
|
|
45
66
|
const heartbeat = readHeartbeat(loomRoot);
|
|
46
|
-
return {
|
|
67
|
+
return { mode, heartbeat };
|
|
47
68
|
}
|
|
48
69
|
|
|
49
70
|
/**
|
|
@@ -79,15 +100,17 @@ export function readHeartbeat(loomRoot) {
|
|
|
79
100
|
|
|
80
101
|
/**
|
|
81
102
|
* 判断当前阶段是否需要人类 review。
|
|
82
|
-
*
|
|
83
|
-
*
|
|
103
|
+
* manual:全部需 review
|
|
104
|
+
* auto-loop:stage 1-3 需 review,stage 4+ 自动
|
|
105
|
+
* auto-design:全部自动
|
|
84
106
|
* @param {string} loomRoot — .loom 目录路径
|
|
85
107
|
* @param {number} stageNum — 阶段号
|
|
86
108
|
* @returns {boolean} 是否需要人类 review
|
|
87
109
|
*/
|
|
88
110
|
export function needsHumanReview(loomRoot, stageNum) {
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
91
|
-
|
|
111
|
+
const mode = getAutoMode(loomRoot);
|
|
112
|
+
if (mode === 'manual') return true;
|
|
113
|
+
if (mode === 'auto-design') return false;
|
|
114
|
+
// auto-loop:stage 1-3 需 review,stage 4+ 自动
|
|
92
115
|
return stageNum > 0 && stageNum < 4;
|
|
93
116
|
}
|
package/cli/src/diagnostics.js
CHANGED
|
@@ -59,7 +59,7 @@ function intentMapDiagnostics(versionDir) {
|
|
|
59
59
|
|
|
60
60
|
const isTemplate = raw._meta?._template === true;
|
|
61
61
|
if (isTemplate) {
|
|
62
|
-
issues.push({ id: 'intent_map', type: 'intent_map_template', severity: 'high', msg: 'Intent Map 仍是模板,尚未由 Architect 产出真实意图图' });
|
|
62
|
+
issues.push({ id: 'intent_map', type: 'intent_map_template', severity: 'high', msg: 'Intent Map 仍是模板,尚未由 Architect 产出真实意图图', is_template: true });
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
try {
|
|
@@ -69,7 +69,7 @@ function intentMapDiagnostics(versionDir) {
|
|
|
69
69
|
valid = false;
|
|
70
70
|
// 模板状态下字段缺失是预期的,降级为 high 而非 fatal
|
|
71
71
|
// 非模板状态下字段缺失是真正的损坏,保持 fatal
|
|
72
|
-
issues.push({ id: 'intent_map', type: 'intent_map_invalid', severity: isTemplate ? 'high' : 'fatal', msg: e.message });
|
|
72
|
+
issues.push({ id: 'intent_map', type: 'intent_map_invalid', severity: isTemplate ? 'high' : 'fatal', msg: e.message, is_template: isTemplate });
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
return { raw, valid, issues, validMap };
|
|
@@ -87,6 +87,34 @@ function normalizeVerificationCommand(command) {
|
|
|
87
87
|
.trim();
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// 包管理器别名——npm test / pnpm test / bun test / yarn test 互相等价
|
|
91
|
+
const PM_ALIASES = ['npm', 'pnpm', 'bun', 'yarn'];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 把命令里的包管理器名归一化成 token,方便比较。
|
|
95
|
+
* "pnpm test" → "<PM> test","npm run build" → "<PM> run build"
|
|
96
|
+
*/
|
|
97
|
+
function normalizePackageManager(command) {
|
|
98
|
+
let result = command;
|
|
99
|
+
for (const pm of PM_ALIASES) {
|
|
100
|
+
result = result.replace(new RegExp(`\\b${pm}\\b`, 'g'), '<PM>');
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 检测项目使用的包管理器。
|
|
107
|
+
* 优先级:锁文件存在性
|
|
108
|
+
* @param {string} projectDir — 项目根目录
|
|
109
|
+
* @returns {string} 'pnpm' | 'bun' | 'yarn' | 'npm'
|
|
110
|
+
*/
|
|
111
|
+
export function detectPackageManager(projectDir) {
|
|
112
|
+
if (existsSync(join(projectDir, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
113
|
+
if (existsSync(join(projectDir, 'bun.lockb'))) return 'bun';
|
|
114
|
+
if (existsSync(join(projectDir, 'yarn.lock'))) return 'yarn';
|
|
115
|
+
return 'npm';
|
|
116
|
+
}
|
|
117
|
+
|
|
90
118
|
function commandCoversMethod(actualCommand, expectedMethod) {
|
|
91
119
|
const actual = normalizeVerificationCommand(actualCommand);
|
|
92
120
|
const expected = normalizeVerificationCommand(expectedMethod);
|
|
@@ -96,8 +124,12 @@ function commandCoversMethod(actualCommand, expectedMethod) {
|
|
|
96
124
|
const expectedPart = normalizeVerificationCommand(part);
|
|
97
125
|
if (!expectedPart) return true;
|
|
98
126
|
if (actual.includes(expectedPart)) return true;
|
|
127
|
+
// 包管理器别名归一化:pnpm test = npm test = bun test = yarn test
|
|
128
|
+
const actualNorm = normalizePackageManager(actual);
|
|
129
|
+
const expectedNorm = normalizePackageManager(expectedPart);
|
|
130
|
+
if (actualNorm.includes(expectedNorm)) return true;
|
|
99
131
|
// npm test is an acceptable broader reproduction for node --test based methods.
|
|
100
|
-
if (expectedPart.startsWith('node --test') &&
|
|
132
|
+
if (expectedPart.startsWith('node --test') && actualNorm.includes('<PM> test')) return true;
|
|
101
133
|
return false;
|
|
102
134
|
});
|
|
103
135
|
}
|
|
@@ -105,6 +137,42 @@ function commandCoversMethod(actualCommand, expectedMethod) {
|
|
|
105
137
|
// ─── doctor ────────────────────────────────────────────
|
|
106
138
|
// 全面健康检查:一致性 + 孤儿引用 + 循环依赖 + 僵尸 Intent
|
|
107
139
|
|
|
140
|
+
// 每种 issue 类型的修复提示——给 Agent 行动化建议
|
|
141
|
+
const FIX_HINTS = {
|
|
142
|
+
intent_map_unreadable: '检查 .loom/v{N}/04_INTENT_MAP.json 是否合法 JSON(jsonlint.com 或 node -e "JSON.parse(require(\'fs\').readFileSync(\'04_INTENT_MAP.json\'))")',
|
|
143
|
+
intent_map_missing: '运行 loom init 或 loom activate architect 产出 04_INTENT_MAP.json',
|
|
144
|
+
intent_map_template: '运行 loom activate architect,Architect 填充真实 Intent Map 后删除 _meta._template 标记',
|
|
145
|
+
intent_map_invalid: '按报错信息修正 04_INTENT_MAP.json 里对应字段(补 title / 加长 acceptance / 填必填字段)',
|
|
146
|
+
completed_no_record: '在 .loom/v{N}/verifications/ 下补验证记录,或运行 loom verify pass {id} --summary "..."',
|
|
147
|
+
in_progress_no_record: '运行 loom verify pass {id} --summary "..." 写入验证记录,或 loom intent update {id} --status pending 回退',
|
|
148
|
+
orphan_philosophy_ref: '检查 04_INTENT_MAP.json 里 {id} 的 philosophy_anchors,移除或修正不存在的哲学文件引用',
|
|
149
|
+
orphan_dependency: '检查 04_INTENT_MAP.json 里 {id} 的 depends_on,移除或修正不存在的 Intent ID',
|
|
150
|
+
cycle: '打破循环:把循环链中某个 Intent 的 depends_on 里去掉前驱,或拆成更小的 Intent',
|
|
151
|
+
zombie: '检查 {id} 是否还需要——不需要就 loom intent update {id} --status completed 或 blocked',
|
|
152
|
+
completed_depends_blocked: '检查依赖 {dep} 为什么 blocked——解决阻塞或把 {id} 回退到 in_progress',
|
|
153
|
+
test_script_missing: '在 package.json 里加 test 脚本,或修正 verification_method 指向实际存在的测试命令',
|
|
154
|
+
verification_method_unverified: '运行 loom verify pass {id} --summary "..." --reproduction-command "..." 覆盖声明的验证方式',
|
|
155
|
+
verification_method_drift: '验证记录的 reproduction_command 要覆盖 verification_method 声明的命令(支持 npm/pnpm/bun 互相等价)',
|
|
156
|
+
inspiration_source: '在哲学文档的"灵感来源"章节填入至少 3 个源(- **源名** — 理由。来源:URL 或 file:// 或 local:./path)',
|
|
157
|
+
part_decomposition: '在哲学文档加"实现部分清单"章节,按 PART_DECOMPOSITION.md 拆解实现部分(- **部分名** 格式)',
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 给 issue 补 fix_hint——把 {id} {dep} 等占位符替换成实际值。
|
|
162
|
+
*/
|
|
163
|
+
function addFixHint(issue) {
|
|
164
|
+
const template = FIX_HINTS[issue.type];
|
|
165
|
+
if (!template) return issue;
|
|
166
|
+
let hint = template;
|
|
167
|
+
// 提取 id 里的实际 Intent ID(issue.id 可能是 "INT-001" 或 "INT-002→INT-001" 等)
|
|
168
|
+
const idMatch = String(issue.id).match(/(INT-\d+)/);
|
|
169
|
+
if (idMatch) hint = hint.replace(/\{id\}/g, idMatch[1]);
|
|
170
|
+
// 提取 dep(从 msg 里找 depends_on 后的 Intent ID)
|
|
171
|
+
const depMatch = issue.msg && issue.msg.match(/依赖.*?(INT-\d+)/);
|
|
172
|
+
if (depMatch) hint = hint.replace(/\{dep\}/g, depMatch[1]);
|
|
173
|
+
return { ...issue, fix_hint: hint };
|
|
174
|
+
}
|
|
175
|
+
|
|
108
176
|
/**
|
|
109
177
|
* 项目健康检查。
|
|
110
178
|
* @param {string} versionDir — 当前版本目录
|
|
@@ -118,7 +186,8 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
|
118
186
|
|
|
119
187
|
if (!mapState.validMap) {
|
|
120
188
|
appendPhilosophyDiagnostics(issues, philosophyDir);
|
|
121
|
-
|
|
189
|
+
const issuesWithHints = issues.map(addFixHint);
|
|
190
|
+
return { issues: issuesWithHints, summary: summarizeIssues(issuesWithHints) };
|
|
122
191
|
}
|
|
123
192
|
|
|
124
193
|
const { intents } = mapState.validMap;
|
|
@@ -193,28 +262,29 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
|
193
262
|
|
|
194
263
|
// 7. 验证脚本可执行性:检查 verification_method 引用的脚本/目录是否存在
|
|
195
264
|
const projectDir = join(versionDir, '..', '..');
|
|
265
|
+
const pm = detectPackageManager(projectDir);
|
|
196
266
|
for (const [id, intent] of Object.entries(intents)) {
|
|
197
267
|
const vm = getIntentVerificationMethod(intent);
|
|
198
268
|
if (!vm) continue;
|
|
199
|
-
//
|
|
200
|
-
|
|
269
|
+
// 检测任意包管理器的 test 引用(npm/pnpm/bun/yarn)
|
|
270
|
+
const pmTestRe = new RegExp(`(?:${PM_ALIASES.join('|')})\\s+(?:run\\s+)?test`);
|
|
271
|
+
if (pmTestRe.test(vm)) {
|
|
201
272
|
const pkgPath = join(projectDir, 'package.json');
|
|
202
273
|
if (!existsSync(pkgPath)) {
|
|
203
|
-
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求
|
|
274
|
+
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 test 但项目根没有 package.json` });
|
|
204
275
|
} else {
|
|
205
276
|
try {
|
|
206
277
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
207
278
|
const testScript = pkg.scripts && pkg.scripts.test;
|
|
208
279
|
if (!testScript) {
|
|
209
|
-
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求
|
|
280
|
+
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 test 但 package.json 没有 test 脚本` });
|
|
210
281
|
} else {
|
|
211
282
|
// 检查 test 脚本引用的目录/文件是否存在
|
|
212
|
-
// 常见模式: "node --test test/" / "mocha test/" / "jest" 等
|
|
213
283
|
const testDirMatch = testScript.match(/(?:--test|test)\s+(\S+)/);
|
|
214
284
|
if (testDirMatch) {
|
|
215
285
|
const testTarget = testDirMatch[1].replace(/['"]/g, '');
|
|
216
286
|
if (!existsSync(join(projectDir, testTarget))) {
|
|
217
|
-
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求
|
|
287
|
+
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 test 但 test 脚本引用的 ${testTarget} 不存在` });
|
|
218
288
|
}
|
|
219
289
|
}
|
|
220
290
|
}
|
|
@@ -248,7 +318,8 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
|
248
318
|
}
|
|
249
319
|
|
|
250
320
|
appendPhilosophyDiagnostics(issues, philosophyDir);
|
|
251
|
-
|
|
321
|
+
const issuesWithHints = issues.map(addFixHint);
|
|
322
|
+
return { issues: issuesWithHints, summary: summarizeIssues(issuesWithHints) };
|
|
252
323
|
}
|
|
253
324
|
|
|
254
325
|
function appendPhilosophyDiagnostics(issues, philosophyDir) {
|
|
@@ -331,11 +402,16 @@ export function contextSummary(versionDir, verificationsDir, philosophyDir) {
|
|
|
331
402
|
const pending = mapState.valid ? getPendingVerifications(versionDir, verificationsDir) : [];
|
|
332
403
|
const { issues } = doctor(versionDir, verificationsDir, philosophyDir);
|
|
333
404
|
|
|
405
|
+
// 区分模板阶段问题(待填充)和真实损坏
|
|
406
|
+
const templateIssues = issues.filter((i) => i.is_template || i.type === 'intent_map_template' || i.type === 'inspiration_source' || i.type === 'part_decomposition');
|
|
407
|
+
const realIssues = issues.filter((i) => !templateIssues.includes(i));
|
|
408
|
+
|
|
334
409
|
const risks = [];
|
|
335
|
-
const fatalCount =
|
|
336
|
-
const highCount =
|
|
410
|
+
const fatalCount = realIssues.filter((i) => i.severity === 'fatal').length;
|
|
411
|
+
const highCount = realIssues.filter((i) => i.severity === 'high').length;
|
|
337
412
|
if (fatalCount > 0) risks.push(`${fatalCount} 个致命问题(Intent Map 损坏/循环依赖)`);
|
|
338
413
|
if (highCount > 0) risks.push(`${highCount} 个高严重度问题(状态不一致/孤儿引用)`);
|
|
414
|
+
if (templateIssues.length > 0) risks.push(`${templateIssues.length} 个待填充(模板未产出,需 Weaver/Architect 填充)`);
|
|
339
415
|
if (status.counts.blocked > 0) risks.push(`${status.counts.blocked} 个阻塞 Intent`);
|
|
340
416
|
|
|
341
417
|
return {
|
|
@@ -349,7 +425,7 @@ export function contextSummary(versionDir, verificationsDir, philosophyDir) {
|
|
|
349
425
|
pending_verifications: pending,
|
|
350
426
|
inconsistent_states: issues.filter((i) => i.type === 'in_progress_no_record' || i.type === 'completed_no_record').map((i) => i.id),
|
|
351
427
|
risks,
|
|
352
|
-
healthy:
|
|
428
|
+
healthy: realIssues.length === 0,
|
|
353
429
|
};
|
|
354
430
|
}
|
|
355
431
|
|
package/cli/src/guide.js
CHANGED
|
@@ -6,7 +6,7 @@ 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, writeHeartbeat, needsHumanReview } from './auto.js';
|
|
9
|
+
import { isAutoOn, getAutoMode, writeHeartbeat, needsHumanReview } from './auto.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* 检测文件是否还是模板(未填充真实内容)。
|
|
@@ -36,17 +36,78 @@ function isTemplate(filePath) {
|
|
|
36
36
|
/**
|
|
37
37
|
* 诊断项目当前阶段。
|
|
38
38
|
* @param {string} projectDir — 项目根目录
|
|
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
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 autoMode = getAutoMode(loomRoot);
|
|
45
|
+
const auto = autoMode !== 'manual'; // 向后兼容 boolean
|
|
46
|
+
const result = diagnoseStage(cwd, loomRoot, auto);
|
|
47
|
+
result.auto_mode = autoMode;
|
|
48
|
+
|
|
49
|
+
// 按 stage 补充可执行信息:要读什么、要产出什么、完成后跑什么校验
|
|
50
|
+
const current = result.details.version || 'v1';
|
|
51
|
+
const stageMeta = {
|
|
52
|
+
not_initialized: {
|
|
53
|
+
inputs: [],
|
|
54
|
+
outputs: ['.loom/v1/'],
|
|
55
|
+
verify_command: 'loom guide',
|
|
56
|
+
},
|
|
57
|
+
no_version: {
|
|
58
|
+
inputs: [],
|
|
59
|
+
outputs: ['.loom/v1/'],
|
|
60
|
+
verify_command: 'loom guide',
|
|
61
|
+
},
|
|
62
|
+
need_philosophy: {
|
|
63
|
+
inputs: ['meta/PHILOSOPHY_WEAVER.md', 'meta/BASELINE.md', 'dimensions/PART_DECOMPOSITION.md', 'dimensions/SEARCH_METHODOLOGY.md'],
|
|
64
|
+
outputs: [`.loom/${current}/00_PHILOSOPHY/PRODUCT_PHILOSOPHY.md`, `.loom/${current}/00_PHILOSOPHY/ENGINEERING_CREED.md`, `.loom/${current}/00_PHILOSOPHY/DECISION_RUBRIC.md`],
|
|
65
|
+
verify_command: 'loom philosophy check',
|
|
66
|
+
},
|
|
67
|
+
need_vision: {
|
|
68
|
+
inputs: ['roles/visionary.md', `.loom/${current}/00_PHILOSOPHY/`],
|
|
69
|
+
outputs: [`.loom/${current}/01_VISION.md`],
|
|
70
|
+
verify_command: 'loom guide',
|
|
71
|
+
},
|
|
72
|
+
need_architecture: {
|
|
73
|
+
inputs: ['roles/architect.md', `.loom/${current}/01_VISION.md`],
|
|
74
|
+
outputs: [`.loom/${current}/02_ARCHITECTURE.md`, `.loom/${current}/04_INTENT_MAP.json`],
|
|
75
|
+
verify_command: 'loom doctor',
|
|
76
|
+
},
|
|
77
|
+
intent_map_broken: {
|
|
78
|
+
inputs: [`.loom/${current}/04_INTENT_MAP.json`],
|
|
79
|
+
outputs: [`.loom/${current}/04_INTENT_MAP.json`],
|
|
80
|
+
verify_command: 'loom intent validate',
|
|
81
|
+
},
|
|
82
|
+
in_loop: {
|
|
83
|
+
inputs: ['roles/forge.md', 'roles/keeper.md', `.loom/${current}/04_INTENT_MAP.json`],
|
|
84
|
+
outputs: ['代码文件', `.loom/${current}/verifications/INT-*.json`],
|
|
85
|
+
verify_command: 'loom verify pending',
|
|
86
|
+
},
|
|
87
|
+
ready_for_loop: {
|
|
88
|
+
inputs: ['roles/forge.md', 'roles/keeper.md', `.loom/${current}/04_INTENT_MAP.json`],
|
|
89
|
+
outputs: ['代码文件', `.loom/${current}/verifications/INT-*.json`],
|
|
90
|
+
verify_command: 'loom verify pending',
|
|
91
|
+
},
|
|
92
|
+
all_done: {
|
|
93
|
+
inputs: [],
|
|
94
|
+
outputs: [],
|
|
95
|
+
verify_command: 'loom doctor',
|
|
96
|
+
},
|
|
97
|
+
unknown: {
|
|
98
|
+
inputs: [],
|
|
99
|
+
outputs: [],
|
|
100
|
+
verify_command: 'loom doctor',
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
const meta = stageMeta[result.stage] || {};
|
|
104
|
+
result.inputs = meta.inputs || [];
|
|
105
|
+
result.outputs = meta.outputs || [];
|
|
106
|
+
result.verify_command = meta.verify_command || null;
|
|
107
|
+
// 统一后处理:写心跳 + 加 AUTO 提示词 + 判断是否需要人类 review
|
|
108
|
+
if (existsSync(loomRoot) && !options.dryRun) {
|
|
109
|
+
try {
|
|
110
|
+
writeHeartbeat(loomRoot, {
|
|
50
111
|
stage: result.stage,
|
|
51
112
|
stage_num: result.stage_num,
|
|
52
113
|
next_command: result.next_command,
|
|
@@ -57,13 +118,19 @@ export function guideProject(projectDir, options = {}) {
|
|
|
57
118
|
result.needs_human_review = needsHumanReview(loomRoot, result.stage_num);
|
|
58
119
|
// 在 message 末尾加 AUTO 提示
|
|
59
120
|
if (result.stage_num > 0 && result.stage_num < 4) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
121
|
+
if (autoMode === 'auto-design') {
|
|
122
|
+
result.message += '\n\n> AUTO 模式 auto-design——设计阶段也自动执行,无需人类确认。';
|
|
123
|
+
} else if (autoMode === 'auto-loop') {
|
|
124
|
+
result.message += '\n\n> ⚠ 这是设计阶段(哲学/愿景/架构),auto-loop 模式下需要人类 review 后再继续。';
|
|
125
|
+
} else {
|
|
126
|
+
result.message += '\n\n> ⚠ AUTO 模式 manual——每步都需要人类确认后再继续。';
|
|
127
|
+
}
|
|
128
|
+
} else if (result.stage_num >= 4) {
|
|
129
|
+
if (auto) {
|
|
130
|
+
result.message += '\n\n> AUTO 模式开启——直接执行 next_command,无需人类确认。';
|
|
131
|
+
} else {
|
|
132
|
+
result.message += '\n\n> ⚠ AUTO 模式关闭——执行 next_command 后等人类确认再继续。';
|
|
133
|
+
}
|
|
67
134
|
}
|
|
68
135
|
return result;
|
|
69
136
|
}
|
|
@@ -145,11 +212,11 @@ function diagnoseStage(cwd, loomRoot, auto) {
|
|
|
145
212
|
}
|
|
146
213
|
|
|
147
214
|
// 状态 4-7: Intent Map 已设计,根据 Intent 状态判断
|
|
148
|
-
let intentMap;
|
|
149
|
-
let intents;
|
|
150
|
-
try {
|
|
151
|
-
intentMap = loadIntentMap(versionDir);
|
|
152
|
-
intents = intentMap.intents;
|
|
215
|
+
let intentMap;
|
|
216
|
+
let intents;
|
|
217
|
+
try {
|
|
218
|
+
intentMap = loadIntentMap(versionDir);
|
|
219
|
+
intents = intentMap.intents;
|
|
153
220
|
} catch (e) {
|
|
154
221
|
return {
|
|
155
222
|
stage: 'intent_map_broken',
|
|
@@ -220,7 +287,7 @@ function diagnoseStage(cwd, loomRoot, auto) {
|
|
|
220
287
|
if (counts.needs_review > 0) {
|
|
221
288
|
const reviewIds = allIntents.filter((i) => i.status === 'needs_review').map((i) => i.id);
|
|
222
289
|
// 读 _meta.pass_count 收敛趟计数(最大 3 趟)
|
|
223
|
-
const passCount = intentMap._meta?.pass_count || 1;
|
|
290
|
+
const passCount = intentMap._meta?.pass_count || 1;
|
|
224
291
|
const MAX_PASSES = 3;
|
|
225
292
|
const isOverLimit = passCount > MAX_PASSES;
|
|
226
293
|
const passMsg = ` [Pass ${passCount}/${MAX_PASSES}]`;
|
package/cli/src/philosophy.js
CHANGED
|
@@ -85,20 +85,22 @@ function parseInspirationSources(content) {
|
|
|
85
85
|
|
|
86
86
|
// 匹配 - xxx / * xxx / 1. xxx / 2. xxx 等
|
|
87
87
|
const ITEM_RE = /^\s*(?:[-*]|\d+\.)\s+/;
|
|
88
|
+
// URL 匹配:https:// / file:// / local:./path / local:/abs/path
|
|
89
|
+
const URL_RE = /(?:https?:|file:)[\/]+[^\s))]+|local:[^\s))]+/g;
|
|
88
90
|
|
|
89
91
|
for (const line of lines) {
|
|
90
92
|
if (ITEM_RE.test(line)) {
|
|
91
93
|
// 新条目
|
|
92
94
|
if (currentItem) items.push(currentItem);
|
|
93
95
|
const raw = line.replace(ITEM_RE, '').trim();
|
|
94
|
-
const urls = [...raw.matchAll(
|
|
96
|
+
const urls = [...raw.matchAll(URL_RE)].map((m) => m[0]);
|
|
95
97
|
const name = raw.replace(/\*\*/g, '').split(/[((——]/)[0].trim();
|
|
96
98
|
const hasReason = REASON_KEYWORDS.some((kw) => raw.includes(kw));
|
|
97
99
|
currentItem = { raw, name, urls, hasReason };
|
|
98
100
|
} else if (currentItem && line.trim()) {
|
|
99
101
|
// 多行条目的续行
|
|
100
102
|
currentItem.raw += ' ' + line.trim();
|
|
101
|
-
const newUrls = [...line.matchAll(
|
|
103
|
+
const newUrls = [...line.matchAll(URL_RE)].map((m) => m[0]);
|
|
102
104
|
currentItem.urls.push(...newUrls);
|
|
103
105
|
if (REASON_KEYWORDS.some((kw) => line.includes(kw))) {
|
|
104
106
|
currentItem.hasReason = true;
|
package/cli/src/verify.js
CHANGED
|
@@ -92,14 +92,14 @@ export function writeVerification(verificationsDir, record) {
|
|
|
92
92
|
data = { intent_id: record.intent_id, records: [] };
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
// 计算轮次和连续 deviated 计数。规范要求中间出现 passed/blocked 后重置。
|
|
96
|
-
const round = data.records.length + 1;
|
|
97
|
-
const recordsWithCurrent = [...data.records, record];
|
|
98
|
-
let deviatedCount = 0;
|
|
99
|
-
for (let i = recordsWithCurrent.length - 1; i >= 0; i--) {
|
|
100
|
-
if (recordsWithCurrent[i].verdict !== 'deviated') break;
|
|
101
|
-
deviatedCount++;
|
|
102
|
-
}
|
|
95
|
+
// 计算轮次和连续 deviated 计数。规范要求中间出现 passed/blocked 后重置。
|
|
96
|
+
const round = data.records.length + 1;
|
|
97
|
+
const recordsWithCurrent = [...data.records, record];
|
|
98
|
+
let deviatedCount = 0;
|
|
99
|
+
for (let i = recordsWithCurrent.length - 1; i >= 0; i--) {
|
|
100
|
+
if (recordsWithCurrent[i].verdict !== 'deviated') break;
|
|
101
|
+
deviatedCount++;
|
|
102
|
+
}
|
|
103
103
|
|
|
104
104
|
// 追加新记录
|
|
105
105
|
data.records.push({
|
|
@@ -134,6 +134,36 @@ export function getVerificationHistory(verificationsDir, intentId) {
|
|
|
134
134
|
return readJsonFile(filePath, '验证记录');
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* 快捷创建验证记录——Agent 不用手动构造完整 JSON。
|
|
139
|
+
* 内部用 summary 填充四个维度的 evidence,生成标准记录格式。
|
|
140
|
+
* @param {string} verificationsDir — verifications/ 目录路径
|
|
141
|
+
* @param {string} intentId — 如 "INT-001"
|
|
142
|
+
* @param {string} verdict — 'passed' | 'deviated' | 'blocked'
|
|
143
|
+
* @param {string} summary — 验证摘要(也会作为四个维度的 evidence)
|
|
144
|
+
* @param {object} [extras]
|
|
145
|
+
* @param {string} [extras.reproduction_command] — 复现命令
|
|
146
|
+
* @param {string} [extras.deviation_detail] — 偏离说明(deviated 时)
|
|
147
|
+
* @returns {{ filePath: string, round: number, deviated_count: number, should_escalate: boolean }}
|
|
148
|
+
*/
|
|
149
|
+
export function createQuickVerification(verificationsDir, intentId, verdict, summary, extras = {}) {
|
|
150
|
+
const timestamp = new Date().toISOString();
|
|
151
|
+
// 用 summary 填充四个维度的 evidence——快捷命令不要求 Agent 逐维度写
|
|
152
|
+
const dimensions = {};
|
|
153
|
+
for (const dim of REQUIRED_DIMENSIONS) {
|
|
154
|
+
dimensions[dim] = { verdict, evidence: summary };
|
|
155
|
+
}
|
|
156
|
+
return writeVerification(verificationsDir, {
|
|
157
|
+
intent_id: intentId,
|
|
158
|
+
verdict,
|
|
159
|
+
timestamp,
|
|
160
|
+
summary,
|
|
161
|
+
dimensions,
|
|
162
|
+
reproduction_command: extras.reproduction_command || null,
|
|
163
|
+
deviation_detail: extras.deviation_detail || null,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
137
167
|
/**
|
|
138
168
|
* 返回所有待验证的 Intent(有实现产物但还没验证记录的)。
|
|
139
169
|
* 需要传入 Intent Map 来判断哪些 Intent 是 in_progress。
|