@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 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(` 创建: ${result.created.length} 项`);
139
- for (const c of result.created) console.log(` + ${c}`);
140
- if (result.skipped.length) {
141
- console.log(` 跳过(已存在): ${result.skipped.length} 项`);
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 result = guideProject(cwd(), { dryRun });
374
- console.log(`阶段 ${result.stage_num}: ${result.stage}`);
375
- if (dryRun) {
376
- console.log('诊断: dry-run(不写 heartbeat)');
377
- }
378
- if (result.auto) {
379
- console.log(`模式: AUTO(自动执行,不等确认)`);
380
- } else {
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 (!result.auto && result.stage_num > 0 && result.stage_num < 6) {
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
- autoOn(loomRoot);
397
- console.log('AUTO 模式已开启。Agent 将自动连续执行,不等用户确认。');
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('关闭: loom auto off');
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
- if (status.on) {
410
- console.log(`AUTO 模式: 开启(自 ${status.since})`);
411
- console.log(' 规则: stage 1-3(哲学/愿景/架构)需人类 review,stage 4+(Intent Loop)自动执行');
412
- if (status.heartbeat) {
413
- const hb = status.heartbeat;
414
- console.log(` 心跳: ${hb.timestamp}`);
415
- console.log(` 阶段: ${hb.stage} (stage ${hb.stage_num})`);
416
- console.log(` 下一步: ${hb.next_action}`);
417
- console.log(` 命令: ${hb.next_command}`);
418
- } else {
419
- console.log(' 心跳: 尚未记录(运行 loom guide 后生成)');
420
- }
421
- } else {
422
- console.log('AUTO 模式: 关闭(所有阶段都需人类确认)');
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 文件存在 = on,不存在 = off
2
+ // 存储机制:.loom/auto 文件内容 = 模式名(auto-loop / auto-design),不存在 = manual
3
3
  // 心跳:每次 guide 调用时写 .loom/heartbeat.json(时间戳 + stage + next_command)
4
- // AUTO on(默认):stage 4+(Intent Loop)自动跑,stage 1-3(哲学/愿景/架构)仍需人类 review
5
- // AUTO off:所有阶段都需人类确认,每步拆得更碎
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
- * 检查 AUTO 模式是否开启。
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 existsSync(join(loomRoot, 'auto'));
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
- writeFileSync(join(loomRoot, 'auto'), new Date().toISOString(), 'utf-8');
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 {{ on: boolean, since: string|null, heartbeat: object|null }}
62
+ * @returns {{ mode: string, heartbeat: object|null }}
40
63
  */
41
64
  export function autoStatus(loomRoot) {
42
- const path = join(loomRoot, 'auto');
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 { on: true, since, heartbeat };
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
- * AUTO on 时:stage 1-3(哲学/愿景/架构)需要人类 review,stage 4+ 自动跑
83
- * AUTO off 时:所有阶段都需要人类 review
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 autoOn = isAutoOn(loomRoot);
90
- if (!autoOn) return true; // AUTO off:所有阶段都需人类 review
91
- // AUTO on:stage 1-3 需人类 review,stage 4+ 自动
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
  }
@@ -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') && actual.includes('npm test')) return true;
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
- return { issues, summary: summarizeIssues(issues) };
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
- // 检测 npm test 引用
200
- if (vm.includes('npm test') || vm.includes('npm run test')) {
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 要求 npm test 但项目根没有 package.json` });
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 要求 npm test 但 package.json 没有 test 脚本` });
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 要求 npm test 但 test 脚本引用的 ${testTarget} 不存在` });
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
- return { issues, summary: summarizeIssues(issues) };
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 = issues.filter((i) => i.severity === 'fatal').length;
336
- const highCount = issues.filter((i) => i.severity === 'high').length;
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: issues.length === 0,
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 auto = isAutoOn(loomRoot);
45
- const result = diagnoseStage(cwd, loomRoot, auto);
46
- // 统一后处理:写心跳 + AUTO 提示词 + 判断是否需要人类 review
47
- if (existsSync(loomRoot) && !options.dryRun) {
48
- try {
49
- writeHeartbeat(loomRoot, {
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
- 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,无需人类确认。';
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}]`;
@@ -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(/https?:\/\/[^\s))]+/g)].map((m) => m[0]);
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(/https?:\/\/[^\s))]+/g)].map((m) => m[0]);
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。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haaaiawd/loom",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "LOOM — 哲学驱动开发框架。Agent 通过 CLI 访问 Intent Map / 哲学 / 验证记录,不直接读文件",
5
5
  "type": "module",
6
6
  "bin": {