sillyspec 3.25.11 → 3.26.1
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/.claude/skills/sillyspec-execute/SKILL.md +1 -1
- package/.claude/skills/sillyspec-quick/SKILL.md +3 -1
- package/.sillyspec-platform-cleaned +1 -0
- package/.sillyspec-platform.json +7 -0
- package/README.md +3 -5
- package/package.json +2 -3
- package/src/db-engine.js +55 -0
- package/src/db.js +53 -35
- package/src/dispatch/probe.js +11 -9
- package/src/doctor-diagnostics.js +6 -6
- package/src/hooks/worktree-guard.js +18 -26
- package/src/index.js +151 -3
- package/src/local-detect.js +120 -110
- package/src/progress/change-registry.js +43 -2
- package/src/progress/shared.js +2 -2
- package/src/progress/stage-machine.js +6 -0
- package/src/progress/step-store.js +10 -0
- package/src/progress.js +302 -10
- package/src/quicklog.js +48 -9
- package/src/run/command.js +13 -2
- package/src/run/complete-handlers.js +37 -8
- package/src/run/complete.js +17 -4
- package/src/run/gates.js +13 -2
- package/src/run/shared.js +74 -0
- package/src/run/stage.js +6 -1
- package/src/sillyhub-mcp/client.js +12 -7
- package/src/sillyhub-mcp/config.js +62 -0
- package/src/stage-review.js +105 -1
- package/src/stages/doctor.js +1 -1
- package/src/stages/execute.js +6 -5
- package/src/stages/quick.js +24 -8
- package/src/stages/scan.js +44 -5
- package/src/stages/verify.js +2 -2
- package/src/sync.js +430 -13
- package/src/task-review.js +29 -0
- package/src/verify-postcheck.js +7 -2
- package/src/worktree-apply.js +135 -27
package/src/progress.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* worktree-guard hook 直读 sillyspec.db,不再有 gate-status.json 缓存双源(task-10 废除)。
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, appendFileSync } from 'fs';
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, appendFileSync, copyFileSync } from 'fs';
|
|
14
14
|
import { join, basename, dirname, resolve, sep } from 'path';
|
|
15
15
|
import { tmpdir } from 'os';
|
|
16
16
|
import { DB } from './db.js';
|
|
@@ -347,7 +347,7 @@ export class ProgressManager {
|
|
|
347
347
|
}
|
|
348
348
|
|
|
349
349
|
const result = {
|
|
350
|
-
_version:
|
|
350
|
+
_version: 5,
|
|
351
351
|
project: projectName,
|
|
352
352
|
currentChange: cName,
|
|
353
353
|
currentStage: currentStage || '',
|
|
@@ -364,6 +364,292 @@ export class ProgressManager {
|
|
|
364
364
|
return result;
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
+
/**
|
|
368
|
+
* 同步专用六表完整序列化(design §8/B1,task-02 / D-005@v2 / FR-07)。
|
|
369
|
+
* 与 read()(聚合视图,不读 approvals 且 changes 只投影五列)不同,本方法输出真正六表投影,
|
|
370
|
+
* 作为同步载体(ProgressManager.import() 的逆运算,task-03)。
|
|
371
|
+
*
|
|
372
|
+
* 输出契约(字段名 = DB 列名 snake_case,import 直接对列写回,round-trip 无映射损耗):
|
|
373
|
+
* project { name, schema_version } —— 全局单行稳定字段(id/created_at/updated_at 本地元数据不同步)
|
|
374
|
+
* changes [{ name, current_stage, status, last_active,
|
|
375
|
+
* last_synced_platform_ts, last_local_modified_ts }] —— 只投影流程进度列;
|
|
376
|
+
* 排除 isolation_* 系列 与 platform_* 系列(platform_change_id/workspace_id/last_sync/sync_enabled)
|
|
377
|
+
* 以及 created_at(本地强相关,不同步,B2)
|
|
378
|
+
* stages [{ change_name, stage, status, started_at, completed_at,
|
|
379
|
+
* revision, reopened_from_step, reopened_at, stale_reason }] —— 用 change_name+stage 替代 id 外键
|
|
380
|
+
* steps [{ change_name, stage, name, status, output, completed_at, ordering,
|
|
381
|
+
* wait_reason, wait_options, wait_answer, waited_at, wait_answers,
|
|
382
|
+
* wait_round, max_wait_rounds }] —— wait_options/wait_answers 透传 DB JSON 字符串
|
|
383
|
+
* batch_progress [{ change_name, total, completed, failed, skipped }]
|
|
384
|
+
* approvals [{ change_name, status, requested_at, approved_by, approved_at, rejection_reason }]
|
|
385
|
+
* 可空列归一为 null(非 undefined,保证 JSON 往返键稳定);不含 user/base_ts/pushed_at(D-015,sync.js 放 HTTP header)。
|
|
386
|
+
*
|
|
387
|
+
* @param {string} cwd
|
|
388
|
+
* @param {string|null} [changeName] - 空时单活跃变更自动推导(对齐 read());多/零活跃返回 null
|
|
389
|
+
* @returns {object|null} 六键同步 JSON;change 不存在或无法推导返回 null
|
|
390
|
+
*/
|
|
391
|
+
serializeForSync(cwd, changeName = null) {
|
|
392
|
+
// 自动检测变更名(与 read() 同口径):单活跃变更推导,多/零无法确定返回 null
|
|
393
|
+
if (!changeName) {
|
|
394
|
+
const changes = this.listChanges(cwd);
|
|
395
|
+
if (changes.length === 1) {
|
|
396
|
+
changeName = changes[0];
|
|
397
|
+
} else {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const db = this._ensureDB(cwd);
|
|
403
|
+
const sqlDb = db.getDb();
|
|
404
|
+
|
|
405
|
+
// 1. changes 行(投影流程进度列 + name 标识)
|
|
406
|
+
const changeRow = sqlDb.prepare(
|
|
407
|
+
`SELECT id, name, current_stage, status, last_active, last_synced_platform_ts, last_local_modified_ts
|
|
408
|
+
FROM changes WHERE name = ?`
|
|
409
|
+
).get(changeName);
|
|
410
|
+
if (changeRow === undefined) return null;
|
|
411
|
+
const changeId = changeRow.id;
|
|
412
|
+
const changeProj = {
|
|
413
|
+
name: changeRow.name,
|
|
414
|
+
current_stage: changeRow.current_stage,
|
|
415
|
+
status: changeRow.status,
|
|
416
|
+
last_active: changeRow.last_active,
|
|
417
|
+
last_synced_platform_ts: changeRow.last_synced_platform_ts ?? null,
|
|
418
|
+
last_local_modified_ts: changeRow.last_local_modified_ts ?? null,
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// 2. project 全局单行(只投影稳定字段 name/schema_version;created_at/updated_at 是本地库元数据,
|
|
422
|
+
// 同步无意义且破坏 round-trip 跨库等值——与 changes 排除 created_at 同理,B2)
|
|
423
|
+
const projectRow = sqlDb.prepare(
|
|
424
|
+
'SELECT name, schema_version FROM project WHERE id = 1'
|
|
425
|
+
).get();
|
|
426
|
+
|
|
427
|
+
// 3. stages(该 change 下,用 change_name+stage 表达外键)
|
|
428
|
+
const stageRows = sqlDb.prepare(
|
|
429
|
+
`SELECT id, stage, status, started_at, completed_at, revision, reopened_from_step, reopened_at, stale_reason
|
|
430
|
+
FROM stages WHERE change_id = ? ORDER BY id`
|
|
431
|
+
).all(changeId);
|
|
432
|
+
const stageJson = stageRows.map(r => ({
|
|
433
|
+
change_name: changeRow.name,
|
|
434
|
+
stage: r.stage,
|
|
435
|
+
status: r.status,
|
|
436
|
+
started_at: r.started_at ?? null,
|
|
437
|
+
completed_at: r.completed_at ?? null,
|
|
438
|
+
revision: r.revision ?? null,
|
|
439
|
+
reopened_from_step: r.reopened_from_step ?? null,
|
|
440
|
+
reopened_at: r.reopened_at ?? null,
|
|
441
|
+
stale_reason: r.stale_reason ?? null,
|
|
442
|
+
}));
|
|
443
|
+
|
|
444
|
+
// 4. steps(stage_id IN;用 change_name+stage 关联回 stage)
|
|
445
|
+
const stageIds = stageRows.map(r => r.id);
|
|
446
|
+
const stageIdToName = new Map(stageRows.map(r => [r.id, r.stage]));
|
|
447
|
+
let stepJson = [];
|
|
448
|
+
if (stageIds.length > 0) {
|
|
449
|
+
const placeholders = stageIds.map(() => '?').join(',');
|
|
450
|
+
const stepRows = sqlDb.prepare(
|
|
451
|
+
`SELECT stage_id, name, status, output, completed_at, ordering, wait_reason, wait_options,
|
|
452
|
+
wait_answer, waited_at, wait_answers, wait_round, max_wait_rounds
|
|
453
|
+
FROM steps WHERE stage_id IN (${placeholders}) ORDER BY stage_id, ordering`
|
|
454
|
+
).all(...stageIds);
|
|
455
|
+
stepJson = stepRows.map(r => ({
|
|
456
|
+
change_name: changeRow.name,
|
|
457
|
+
stage: stageIdToName.get(r.stage_id),
|
|
458
|
+
name: r.name,
|
|
459
|
+
status: r.status,
|
|
460
|
+
output: r.output ?? null,
|
|
461
|
+
completed_at: r.completed_at ?? null,
|
|
462
|
+
ordering: r.ordering ?? null,
|
|
463
|
+
wait_reason: r.wait_reason ?? null,
|
|
464
|
+
wait_options: r.wait_options ?? null,
|
|
465
|
+
wait_answer: r.wait_answer ?? null,
|
|
466
|
+
waited_at: r.waited_at ?? null,
|
|
467
|
+
wait_answers: r.wait_answers ?? null,
|
|
468
|
+
wait_round: r.wait_round ?? null,
|
|
469
|
+
max_wait_rounds: r.max_wait_rounds ?? null,
|
|
470
|
+
}));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// 5. batch_progress(无行则空数组,import 侧幂等)
|
|
474
|
+
const batchRow = sqlDb.prepare(
|
|
475
|
+
'SELECT total, completed, failed, skipped FROM batch_progress WHERE change_id = ?'
|
|
476
|
+
).get(changeId);
|
|
477
|
+
const batchJson = batchRow !== undefined ? [{
|
|
478
|
+
change_name: changeRow.name,
|
|
479
|
+
total: batchRow.total,
|
|
480
|
+
completed: batchRow.completed,
|
|
481
|
+
failed: batchRow.failed,
|
|
482
|
+
skipped: batchRow.skipped,
|
|
483
|
+
}] : [];
|
|
484
|
+
|
|
485
|
+
// 6. approvals(read() 不读此表,serializeForSync 补齐;无行则空数组)
|
|
486
|
+
const approvalRow = sqlDb.prepare(
|
|
487
|
+
'SELECT status, requested_at, approved_by, approved_at, rejection_reason FROM approvals WHERE change_id = ?'
|
|
488
|
+
).get(changeId);
|
|
489
|
+
const approvalJson = approvalRow !== undefined ? [{
|
|
490
|
+
change_name: changeRow.name,
|
|
491
|
+
status: approvalRow.status,
|
|
492
|
+
requested_at: approvalRow.requested_at ?? null,
|
|
493
|
+
approved_by: approvalRow.approved_by ?? null,
|
|
494
|
+
approved_at: approvalRow.approved_at ?? null,
|
|
495
|
+
rejection_reason: approvalRow.rejection_reason ?? null,
|
|
496
|
+
}] : [];
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
project: projectRow || null,
|
|
500
|
+
changes: [changeProj],
|
|
501
|
+
stages: stageJson,
|
|
502
|
+
steps: stepJson,
|
|
503
|
+
batch_progress: batchJson,
|
|
504
|
+
approvals: approvalJson,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* serializeForSync 的逆运算:把平台权威 JSON 原子写回本地 DB 的该 change 行(design §8/B1,task-03 / D-005@v2 / D-011 / FR-07)。
|
|
510
|
+
* ⚠️ 必须保护真实资产:import 前对 DB 做【独立】snapshot `.runtime/sillyspec.db.pre-import-<ts>.bak`,
|
|
511
|
+
* 不抢 _openWithFallback 的 `${dbPath}.bak`(gap5/R-08);任何失败本地进度库不损坏。
|
|
512
|
+
*
|
|
513
|
+
* 语义:
|
|
514
|
+
* - 单个 DB.transaction() 包裹,原子重建 stages/steps/batch_progress/approvals 四表(任一失败整体回滚)
|
|
515
|
+
* - changes 行用 UPDATE 选择投影列(current_stage/status/last_active/last_synced_platform_ts/last_local_modified_ts),
|
|
516
|
+
* 保留 isolation_* / platform_* / created_at(本地强相关状态不被覆盖,B2;change 不存在时 INSERT 兜底平台新增)
|
|
517
|
+
* - import 后 last_synced_platform_ts 与 last_local_modified_ts 均置为 progressObj.pushed_at(D-013 例外:
|
|
518
|
+
* 不更新 now()——否则 now()>base_ts 下次 pull 误判冲突;pushed_at 由 sync.js pull() 从响应 header attach)
|
|
519
|
+
* - 本地确定性操作,失败 throw 中文(非 sync 类 Best Effort,CONVENTIONS #4)
|
|
520
|
+
*
|
|
521
|
+
* @param {string} cwd
|
|
522
|
+
* @param {object} progressObj - serializeForSync 输出 + sync.js 附加的 pushed_at
|
|
523
|
+
* @param {string|null} [changeName] - 空则从 progressObj.changes[0].name 推导
|
|
524
|
+
* @returns {ImportResult} { ok:true, imported:changeName, reason?, bakPath }
|
|
525
|
+
*/
|
|
526
|
+
import(cwd, progressObj, changeName = null) {
|
|
527
|
+
if (!progressObj || typeof progressObj !== 'object') {
|
|
528
|
+
throw new Error('import 参数无效:progressObj 必须是 serializeForSync() 输出的六表 JSON 对象');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const cn = changeName || (progressObj.changes && progressObj.changes[0] && progressObj.changes[0].name);
|
|
532
|
+
if (!cn) {
|
|
533
|
+
throw new Error('import 缺少 changeName:无法定位目标 change(progressObj.changes[0].name 为空)');
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ⚠️ 必须保护真实资产:独立 .bak snapshot(前缀 pre-import- 区分于主 .bak 备份机制)
|
|
537
|
+
const dbPath = this._runtimePath(cwd, 'sillyspec.db');
|
|
538
|
+
if (!existsSync(dbPath)) {
|
|
539
|
+
throw new Error(`本地进度库不存在,无法 import:${dbPath}`);
|
|
540
|
+
}
|
|
541
|
+
const bakTs = new Date().toISOString().replace(/[:.]/g, '-');
|
|
542
|
+
const bakPath = this._runtimePath(cwd, `sillyspec.db.pre-import-${bakTs}.bak`);
|
|
543
|
+
copyFileSync(dbPath, bakPath);
|
|
544
|
+
|
|
545
|
+
const pushedAt = progressObj.pushed_at ?? null;
|
|
546
|
+
const now = new Date().toISOString();
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
const db = this._ensureDB(cwd);
|
|
550
|
+
db.transaction(() => {
|
|
551
|
+
const sqlDb = db.getDb();
|
|
552
|
+
|
|
553
|
+
// 1. changes 行:INSERT OR IGNORE 兜底平台新增 change;UPDATE 只覆盖投影列(保留本地隔离/平台/created_at 列)
|
|
554
|
+
sqlDb.prepare(
|
|
555
|
+
`INSERT OR IGNORE INTO changes (name, current_stage, status, created_at, last_active)
|
|
556
|
+
VALUES (?, '', 'active', ?, ?)`
|
|
557
|
+
).run(cn, now, now);
|
|
558
|
+
const ch = progressObj.changes && progressObj.changes[0];
|
|
559
|
+
sqlDb.prepare(
|
|
560
|
+
`UPDATE changes SET
|
|
561
|
+
current_stage = ?, status = ?, last_active = ?,
|
|
562
|
+
last_synced_platform_ts = ?, last_local_modified_ts = ?
|
|
563
|
+
WHERE name = ?`
|
|
564
|
+
).run(
|
|
565
|
+
ch && ch.current_stage != null ? ch.current_stage : '',
|
|
566
|
+
ch && ch.status != null ? ch.status : 'active',
|
|
567
|
+
ch && ch.last_active != null ? ch.last_active : now,
|
|
568
|
+
pushedAt, pushedAt, cn
|
|
569
|
+
);
|
|
570
|
+
const changeRow = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(cn);
|
|
571
|
+
const changeId = changeRow.id;
|
|
572
|
+
|
|
573
|
+
// 2. project 行:幂等确保存在(不覆盖已有全局行,round-trip 等值不受影响;仅补缺失)
|
|
574
|
+
const projExists = sqlDb.prepare('SELECT id FROM project WHERE id = 1').get();
|
|
575
|
+
if (projExists === undefined) {
|
|
576
|
+
const pj = progressObj.project || {};
|
|
577
|
+
sqlDb.prepare(
|
|
578
|
+
`INSERT INTO project (id, name, schema_version, created_at, updated_at)
|
|
579
|
+
VALUES (1, ?, ?, ?, ?)`
|
|
580
|
+
).run(pj.name != null ? pj.name : 'project', pj.schema_version != null ? pj.schema_version : CURRENT_VERSION, now, now);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// 3. 重建 stages + steps:DELETE stages(ON DELETE CASCADE 级联删 steps),按 JSON 重建
|
|
584
|
+
sqlDb.prepare('DELETE FROM stages WHERE change_id = ?').run(changeId);
|
|
585
|
+
const stageToId = new Map();
|
|
586
|
+
for (const s of (progressObj.stages || [])) {
|
|
587
|
+
if (!s || !s.stage) continue;
|
|
588
|
+
sqlDb.prepare(
|
|
589
|
+
`INSERT INTO stages (change_id, stage, status, started_at, completed_at, revision, reopened_from_step, reopened_at, stale_reason)
|
|
590
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
591
|
+
).run(changeId, s.stage, s.status != null ? s.status : 'pending', s.started_at ?? null, s.completed_at ?? null,
|
|
592
|
+
s.revision != null ? s.revision : 0, s.reopened_from_step ?? null, s.reopened_at ?? null, s.stale_reason ?? null);
|
|
593
|
+
const row = sqlDb.prepare('SELECT id FROM stages WHERE change_id = ? AND stage = ?').get(changeId, s.stage);
|
|
594
|
+
stageToId.set(s.stage, row.id);
|
|
595
|
+
}
|
|
596
|
+
for (const st of (progressObj.steps || [])) {
|
|
597
|
+
if (!st || !st.name) continue;
|
|
598
|
+
const stageId = stageToId.get(st.stage);
|
|
599
|
+
if (stageId === undefined) continue; // 孤儿 step(stage 不在 JSON)跳过,不破坏完整性
|
|
600
|
+
sqlDb.prepare(
|
|
601
|
+
`INSERT INTO steps (stage_id, name, status, output, completed_at, ordering, wait_reason, wait_options, wait_answer, waited_at, wait_answers, wait_round, max_wait_rounds)
|
|
602
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
603
|
+
).run(stageId, st.name, st.status != null ? st.status : 'pending', st.output ?? null, st.completed_at ?? null,
|
|
604
|
+
st.ordering != null ? st.ordering : 0, st.wait_reason ?? null, st.wait_options ?? null, st.wait_answer ?? null,
|
|
605
|
+
st.waited_at ?? null, st.wait_answers ?? null, st.wait_round ?? null, st.max_wait_rounds ?? null);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// 4. 重建 batch_progress(DELETE 再插,无行则清空)
|
|
609
|
+
sqlDb.prepare('DELETE FROM batch_progress WHERE change_id = ?').run(changeId);
|
|
610
|
+
const bp = progressObj.batch_progress && progressObj.batch_progress[0];
|
|
611
|
+
if (bp) {
|
|
612
|
+
sqlDb.prepare(
|
|
613
|
+
`INSERT INTO batch_progress (change_id, total, completed, failed, skipped)
|
|
614
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
615
|
+
).run(changeId, bp.total != null ? bp.total : 0, bp.completed != null ? bp.completed : 0,
|
|
616
|
+
bp.failed != null ? bp.failed : 0, bp.skipped != null ? bp.skipped : 0);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// 5. 重建 approvals(DELETE 再插,无行则清空)
|
|
620
|
+
sqlDb.prepare('DELETE FROM approvals WHERE change_id = ?').run(changeId);
|
|
621
|
+
const ap = progressObj.approvals && progressObj.approvals[0];
|
|
622
|
+
if (ap) {
|
|
623
|
+
sqlDb.prepare(
|
|
624
|
+
`INSERT INTO approvals (change_id, status, requested_at, approved_by, approved_at, rejection_reason)
|
|
625
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
626
|
+
).run(changeId, ap.status != null ? ap.status : 'not_required', ap.requested_at ?? null, ap.approved_by ?? null,
|
|
627
|
+
ap.approved_at ?? null, ap.rejection_reason ?? null);
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
} catch (e) {
|
|
631
|
+
// 事务失败自动回滚(DB 未变),.bak 保留可恢复;fail-loud 中文
|
|
632
|
+
throw new Error(`import 失败,本地进度库已回滚(snapshot: ${bakPath}):${e.message}`);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return { ok: true, imported: cn, reason: undefined, bakPath };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* 本地脏度标记(D-013 / task-04):写入路径末尾更新 changes.last_local_modified_ts。
|
|
640
|
+
* 在写方法的事务内调用则属于该事务(原子,失败整体回滚);import 是例外不调本方法
|
|
641
|
+
* (import 自己把 last_local_modified_ts 置为 pushed_at,表示本地=平台干净)。
|
|
642
|
+
* @param {string} cwd
|
|
643
|
+
* @param {string} changeName
|
|
644
|
+
* @param {string} [ts] - 复用调用方事务内的时间戳,保证与本次写入同时刻
|
|
645
|
+
*/
|
|
646
|
+
_touchLocalModified(cwd, changeName, ts = null) {
|
|
647
|
+
if (!changeName) return;
|
|
648
|
+
const db = this._ensureDB(cwd);
|
|
649
|
+
const stamp = ts || new Date().toISOString();
|
|
650
|
+
db.getDb().prepare('UPDATE changes SET last_local_modified_ts = ? WHERE name = ?').run(stamp, changeName);
|
|
651
|
+
}
|
|
652
|
+
|
|
367
653
|
/**
|
|
368
654
|
* 写入指定变更的 progress
|
|
369
655
|
* @param {string} cwd
|
|
@@ -392,12 +678,12 @@ export class ProgressManager {
|
|
|
392
678
|
const db = this._ensureDB(cwd);
|
|
393
679
|
db.transaction(() => {
|
|
394
680
|
const sqlDb = db.getDb();
|
|
395
|
-
// 1. 更新 changes
|
|
681
|
+
// 1. 更新 changes 表(last_local_modified_ts 脏度:D-013 / task-04)
|
|
396
682
|
const now = new Date().toISOString();
|
|
397
683
|
const noWorktree = data.noWorktree ? 1 : 0;
|
|
398
684
|
sqlDb.prepare(
|
|
399
|
-
'UPDATE changes SET current_stage = ?, last_active = ?, no_worktree = ? WHERE name = ?'
|
|
400
|
-
).run(data.currentStage || '', now, noWorktree, cn);
|
|
685
|
+
'UPDATE changes SET current_stage = ?, last_active = ?, no_worktree = ?, last_local_modified_ts = ? WHERE name = ?'
|
|
686
|
+
).run(data.currentStage || '', now, noWorktree, now, cn);
|
|
401
687
|
|
|
402
688
|
// 2. 获取 change_id
|
|
403
689
|
const changeRow = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(cn);
|
|
@@ -490,6 +776,8 @@ export class ProgressManager {
|
|
|
490
776
|
|
|
491
777
|
unregisterChange(cwd, changeName) { return this._changeRegistry.unregisterChange(cwd, changeName); }
|
|
492
778
|
|
|
779
|
+
updateChangeMeta(cwd, changeName, meta) { return this._changeRegistry.updateChangeMeta(cwd, changeName, meta); }
|
|
780
|
+
|
|
493
781
|
// ── CLI 命令 ──
|
|
494
782
|
|
|
495
783
|
init(cwd) {
|
|
@@ -529,7 +817,7 @@ export class ProgressManager {
|
|
|
529
817
|
* 初始化指定变更的 progress
|
|
530
818
|
* SQL: INSERT changes + 批量 INSERT stages
|
|
531
819
|
*/
|
|
532
|
-
initChange(cwd, changeName) {
|
|
820
|
+
initChange(cwd, changeName, meta = {}) {
|
|
533
821
|
if (!changeName) {
|
|
534
822
|
console.warn('⚠️ initChange: changeName 为空,跳过');
|
|
535
823
|
return null;
|
|
@@ -550,9 +838,9 @@ export class ProgressManager {
|
|
|
550
838
|
if (existing === undefined) {
|
|
551
839
|
// 插入 changes 行
|
|
552
840
|
sqlDb.prepare(
|
|
553
|
-
`INSERT INTO changes (name, current_stage, status, created_at, last_active)
|
|
554
|
-
VALUES (?, 'scan', 'active', ?, ?)`
|
|
555
|
-
).run(changeName, now, now);
|
|
841
|
+
`INSERT INTO changes (name, current_stage, status, created_at, last_active, title, quicklog_id)
|
|
842
|
+
VALUES (?, 'scan', 'active', ?, ?, ?, ?)`
|
|
843
|
+
).run(changeName, now, now, meta.title || null, meta.quicklogId || null);
|
|
556
844
|
}
|
|
557
845
|
|
|
558
846
|
// 获取 change_id
|
|
@@ -566,6 +854,8 @@ export class ProgressManager {
|
|
|
566
854
|
VALUES (?, ?, 'pending')`
|
|
567
855
|
).run(changeId, stage);
|
|
568
856
|
}
|
|
857
|
+
// 本地脏度(D-013 / task-04):新建 change 视为有未同步数据
|
|
858
|
+
this._touchLocalModified(cwd, changeName, now);
|
|
569
859
|
});
|
|
570
860
|
|
|
571
861
|
// 不再需要写文件:read() 已改为 SQL
|
|
@@ -620,7 +910,7 @@ export class ProgressManager {
|
|
|
620
910
|
db.transaction(() => {
|
|
621
911
|
const sqlDb = db.getDb();
|
|
622
912
|
const now = new Date().toISOString();
|
|
623
|
-
sqlDb.prepare(
|
|
913
|
+
const ins = sqlDb.prepare(
|
|
624
914
|
`INSERT OR IGNORE INTO changes (name, current_stage, status, created_at, last_active) VALUES (?, 'scan', 'active', ?, ?)`
|
|
625
915
|
).run(changeName, now, now);
|
|
626
916
|
const changeRow = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(changeName);
|
|
@@ -630,6 +920,8 @@ export class ProgressManager {
|
|
|
630
920
|
sqlDb.prepare(`INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, 'pending')`).run(changeId, s);
|
|
631
921
|
}
|
|
632
922
|
}
|
|
923
|
+
// 只在真正创建 change 时标脏(D-013/task-04);读路径(change 已存在)不标,避免 pull 误判本地有未同步改动
|
|
924
|
+
if (ins.changes > 0) this._touchLocalModified(cwd, changeName, now);
|
|
633
925
|
});
|
|
634
926
|
this.registerChange(cwd, changeName);
|
|
635
927
|
}
|
package/src/quicklog.js
CHANGED
|
@@ -75,7 +75,7 @@ function nowDatetime(d = new Date()) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// 描述压成一行、限长,避免破坏 QUICKLOG 条目结构
|
|
78
|
-
function sanitizeDesc(description) {
|
|
78
|
+
export function sanitizeDesc(description) {
|
|
79
79
|
const s = String(description || '').replace(/[\r\n]+/g, ' ').trim()
|
|
80
80
|
if (!s) return '(quick 任务)'
|
|
81
81
|
return s.length > 120 ? s.slice(0, 120) + '…' : s
|
|
@@ -101,7 +101,7 @@ export function deriveTitleFromLinkedChange(specBase, change) {
|
|
|
101
101
|
|
|
102
102
|
// 从 quick step3 --output 四字段提取「需求:」摘要,用于翻完成时刷新标题行
|
|
103
103
|
// (覆盖启动时的占位/弱标题)。提取失败返回 ''(不刷新)。
|
|
104
|
-
function extractTitleFromResult(result) {
|
|
104
|
+
export function extractTitleFromResult(result) {
|
|
105
105
|
if (!result) return ''
|
|
106
106
|
const m = String(result).match(/需求:([^\n\r]*?)(?:\s+根因:|$)/)
|
|
107
107
|
if (!m) return ''
|
|
@@ -117,6 +117,36 @@ function sanitizeResult(resultText) {
|
|
|
117
117
|
return String(resultText || '').trim()
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
// ── quick step3 --file-notes 旁路通道 ──
|
|
121
|
+
// flag 在 command.js 解析(--file-notes),completeQuicklogEntry 在 quick 收尾时消费。
|
|
122
|
+
// 不经 completeStep → handleQuickStageCompletion → completeQuicklogEntry 三层透传(会扩 3 处签名,
|
|
123
|
+
// 且 quick 收尾路径是多 agent 并发改热点);用 per-process setter,CLI 单进程生命周期内有效,
|
|
124
|
+
// completeQuicklogEntry 读后即清。无 --file-notes 时为 '' → 回退 changedFiles 单行(向后兼容)。
|
|
125
|
+
let _pendingFileNotes = ''
|
|
126
|
+
export function setQuickFileNotes(raw) { _pendingFileNotes = raw == null ? '' : String(raw) }
|
|
127
|
+
export function getQuickFileNotes() { return _pendingFileNotes }
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 解析 --file-notes 原文 → [{path, note}]。
|
|
131
|
+
* 格式:「path1::括注1 || path2::括注2」——`||` 分隔条目,`::` 分隔路径与括注(首个 `::` 为界,
|
|
132
|
+
* 括注内含 `::` 不误切);路径反斜杠归一正斜杠(匹配 git 路径风格)。无 `::` 的段 → note=''。
|
|
133
|
+
* 空 / 全空段 → [](调用方回退 changedFiles 单行)。导出供测试 + 未来直接调用。
|
|
134
|
+
*/
|
|
135
|
+
export function parseFileNotes(raw) {
|
|
136
|
+
if (!raw) return []
|
|
137
|
+
return String(raw)
|
|
138
|
+
.split('||')
|
|
139
|
+
.map((seg) => {
|
|
140
|
+
const s = seg.trim()
|
|
141
|
+
if (!s) return null
|
|
142
|
+
const idx = s.indexOf('::')
|
|
143
|
+
const path = (idx === -1 ? s : s.slice(0, idx)).replace(/\\/g, '/').trim()
|
|
144
|
+
const note = idx === -1 ? '' : s.slice(idx + 2).trim()
|
|
145
|
+
return path ? { path, note } : null
|
|
146
|
+
})
|
|
147
|
+
.filter(Boolean)
|
|
148
|
+
}
|
|
149
|
+
|
|
120
150
|
// 结果块必填字段(quick step3 --done --output 的结构化结果模板,见 src/stages/quick.js)。
|
|
121
151
|
// 4 个必填字段标签从 manifest 同源(stage-contract-spec.js quick.result-labels),prompt 事前契约与本校验单源。
|
|
122
152
|
const RESULT_REQUIRED_LABELS = getRule('quick.result-labels').data.literals
|
|
@@ -284,7 +314,7 @@ function splitSingleLineFields(body) {
|
|
|
284
314
|
return scanFields(body, findBoundaryLabel) ?? scanFields(body, (b, l, f) => b.indexOf(l, f))
|
|
285
315
|
}
|
|
286
316
|
|
|
287
|
-
function flipEntryInContent(content, qlId, result, changedFiles = []) {
|
|
317
|
+
function flipEntryInContent(content, qlId, result, changedFiles = [], fileNotes = []) {
|
|
288
318
|
const lines = content.split('\n')
|
|
289
319
|
const startIdx = lines.findIndex(l => l.startsWith(`## ${qlId} |`))
|
|
290
320
|
if (startIdx === -1) return null
|
|
@@ -303,11 +333,16 @@ function flipEntryInContent(content, qlId, result, changedFiles = []) {
|
|
|
303
333
|
for (let i = startIdx + 1; i < endIdx; i++) {
|
|
304
334
|
if (/^状态:进行中\r?$/.test(lines[i])) { lines[i] = '状态:已完成'; flipped = true }
|
|
305
335
|
if (lines[i].startsWith('结果:')) hasResult = true
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
if (
|
|
310
|
-
|
|
336
|
+
// 文件行落盘:--file-notes 优先(多行 bullet 带括注),否则回填审计到的实际改动文件单行,
|
|
337
|
+
// 都空则不动(保持「(见实际改动)」)。bullet 用「数组元素内嵌 \n」写——lines.join('\n') 展平为
|
|
338
|
+
// 多行,数组长度不变 → 与下方状态/结果 splice 的索引完全解耦(changedFiles 单行同理原地替换)。
|
|
339
|
+
if (lines[i].startsWith('文件:')) {
|
|
340
|
+
if (fileNotes.length > 0) {
|
|
341
|
+
const bullets = fileNotes.map((n) => `- ${n.path}${n.note ? `(${n.note})` : ''}`)
|
|
342
|
+
lines[i] = `文件:\n${bullets.join('\n')}`
|
|
343
|
+
} else if (changedFiles.length > 0) {
|
|
344
|
+
lines[i] = `文件:${changedFiles.join(', ')}`
|
|
345
|
+
}
|
|
311
346
|
}
|
|
312
347
|
}
|
|
313
348
|
if (!flipped && !lines.slice(startIdx, endIdx).some(l => /^状态:已完成\r?$/.test(l))) {
|
|
@@ -405,6 +440,10 @@ export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultTex
|
|
|
405
440
|
const linked = Array.isArray(linkedChanges) ? linkedChanges : []
|
|
406
441
|
// 实际改动文件(调用方 complete-handlers 已用 isQuickMetadata 过滤 quick 自身元数据)。空 → 不动文件行。
|
|
407
442
|
const realFiles = Array.isArray(changedFiles) ? changedFiles.filter(Boolean) : []
|
|
443
|
+
// --file-notes(command.js 经 setQuickFileNotes 注入):有则文件行写多行 bullet 带括注。
|
|
444
|
+
// 读后即清(per-process,防残留跨调用)。flipEntryInContent 优先用 fileNotes,空则回退 realFiles。
|
|
445
|
+
const fileNotes = parseFileNotes(_pendingFileNotes)
|
|
446
|
+
_pendingFileNotes = ''
|
|
408
447
|
|
|
409
448
|
await withFileLock(lockPath, async () => {
|
|
410
449
|
// 条目可能在主文件或轮转归档中
|
|
@@ -412,7 +451,7 @@ export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultTex
|
|
|
412
451
|
const filePath = join(quicklogDir, f)
|
|
413
452
|
let content = ''
|
|
414
453
|
try { content = readFileSync(filePath, 'utf8') } catch { continue }
|
|
415
|
-
const updated = flipEntryInContent(content, qlId, result, realFiles)
|
|
454
|
+
const updated = flipEntryInContent(content, qlId, result, realFiles, fileNotes)
|
|
416
455
|
if (updated !== null) {
|
|
417
456
|
await writeAtomic(filePath, updated) // 命中处原子落盘(只改含目标条目的那一个文件)
|
|
418
457
|
break
|
package/src/run/command.js
CHANGED
|
@@ -32,6 +32,7 @@ import { ProgressManager } from '../progress.js'
|
|
|
32
32
|
import { validateChangeExists } from '../stage-contract.js'
|
|
33
33
|
import { stageRegistry, auxiliaryStages } from '../stages/index.js'
|
|
34
34
|
import { definition as brainstormAutoDef } from '../stages/brainstorm-auto.js'
|
|
35
|
+
import { setQuickFileNotes } from '../quicklog.js'
|
|
35
36
|
|
|
36
37
|
// F2/F4: 哪些 flag 吃下一个 token 作「值」(其余 --flag 都是布尔,不吞值)。
|
|
37
38
|
// 校验循环只对 VALUE_FLAGS 跳下一个 token(否则 --done 后跟 typo 会被当 --done 的值吞掉)。
|
|
@@ -40,7 +41,7 @@ const VALUE_FLAGS = new Set([
|
|
|
40
41
|
'--reason', '--options', '--answer', '--confirm-mode',
|
|
41
42
|
'--output', '--input', '--change', '--linked-changes',
|
|
42
43
|
'--spec-dir', '--spec-root', '--runtime-root', '--workspace-id', '--scan-run-id',
|
|
43
|
-
'--files', '--from-step', '--mode', '--dir', '--confirm-mode',
|
|
44
|
+
'--files', '--file-notes', '--from-step', '--mode', '--dir', '--confirm-mode',
|
|
44
45
|
])
|
|
45
46
|
|
|
46
47
|
/**
|
|
@@ -449,6 +450,16 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
449
450
|
quickFiles = flags[filesIdx + 1].split(',').map(f => f.trim()).filter(Boolean)
|
|
450
451
|
}
|
|
451
452
|
|
|
453
|
+
// 解析 --file-notes "path::括注 || path::括注"(quick 专用:QUICKLOG「文件:」行落盘多行括注)。
|
|
454
|
+
// 旁路注入 quicklog.js(per-process setter),不经 complete 收尾路径透传(避碰多 agent 并发改的收尾热点)。
|
|
455
|
+
// completeQuicklogEntry 读后即清;不传 → '' → 回填审计到的实际文件单行(向后兼容)。
|
|
456
|
+
let quickFileNotes = ''
|
|
457
|
+
const fileNotesIdx = flags.indexOf('--file-notes')
|
|
458
|
+
if (fileNotesIdx !== -1 && flags[fileNotesIdx + 1]) {
|
|
459
|
+
quickFileNotes = flags[fileNotesIdx + 1]
|
|
460
|
+
}
|
|
461
|
+
setQuickFileNotes(quickFileNotes)
|
|
462
|
+
|
|
452
463
|
const isAllowNew = flags.includes('--allow-new')
|
|
453
464
|
const isForceBaseline = flags.includes('--force-baseline')
|
|
454
465
|
const isForceRescan = flags.includes('--force-rescan')
|
|
@@ -460,7 +471,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
460
471
|
'--reason', '--options', '--answer', '--confirm-mode',
|
|
461
472
|
'--output', '--input', '--change', '--linked-changes',
|
|
462
473
|
'--spec-dir', '--spec-root', '--runtime-root', '--workspace-id', '--scan-run-id',
|
|
463
|
-
'--files', '--allow-new', '--force-baseline', '--force-rescan',
|
|
474
|
+
'--files', '--file-notes', '--allow-new', '--force-baseline', '--force-rescan',
|
|
464
475
|
'--json', '--dir', '--help',
|
|
465
476
|
'--reopen', '--from-step', '--mode',
|
|
466
477
|
'--deep', '--quick', '--standard', // scan profile 三档显式选择(scan-profile.js 从 argv 读;互斥见下方 PROFILE_FLAGS 检测)
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
import { basename, join, resolve, relative, isAbsolute } from 'node:path'
|
|
26
26
|
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, rmSync } from 'node:fs'
|
|
27
27
|
import { renameSyncRetry, writeAtomicSync } from '../fs-atomic.js'
|
|
28
|
-
import { resolveChangeDir, resolveQuickSessionsDir, safeGit, auditQuickCompletion, triggerSync, isQuickMetadata } from './shared.js'
|
|
28
|
+
import { resolveChangeDir, resolveQuickSessionsDir, safeGit, auditQuickCompletion, triggerSync, isQuickMetadata, resolveRuntimeRoot } from './shared.js'
|
|
29
29
|
import { detectConcurrentChanges, formatConcurrentWarning } from './concurrent-detect.js'
|
|
30
30
|
import { stageRegistry } from '../stages/index.js'
|
|
31
31
|
import { SCAN_STATUS, POINTER_STATUS } from '../constants.js'
|
|
32
32
|
import { printQuickAuditReview } from './quick-audit.js'
|
|
33
|
-
import { validateQuickResult, allocateQuicklogEntry, findQuicklogEntry, completeQuicklogEntry } from '../quicklog.js'
|
|
33
|
+
import { validateQuickResult, allocateQuicklogEntry, findQuicklogEntry, completeQuicklogEntry, extractTitleFromResult } from '../quicklog.js'
|
|
34
34
|
import { getRule } from '../stage-contract-spec.js'
|
|
35
35
|
import { archiveDestDirName } from '../stage-contract.js'
|
|
36
36
|
|
|
@@ -130,9 +130,32 @@ export function findAlreadyArchivedDir(archiveDir, changeName) {
|
|
|
130
130
|
/**
|
|
131
131
|
* 归档时清理可能残留的 worktree(execute 自动清理未走到 / 有未 apply 变更被遗弃)。
|
|
132
132
|
* 安全策略:有未 apply 变更时保留 worktree 并警告,避免误删用户未应用的代码。
|
|
133
|
+
* 同时清理该变更的 execute / stage-review runId marker(只写不删的累积物,
|
|
134
|
+
* 见 stage.js execute 启动固定 executeRunId / stage-review.js stageReviewMarkerPath)。
|
|
133
135
|
* 从 archiveChangeDirectory 抽出,供正常归档 + 自愈归档复用。
|
|
134
136
|
*/
|
|
135
|
-
async function archiveWorktreeCleanup(cwd, archiveChangeName) {
|
|
137
|
+
async function archiveWorktreeCleanup(cwd, archiveChangeName, specBase, platformOpts = {}) {
|
|
138
|
+
// ── 清理 runId marker:execute(current-execute-run-id-<change>)与 stage-review
|
|
139
|
+
// (current-stage-review-run-id-<stage>-<change>)。marker 只服务 execute→verify→archive
|
|
140
|
+
// 期间,归档后无读者;不删则 .runtime 随变更数无限累积。runtimeRoot 解析同写入侧
|
|
141
|
+
// (resolveRuntimeRoot 锚主仓),避免平台模式清理错位置。
|
|
142
|
+
try {
|
|
143
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
144
|
+
if (existsSync(runtimeRoot)) {
|
|
145
|
+
const markers = readdirSync(runtimeRoot).filter((f) =>
|
|
146
|
+
f === `current-execute-run-id-${archiveChangeName}` ||
|
|
147
|
+
(f.startsWith('current-stage-review-run-id-') && f.endsWith(`-${archiveChangeName}`)),
|
|
148
|
+
)
|
|
149
|
+
for (const m of markers) {
|
|
150
|
+
try { unlinkSync(join(runtimeRoot, m)) } catch {}
|
|
151
|
+
}
|
|
152
|
+
if (markers.length > 0) {
|
|
153
|
+
console.log(`🧹 归档清理 ${markers.length} 个 runId marker(execute/stage-review)`)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch (e) {
|
|
157
|
+
console.warn(`⚠️ 归档 runId marker 清理失败(不阻断归档): ${e.message}`)
|
|
158
|
+
}
|
|
136
159
|
try {
|
|
137
160
|
const { WorktreeManager } = await import('../worktree.js')
|
|
138
161
|
const wm = new WorktreeManager({ cwd })
|
|
@@ -154,7 +177,7 @@ async function archiveWorktreeCleanup(cwd, archiveChangeName) {
|
|
|
154
177
|
}
|
|
155
178
|
}
|
|
156
179
|
|
|
157
|
-
export async function archiveChangeDirectory(pm, cwd, progress, specBase) {
|
|
180
|
+
export async function archiveChangeDirectory(pm, cwd, progress, specBase, platformOpts = {}) {
|
|
158
181
|
const archiveChangeName = progress.currentChange
|
|
159
182
|
if (!archiveChangeName) {
|
|
160
183
|
console.error('❌ 归档失败:未找到当前变更名(currentChange)')
|
|
@@ -176,7 +199,7 @@ export async function archiveChangeDirectory(pm, cwd, progress, specBase) {
|
|
|
176
199
|
if (alreadyArchivedDir) {
|
|
177
200
|
console.log(`ℹ️ 源目录不存在但变更已在 archive/(${basename(alreadyArchivedDir)}),判定已归档,自愈进度 DB`)
|
|
178
201
|
pm.unregisterChange(cwd, archiveChangeName)
|
|
179
|
-
await archiveWorktreeCleanup(cwd, archiveChangeName)
|
|
202
|
+
await archiveWorktreeCleanup(cwd, archiveChangeName, specBase, platformOpts)
|
|
180
203
|
console.log(`📦 已自愈归档:${archiveChangeName} → archive/${basename(alreadyArchivedDir)}/`)
|
|
181
204
|
return alreadyArchivedDir
|
|
182
205
|
}
|
|
@@ -221,7 +244,7 @@ export async function archiveChangeDirectory(pm, cwd, progress, specBase) {
|
|
|
221
244
|
} catch {}
|
|
222
245
|
|
|
223
246
|
// 归档时清理可能残留的 worktree(自愈路径也复用,见上方 srcDir 缺失分支)。
|
|
224
|
-
await archiveWorktreeCleanup(cwd, archiveChangeName)
|
|
247
|
+
await archiveWorktreeCleanup(cwd, archiveChangeName, specBase, platformOpts)
|
|
225
248
|
|
|
226
249
|
console.log(`📦 已归档:${archiveChangeName} → archive/${destName}/`)
|
|
227
250
|
return destDir
|
|
@@ -236,7 +259,7 @@ export async function archiveChangeDirectory(pm, cwd, progress, specBase) {
|
|
|
236
259
|
*
|
|
237
260
|
* @returns {{stageCompleted:false,currentIdx,nextPendingIdx:number}|null}
|
|
238
261
|
*/
|
|
239
|
-
export async function handleArchiveConfirmStep({ stageName, steps, currentIdx, confirm, outputText, pm, cwd, progress, changeName, specBase }) {
|
|
262
|
+
export async function handleArchiveConfirmStep({ stageName, steps, currentIdx, confirm, outputText, pm, cwd, progress, changeName, specBase, platformOpts = {} }) {
|
|
240
263
|
if (stageName !== 'archive' || steps[currentIdx]?.name !== '确认归档') return null
|
|
241
264
|
if (!confirm) {
|
|
242
265
|
steps[currentIdx].status = 'pending'
|
|
@@ -246,7 +269,7 @@ export async function handleArchiveConfirmStep({ stageName, steps, currentIdx, c
|
|
|
246
269
|
console.log('⚠️ 请添加 --confirm 确认归档,例如:sillyspec run archive --done --confirm --output "确认归档"')
|
|
247
270
|
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
248
271
|
}
|
|
249
|
-
const archivedDir = await archiveChangeDirectory(pm, cwd, progress, specBase)
|
|
272
|
+
const archivedDir = await archiveChangeDirectory(pm, cwd, progress, specBase, platformOpts)
|
|
250
273
|
if (archivedDir && existsSync(archivedDir)) {
|
|
251
274
|
const recommendedDocs = ['design.md', 'module-impact.md']
|
|
252
275
|
const missingRecommended = recommendedDocs.filter(d => !existsSync(join(archivedDir, d)))
|
|
@@ -751,6 +774,12 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
751
774
|
changedFiles: realFiles,
|
|
752
775
|
})
|
|
753
776
|
console.log(`📝 QUICKLOG 条目 ${qlId} 已标记完成`)
|
|
777
|
+
// 刷新 DB title:从 step3「需求:」提取(agent 可改 title 的途径),覆盖启动时的兜底快照。
|
|
778
|
+
// 与 QUICKLOG 标题刷新(flipEntryInContent 内 extractTitleFromResult)同源,保持 DB↔QUICKLOG 一致。
|
|
779
|
+
try {
|
|
780
|
+
const refinedTitle = extractTitleFromResult(outputText || '');
|
|
781
|
+
if (refinedTitle) pm.updateChangeMeta(cwd, changeName, { title: refinedTitle });
|
|
782
|
+
} catch { /* title 刷新失败不阻断完成 */ }
|
|
754
783
|
} catch (e) {
|
|
755
784
|
console.warn(`⚠️ QUICKLOG 完成态写入失败: ${e.message}`)
|
|
756
785
|
}
|