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/index.js
CHANGED
|
@@ -84,6 +84,8 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
84
84
|
sillyspec gate <stage> --change <name> [--json] 机器门控:阶段能否标记完成(只读)
|
|
85
85
|
sillyspec derive <facet> --change <name> [--json] 单项事实核验(facet: execute-evidence|verify-test|task-reviews|artifacts)
|
|
86
86
|
sillyspec backfill-reviews --change <name> [--json] 为手动补的 task 生成 review.json 草稿(cannot_verify,解 archive 客观完成度阻断)
|
|
87
|
+
sillyspec register-stage-review --change <name> --stage <brainstorm|plan|execute> [--from <review.json>] [--json]
|
|
88
|
+
生成/adopt stage 级 review.json(docHash 自动算 + 写 marker,治 tier=independent marker 死锁)
|
|
87
89
|
|
|
88
90
|
sillyspec doctor [子命令] 进度库健康检查 + 修复(顶层命令,非 worktree doctor)
|
|
89
91
|
(无子命令) 跑诊断,列出问题
|
|
@@ -458,6 +460,41 @@ async function main() {
|
|
|
458
460
|
}
|
|
459
461
|
break;
|
|
460
462
|
}
|
|
463
|
+
case 'register-stage-review': {
|
|
464
|
+
// 手动注册 stage 级 review(brainstorm/plan/execute-acceptance)的确定性 writer ——
|
|
465
|
+
// 生成 run 目录 + review.json 骨架(cannot_verify,待独立审查子代理填 verdict)或 adopt
|
|
466
|
+
// agent 草稿(--from,保留 verdict/checklist 重算 docHash)+ 写 marker。治 tier=independent 时
|
|
467
|
+
// 调度者手动派独立子代理不写 marker 的死锁。docHash 由 CLI computeDocHash 算(部分实现 P6.1b defer)。
|
|
468
|
+
// 纯新增,不改 gate 语义。与 task 级 backfill-reviews 对称(stage 级等价物)。
|
|
469
|
+
const rsrChangeIdx = args.indexOf('--change');
|
|
470
|
+
const rsrChange = rsrChangeIdx >= 0 && args[rsrChangeIdx + 1] ? args[rsrChangeIdx + 1] : null;
|
|
471
|
+
const rsrStageIdx = args.indexOf('--stage');
|
|
472
|
+
const rsrStage = rsrStageIdx >= 0 && args[rsrStageIdx + 1] ? args[rsrStageIdx + 1] : null;
|
|
473
|
+
const rsrFromIdx = args.indexOf('--from');
|
|
474
|
+
const rsrFrom = rsrFromIdx >= 0 && args[rsrFromIdx + 1] ? args[rsrFromIdx + 1] : null;
|
|
475
|
+
if (!rsrChange || !rsrStage) {
|
|
476
|
+
console.error('用法: sillyspec register-stage-review --change <名> --stage <brainstorm|plan|execute> [--from <review.json>] [--spec-dir <path>] [--json]\n 生成/adopt stage 级 review.json(docHash 自动算 + 写 marker),治 tier=independent marker 死锁');
|
|
477
|
+
process.exit(2);
|
|
478
|
+
}
|
|
479
|
+
const { registerStageReview } = await import('./stage-review.js');
|
|
480
|
+
const rsrPlatformOpts = {};
|
|
481
|
+
if (specDir) rsrPlatformOpts.specRoot = specDir;
|
|
482
|
+
try {
|
|
483
|
+
const result = registerStageReview({ changeName: rsrChange, stage: rsrStage, fromFile: rsrFrom, cwd: dir, platformOpts: rsrPlatformOpts });
|
|
484
|
+
if (json) {
|
|
485
|
+
process.stdout.write(JSON.stringify({ ok: true, command: 'register-stage-review', change: rsrChange, stage: rsrStage, ...result }));
|
|
486
|
+
} else {
|
|
487
|
+
console.log(`✅ 已注册 ${rsrStage} stage review [${result.reviewRunId}] → ${result.reviewPath}(mode: ${result.mode})`);
|
|
488
|
+
console.log(` marker → ${result.markerPath}`);
|
|
489
|
+
console.log(` 下一步:独立审查子代理对照 ${result.mainDoc} 填 verdict/checklist 后重跑 --done`);
|
|
490
|
+
}
|
|
491
|
+
} catch (e) {
|
|
492
|
+
if (json) process.stdout.write(JSON.stringify({ ok: false, command: 'register-stage-review', error: e.message }));
|
|
493
|
+
else console.error('❌ ' + e.message);
|
|
494
|
+
process.exitCode = 1;
|
|
495
|
+
}
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
461
498
|
case 'docs': {
|
|
462
499
|
const docsSubCmd = filteredArgs[1];
|
|
463
500
|
if (docsSubCmd === 'migrate') {
|
|
@@ -612,6 +649,10 @@ async function main() {
|
|
|
612
649
|
const { runCommand } = await import('./run.js')
|
|
613
650
|
const stageArgs = [command, ...filteredArgs.slice(1)]
|
|
614
651
|
const effectiveDir = specDir ? dir : resolveEffectiveDir(dir)
|
|
652
|
+
// 下行 pull:stage 命令(run/--done,含 archive)启动时拉一次(task-10 / D-009 / FR-04)
|
|
653
|
+
// 不在每步 pull,仅低频边界点;未连接平台静默跳过
|
|
654
|
+
const { triggerPullActiveChange } = await import('./run/shared.js')
|
|
655
|
+
await triggerPullActiveChange(effectiveDir)
|
|
615
656
|
await runCommand(stageArgs, effectiveDir, specDir, { json })
|
|
616
657
|
break
|
|
617
658
|
}
|
|
@@ -734,6 +775,12 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
734
775
|
for (const err of result.errors) {
|
|
735
776
|
console.error(` ${err}`);
|
|
736
777
|
}
|
|
778
|
+
if (result.rescueCommands) {
|
|
779
|
+
console.error('');
|
|
780
|
+
console.error(`🆘 Rescue commands (${result.rescueCommands.cpFileCount} safe / ${result.rescueCommands.excludedCount} excluded,旁路 git apply,cp 后需手动 sillyspec worktree cleanup ${wtName}):`);
|
|
781
|
+
for (const c of result.rescueCommands.commands) console.error(` ${c}`);
|
|
782
|
+
for (const w of result.rescueCommands.warnings) console.error(` ⚠️ ${w}`);
|
|
783
|
+
}
|
|
737
784
|
process.exit(1);
|
|
738
785
|
}
|
|
739
786
|
|
|
@@ -795,6 +842,12 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
795
842
|
for (const w of assessment.warnings) console.log(` ⚠️ ${w}`);
|
|
796
843
|
}
|
|
797
844
|
|
|
845
|
+
if (assessment.rescueCommands) {
|
|
846
|
+
console.log('');
|
|
847
|
+
console.log(`🆘 Rescue commands (${assessment.rescueCommands.cpFileCount} safe / ${assessment.rescueCommands.excludedCount} excluded,旁路 git apply,cp 后需手动 sillyspec worktree cleanup ${wtName}):`);
|
|
848
|
+
for (const c of assessment.rescueCommands.commands) console.log(` ${c}`);
|
|
849
|
+
for (const w of assessment.rescueCommands.warnings) console.log(` ⚠️ ${w}`);
|
|
850
|
+
}
|
|
798
851
|
console.log('');
|
|
799
852
|
if (assessment.decision === 'SAFE' || assessment.decision === 'WARNING') {
|
|
800
853
|
console.log('Action: auto-applying...');
|
|
@@ -1045,6 +1098,8 @@ SillySpec platform — SillyHub 平台同步
|
|
|
1045
1098
|
sillyspec platform disconnect
|
|
1046
1099
|
sillyspec platform sync [--change <name>]
|
|
1047
1100
|
sillyspec platform sync-docs [--change <name>]
|
|
1101
|
+
sillyspec platform pull [--change <name>]
|
|
1102
|
+
sillyspec platform resolve <change-name> <--keep-local|--take-platform|--abort>
|
|
1048
1103
|
sillyspec platform status
|
|
1049
1104
|
sillyspec platform pointer [--cleanup]
|
|
1050
1105
|
sillyspec platform approve <change-name>
|
|
@@ -1145,15 +1200,108 @@ SillySpec platform — SillyHub 平台同步
|
|
|
1145
1200
|
await syncModule.syncDocuments(syncDocsChangeName, dir);
|
|
1146
1201
|
break;
|
|
1147
1202
|
}
|
|
1148
|
-
case 'status':
|
|
1149
|
-
|
|
1203
|
+
case 'status': {
|
|
1204
|
+
// task-14 / FR-05:扩展 status(落后标记 + 未决冲突列表);未连接输出与现状一致
|
|
1205
|
+
const st = await syncModule.collectStatus(dir);
|
|
1206
|
+
if (!st.connected) {
|
|
1207
|
+
console.log('平台: 未连接');
|
|
1208
|
+
break;
|
|
1209
|
+
}
|
|
1210
|
+
console.log(`平台: ${st.url}`);
|
|
1211
|
+
console.log(`上次连接: ${st.lastSync || '未知'}`);
|
|
1212
|
+
if (st.listFailed) {
|
|
1213
|
+
console.log('ℹ️ 无法连接平台获取落后状态');
|
|
1214
|
+
} else if (st.behind.length > 0) {
|
|
1215
|
+
console.log(`⚠️ 本地可能落后(${st.behind.length} 个变更平台有更新):`);
|
|
1216
|
+
for (const b of st.behind) {
|
|
1217
|
+
console.log(` - ${b.name}: 平台 ${b.platform_pushed_at} > 本地 ${b.local_synced}`);
|
|
1218
|
+
}
|
|
1219
|
+
} else {
|
|
1220
|
+
console.log('✅ 本地与平台进度同步');
|
|
1221
|
+
}
|
|
1222
|
+
if (st.conflicts.length > 0) {
|
|
1223
|
+
console.log(`⚠️ 未决冲突(${st.conflicts.length} 个,请 platform resolve 处理):`);
|
|
1224
|
+
for (const c of st.conflicts) {
|
|
1225
|
+
console.log(` - ${c.change}${c.created_at ? ` (创建于 ${c.created_at})` : ''}`);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1150
1228
|
break;
|
|
1229
|
+
}
|
|
1230
|
+
case 'pull': {
|
|
1231
|
+
// task-11 / D-006 / D-009 / FR-03:手动下行拉取平台进度到本地。
|
|
1232
|
+
// 与自动 triggerPull 共用 syncModule.pull(SyncManager.pull 实例方法),行为一致。
|
|
1233
|
+
const pullChangeIdx = args.indexOf('--change');
|
|
1234
|
+
const pullChangeName = pullChangeIdx >= 0 && args[pullChangeIdx + 1] ? args[pullChangeIdx + 1] : null;
|
|
1235
|
+
// 未连接明确提示不崩(acceptance:未连接平台输出明确提示)
|
|
1236
|
+
const smForCheck = new syncModule.SyncManager(dir);
|
|
1237
|
+
if (!smForCheck._getPlatform()) {
|
|
1238
|
+
console.error('❌ 未连接平台,请先 sillyspec platform connect');
|
|
1239
|
+
process.exit(1);
|
|
1240
|
+
}
|
|
1241
|
+
if (pullChangeName) {
|
|
1242
|
+
// 单变更完整 pull(两级 pull 第二级)
|
|
1243
|
+
const r = await syncModule.pull(pullChangeName, {}, dir);
|
|
1244
|
+
if (r.conflict) {
|
|
1245
|
+
console.log(`⚠️ ${pullChangeName}: 冲突(已写 sync-conflict 文件),请 sillyspec platform resolve 处理`);
|
|
1246
|
+
} else if (r.imported) {
|
|
1247
|
+
console.log(`✅ ${pullChangeName}: 已拉取并 import`);
|
|
1248
|
+
} else {
|
|
1249
|
+
console.log(`⏭️ ${pullChangeName}: 未 import(${r.reason || '无变更'})`);
|
|
1250
|
+
}
|
|
1251
|
+
} else {
|
|
1252
|
+
// 无 --change:先拉轻量列表(两级 pull 第一级),再对每个 change 按需完整 pull
|
|
1253
|
+
const list = await syncModule.pullList(dir);
|
|
1254
|
+
if (!list.ok) {
|
|
1255
|
+
console.error(`❌ 拉取变更列表失败: ${list.reason || '未知'}`);
|
|
1256
|
+
process.exit(1);
|
|
1257
|
+
}
|
|
1258
|
+
console.log(`平台变更 ${list.changes.length} 个:`);
|
|
1259
|
+
let importedCount = 0, conflictCount = 0;
|
|
1260
|
+
for (const ch of list.changes) {
|
|
1261
|
+
const name = typeof ch === 'string' ? ch : ch.name;
|
|
1262
|
+
if (!name) continue;
|
|
1263
|
+
const r = await syncModule.pull(name, {}, dir);
|
|
1264
|
+
if (r.imported) { importedCount++; console.log(` ✅ ${name}: 已 import`); }
|
|
1265
|
+
else if (r.conflict) { conflictCount++; console.log(` ⚠️ ${name}: 冲突,请 platform resolve`); }
|
|
1266
|
+
else { console.log(` ⏭️ ${name}: ${r.reason || '未 import'}`); }
|
|
1267
|
+
}
|
|
1268
|
+
console.log(`拉取完成:imported ${importedCount},冲突 ${conflictCount}`);
|
|
1269
|
+
}
|
|
1270
|
+
break;
|
|
1271
|
+
}
|
|
1272
|
+
case 'resolve': {
|
|
1273
|
+
// task-13 / D-002 / D-010 / D-013 / FR-05:冲突解决三选一(绝不字段级 auto-merge)
|
|
1274
|
+
const resolveName = platformArgs[0];
|
|
1275
|
+
if (!resolveName) {
|
|
1276
|
+
console.error('❌ 用法: sillyspec platform resolve <change-name> <--keep-local|--take-platform|--abort>');
|
|
1277
|
+
process.exit(1);
|
|
1278
|
+
}
|
|
1279
|
+
// 解析 mode flag(三选一互斥,多传/不传均报错)
|
|
1280
|
+
const resolveFlags = ['--keep-local', '--take-platform', '--abort'].filter((f) => args.includes(f));
|
|
1281
|
+
if (resolveFlags.length !== 1) {
|
|
1282
|
+
console.error('❌ 必须恰好指定 --keep-local / --take-platform / --abort 之一');
|
|
1283
|
+
process.exit(1);
|
|
1284
|
+
}
|
|
1285
|
+
const modeMap = { '--keep-local': 'keep-local', '--take-platform': 'take-platform', '--abort': 'abort' };
|
|
1286
|
+
const resolveMode = modeMap[resolveFlags[0]];
|
|
1287
|
+
const r = await syncModule.resolve(resolveName, resolveMode, dir);
|
|
1288
|
+
if (r.ok && r.resolved) {
|
|
1289
|
+
console.log(`✅ ${resolveName} [${resolveMode}]: ${r.reason}`);
|
|
1290
|
+
} else {
|
|
1291
|
+
console.error(`❌ ${resolveName}: ${r.reason}`);
|
|
1292
|
+
process.exit(1);
|
|
1293
|
+
}
|
|
1294
|
+
break;
|
|
1295
|
+
}
|
|
1151
1296
|
case 'approve': {
|
|
1152
1297
|
const approveName = platformArgs[0];
|
|
1153
1298
|
if (!approveName) {
|
|
1154
1299
|
console.error('❌ 用法: sillyspec platform approve <change-name>');
|
|
1155
1300
|
process.exit(1);
|
|
1156
1301
|
}
|
|
1302
|
+
// 审批前下行 pull(task-10 / D-009 / FR-06):拉最新进度避免基于过期状态审批;未连接跳过
|
|
1303
|
+
const { triggerPull } = await import('./run/shared.js');
|
|
1304
|
+
await triggerPull(dir, approveName);
|
|
1157
1305
|
await syncModule.approve(approveName, dir);
|
|
1158
1306
|
break;
|
|
1159
1307
|
}
|
|
@@ -1481,7 +1629,7 @@ SillySpec modules — 模块文档管理
|
|
|
1481
1629
|
break;
|
|
1482
1630
|
}
|
|
1483
1631
|
default: {
|
|
1484
|
-
const topCommands = ['init', 'setup', 'run', 'progress', 'worktree', 'dispatch', 'local', 'workflow', 'gate', 'derive', 'backfill-reviews', 'modules', 'change-rename', 'knowledge', 'platform', 'scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore', 'status', 'doctor', 'auto', 'runtime'];
|
|
1632
|
+
const topCommands = ['init', 'setup', 'run', 'progress', 'worktree', 'dispatch', 'local', 'workflow', 'gate', 'derive', 'backfill-reviews', 'register-stage-review', 'modules', 'change-rename', 'knowledge', 'platform', 'scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore', 'status', 'doctor', 'auto', 'runtime'];
|
|
1485
1633
|
const suggestion = didYouMean(command, topCommands);
|
|
1486
1634
|
console.error(`❌ 未知命令: ${command}`);
|
|
1487
1635
|
if (command === '--status') {
|
package/src/local-detect.js
CHANGED
|
@@ -1,110 +1,120 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local Detect — 纯 fs 项目类型嗅探
|
|
3
|
-
*
|
|
4
|
-
* 把"生成本地配置"里的项目类型判定从 scan(半小时 + 大量 token)抽出来,
|
|
5
|
-
* 变成纯 fs 同步读几秒可完成的独立探测。create/gate 只需 project.type +
|
|
6
|
-
* commands,不该被迫跑完整 scan。
|
|
7
|
-
*
|
|
8
|
-
* 纯函数:不 spawn 任何子进程、不调用任何 AI/LLM、不消耗 token。
|
|
9
|
-
* 不写 local.yaml 到磁盘——只返回数据结构,落盘由调用方(CLI 路由 / scan.js)负责。
|
|
10
|
-
* 不引 yaml 依赖。
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { existsSync, readFileSync } from 'fs'
|
|
14
|
-
import { join } from 'path'
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* 从 Makefile 文本提取 test 目标的命令(轻量正则,与 verify-postcheck 同风格)。
|
|
18
|
-
* 支持两种常见写法:
|
|
19
|
-
* - 同行命令:`test: pytest` → 'pytest'
|
|
20
|
-
* - tab 续行:`test:\n\tpytest\n` → 'pytest'(命令在下一 tab 开头行)
|
|
21
|
-
* 边界:
|
|
22
|
-
* - 纯空目标(`test:` 无同行命令、无 tab 续行)→ 回退 'make test'
|
|
23
|
-
* - 命令行内 `#` 注释截断;取第一个非空命令行
|
|
24
|
-
* @param {string} makefileText
|
|
25
|
-
* @returns {string}
|
|
26
|
-
*/
|
|
27
|
-
function parseMakefileTestCommand(makefileText) {
|
|
28
|
-
if (!makefileText) return 'make test'
|
|
29
|
-
// 定位 `test:` 行;捕获 `:` 后同行内容(同行命令或空)
|
|
30
|
-
const head = makefileText.match(/^test:\s*([^\n#]*?)\s*(?:#.*)?$/m)
|
|
31
|
-
if (!head) return 'make test'
|
|
32
|
-
const inline = (head[1] || '').trim()
|
|
33
|
-
if (inline) return inline
|
|
34
|
-
// 同行为空 → 找该行之后第一个 tab 续行命令(`\tcmd` 或 ` cmd`)
|
|
35
|
-
const after = makefileText.slice(head.index + head[0].length)
|
|
36
|
-
const cont = after.match(/^[ \t]+([^\n#]+?)\s*(?:#.*)?$/m)
|
|
37
|
-
if (cont && cont[1]) return cont[1].trim()
|
|
38
|
-
return 'make test'
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* 嗅探 workdir 的项目类型与默认 commands(纯 fs,零 AI / 零 token)。
|
|
43
|
-
*
|
|
44
|
-
* 嗅探规则(按顺序,命中即返回):
|
|
45
|
-
* - package.json → nodejs
|
|
46
|
-
* - pom.xml → maven(mvn compile / mvn test / mvn checkstyle:check)
|
|
47
|
-
* - build.gradle → gradle
|
|
48
|
-
* - Makefile → make(test 命令从 test: 目标解析;build/lint 无则不写)
|
|
49
|
-
* - 都没有 → generic(commands 为空对象)
|
|
50
|
-
*
|
|
51
|
-
* @param {string} workdir - 项目根目录
|
|
52
|
-
* @returns {{
|
|
53
|
-
* project: { type: 'nodejs'|'maven'|'gradle'|'make'|'generic' },
|
|
54
|
-
* commands: { build?: string, test?: string, lint?: string }
|
|
55
|
-
* }}
|
|
56
|
-
*/
|
|
57
|
-
export function detectLocalYaml(workdir) {
|
|
58
|
-
// 1. nodejs
|
|
59
|
-
if (existsSync(join(workdir, 'package.json'))) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Local Detect — 纯 fs 项目类型嗅探
|
|
3
|
+
*
|
|
4
|
+
* 把"生成本地配置"里的项目类型判定从 scan(半小时 + 大量 token)抽出来,
|
|
5
|
+
* 变成纯 fs 同步读几秒可完成的独立探测。create/gate 只需 project.type +
|
|
6
|
+
* commands,不该被迫跑完整 scan。
|
|
7
|
+
*
|
|
8
|
+
* 纯函数:不 spawn 任何子进程、不调用任何 AI/LLM、不消耗 token。
|
|
9
|
+
* 不写 local.yaml 到磁盘——只返回数据结构,落盘由调用方(CLI 路由 / scan.js)负责。
|
|
10
|
+
* 不引 yaml 依赖。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, readFileSync } from 'fs'
|
|
14
|
+
import { join } from 'path'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 从 Makefile 文本提取 test 目标的命令(轻量正则,与 verify-postcheck 同风格)。
|
|
18
|
+
* 支持两种常见写法:
|
|
19
|
+
* - 同行命令:`test: pytest` → 'pytest'
|
|
20
|
+
* - tab 续行:`test:\n\tpytest\n` → 'pytest'(命令在下一 tab 开头行)
|
|
21
|
+
* 边界:
|
|
22
|
+
* - 纯空目标(`test:` 无同行命令、无 tab 续行)→ 回退 'make test'
|
|
23
|
+
* - 命令行内 `#` 注释截断;取第一个非空命令行
|
|
24
|
+
* @param {string} makefileText
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
function parseMakefileTestCommand(makefileText) {
|
|
28
|
+
if (!makefileText) return 'make test'
|
|
29
|
+
// 定位 `test:` 行;捕获 `:` 后同行内容(同行命令或空)
|
|
30
|
+
const head = makefileText.match(/^test:\s*([^\n#]*?)\s*(?:#.*)?$/m)
|
|
31
|
+
if (!head) return 'make test'
|
|
32
|
+
const inline = (head[1] || '').trim()
|
|
33
|
+
if (inline) return inline
|
|
34
|
+
// 同行为空 → 找该行之后第一个 tab 续行命令(`\tcmd` 或 ` cmd`)
|
|
35
|
+
const after = makefileText.slice(head.index + head[0].length)
|
|
36
|
+
const cont = after.match(/^[ \t]+([^\n#]+?)\s*(?:#.*)?$/m)
|
|
37
|
+
if (cont && cont[1]) return cont[1].trim()
|
|
38
|
+
return 'make test'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 嗅探 workdir 的项目类型与默认 commands(纯 fs,零 AI / 零 token)。
|
|
43
|
+
*
|
|
44
|
+
* 嗅探规则(按顺序,命中即返回):
|
|
45
|
+
* - package.json → nodejs(核验 scripts.{build,test,lint} 存在才写对应键;缺失不写)
|
|
46
|
+
* - pom.xml → maven(mvn compile / mvn test / mvn checkstyle:check)
|
|
47
|
+
* - build.gradle → gradle(gradlew 存在用 ./gradlew 否则 gradle;build / test / check)
|
|
48
|
+
* - Makefile → make(test 命令从 test: 目标解析;build/lint 无则不写)
|
|
49
|
+
* - 都没有 → generic(commands 为空对象)
|
|
50
|
+
*
|
|
51
|
+
* @param {string} workdir - 项目根目录
|
|
52
|
+
* @returns {{
|
|
53
|
+
* project: { type: 'nodejs'|'maven'|'gradle'|'make'|'generic' },
|
|
54
|
+
* commands: { build?: string, test?: string, lint?: string }
|
|
55
|
+
* }}
|
|
56
|
+
*/
|
|
57
|
+
export function detectLocalYaml(workdir) {
|
|
58
|
+
// 1. nodejs
|
|
59
|
+
if (existsSync(join(workdir, 'package.json'))) {
|
|
60
|
+
const pkgPath = join(workdir, 'package.json')
|
|
61
|
+
const raw = readFileSync(pkgPath, 'utf8')
|
|
62
|
+
let pkg
|
|
63
|
+
try {
|
|
64
|
+
pkg = JSON.parse(raw)
|
|
65
|
+
} catch (e) {
|
|
66
|
+
throw new Error('package.json 解析失败:' + pkgPath)
|
|
67
|
+
}
|
|
68
|
+
const scripts = (pkg && pkg.scripts) || {}
|
|
69
|
+
const commands = {}
|
|
70
|
+
if (scripts.build) commands.build = 'npm run build'
|
|
71
|
+
if (scripts.test) commands.test = 'npm test'
|
|
72
|
+
if (scripts.lint) commands.lint = 'npm run lint'
|
|
73
|
+
return {
|
|
74
|
+
project: { type: 'nodejs' },
|
|
75
|
+
commands,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 2. maven
|
|
80
|
+
if (existsSync(join(workdir, 'pom.xml'))) {
|
|
81
|
+
return {
|
|
82
|
+
project: { type: 'maven' },
|
|
83
|
+
commands: {
|
|
84
|
+
build: 'mvn compile',
|
|
85
|
+
test: 'mvn test',
|
|
86
|
+
lint: 'mvn checkstyle:check',
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 3. gradle
|
|
92
|
+
if (existsSync(join(workdir, 'build.gradle'))) {
|
|
93
|
+
const gradlePrefix = existsSync(join(workdir, 'gradlew')) ? './gradlew' : 'gradle'
|
|
94
|
+
return {
|
|
95
|
+
project: { type: 'gradle' },
|
|
96
|
+
commands: {
|
|
97
|
+
build: `${gradlePrefix} build`,
|
|
98
|
+
test: `${gradlePrefix} test`,
|
|
99
|
+
lint: `${gradlePrefix} check`,
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 4. make(test 命令从 Makefile 解析;build/lint 无则省略)
|
|
105
|
+
const makefilePath = join(workdir, 'Makefile')
|
|
106
|
+
if (existsSync(makefilePath)) {
|
|
107
|
+
const makefileText = readFileSync(makefilePath, 'utf8')
|
|
108
|
+
const test = parseMakefileTestCommand(makefileText)
|
|
109
|
+
return {
|
|
110
|
+
project: { type: 'make' },
|
|
111
|
+
commands: { test },
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 5. generic
|
|
116
|
+
return {
|
|
117
|
+
project: { type: 'generic' },
|
|
118
|
+
commands: {},
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -33,10 +33,12 @@ export class ChangeRegistry {
|
|
|
33
33
|
const sqlDb = db.getDb();
|
|
34
34
|
const now = new Date().toISOString();
|
|
35
35
|
// 尝试插入新行
|
|
36
|
-
sqlDb.prepare(
|
|
36
|
+
const ins = sqlDb.prepare(
|
|
37
37
|
`INSERT OR IGNORE INTO changes (name, created_at, last_active)
|
|
38
38
|
VALUES (?, ?, ?)`
|
|
39
39
|
).run(changeName, now, now);
|
|
40
|
+
// 只在真正创建 change 时标脏(D-013/task-04);已存在行不标(读路径/重复调用不误判脏)
|
|
41
|
+
if (ins.changes > 0) this.pm._touchLocalModified(cwd, changeName, now);
|
|
40
42
|
// 注意:不复活已归档的变更——归档是不可逆操作
|
|
41
43
|
// 如果变更已存在且为 archived,保持 archived 状态不变
|
|
42
44
|
});
|
|
@@ -53,15 +55,46 @@ export class ChangeRegistry {
|
|
|
53
55
|
try {
|
|
54
56
|
db.transaction(() => {
|
|
55
57
|
const sqlDb = db.getDb();
|
|
58
|
+
const now = new Date().toISOString();
|
|
56
59
|
sqlDb.prepare(
|
|
57
60
|
`UPDATE changes SET isolation_status = ?, isolation_mode = ?, isolation_reason = ?, last_active = ? WHERE name = ?`
|
|
58
|
-
).run(isolation.status, isolation.mode || null, isolation.reason || null,
|
|
61
|
+
).run(isolation.status, isolation.mode || null, isolation.reason || null, now, changeName);
|
|
62
|
+
// 本地脏度(D-013 / task-04):隔离状态变更也是本地推进
|
|
63
|
+
this.pm._touchLocalModified(cwd, changeName, now);
|
|
59
64
|
});
|
|
60
65
|
} catch (err) {
|
|
61
66
|
console.warn('⚠️ 更新 isolation 状态失败:', err.message);
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
/**
|
|
71
|
+
* 更新变更的人类可读元信息(title / quicklog_id),让 quick-<hex> 行可读、DB↔QUICKLOG 可对账。
|
|
72
|
+
* quick 启动时回填(title 从任务描述、quicklog_id 用分配的 qlId);--done 时从 step3「需求:」刷新 title。
|
|
73
|
+
* 部分更新(只传 title 不动 quicklog_id,反之亦然)。不调 _touchLocalModified:title/quicklog_id 是
|
|
74
|
+
* 本地展示用元信息,纳入脏度会扰动平台同步(平台 changes 表无此列)。
|
|
75
|
+
* @param {string} cwd
|
|
76
|
+
* @param {string} changeName
|
|
77
|
+
* @param {{ title?: string, quicklogId?: string }} meta
|
|
78
|
+
*/
|
|
79
|
+
updateChangeMeta(cwd, changeName, meta) {
|
|
80
|
+
if (!changeName || !meta) return;
|
|
81
|
+
const db = this.pm._ensureDB(cwd);
|
|
82
|
+
try {
|
|
83
|
+
db.transaction(() => {
|
|
84
|
+
const sqlDb = db.getDb();
|
|
85
|
+
const sets = [];
|
|
86
|
+
const params = [];
|
|
87
|
+
if (meta.title !== undefined) { sets.push('title = ?'); params.push(meta.title); }
|
|
88
|
+
if (meta.quicklogId !== undefined) { sets.push('quicklog_id = ?'); params.push(meta.quicklogId); }
|
|
89
|
+
if (sets.length === 0) return;
|
|
90
|
+
params.push(changeName);
|
|
91
|
+
sqlDb.prepare(`UPDATE changes SET ${sets.join(', ')} WHERE name = ?`).run(...params);
|
|
92
|
+
});
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.warn('⚠️ 更新 change 元信息失败:', err.message);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
65
98
|
/**
|
|
66
99
|
* 读取变更的隔离状态
|
|
67
100
|
* @param {string} cwd - 项目根目录
|
|
@@ -117,6 +150,8 @@ export class ChangeRegistry {
|
|
|
117
150
|
status === 'approved' ? now : null,
|
|
118
151
|
status === 'rejected' ? reason : null,
|
|
119
152
|
);
|
|
153
|
+
// 本地脏度(D-013 / task-04):审批状态变更也是本地推进
|
|
154
|
+
this.pm._touchLocalModified(cwd, changeName, now);
|
|
120
155
|
});
|
|
121
156
|
}
|
|
122
157
|
|
|
@@ -167,6 +202,8 @@ export class ChangeRegistry {
|
|
|
167
202
|
db.transaction(() => {
|
|
168
203
|
const sqlDb = db.getDb();
|
|
169
204
|
sqlDb.prepare(`UPDATE changes SET name = ?, last_active = ? WHERE name = ?`).run(newName, now, oldName);
|
|
205
|
+
// 本地脏度(D-013 / task-04):重命名也是本地推进(标新名)
|
|
206
|
+
this.pm._touchLocalModified(cwd, newName, now);
|
|
170
207
|
});
|
|
171
208
|
} catch (e) {
|
|
172
209
|
console.error(`❌ 重命名失败:更新数据库时出错(${e.message})`);
|
|
@@ -183,6 +220,8 @@ export class ChangeRegistry {
|
|
|
183
220
|
db.transaction(() => {
|
|
184
221
|
const sqlDb = db.getDb();
|
|
185
222
|
sqlDb.prepare(`UPDATE changes SET name = ?, last_active = ? WHERE name = ?`).run(oldName, now, newName);
|
|
223
|
+
// 回滚也是写(恢复 oldName),对称标脏旧名
|
|
224
|
+
this.pm._touchLocalModified(cwd, oldName, now);
|
|
186
225
|
});
|
|
187
226
|
console.error(`❌ 重命名失败:移动目录出错(${e.message}),已回滚数据库`);
|
|
188
227
|
} catch (rollbackErr) {
|
|
@@ -213,6 +252,8 @@ export class ChangeRegistry {
|
|
|
213
252
|
sqlDb.prepare(
|
|
214
253
|
`UPDATE changes SET status = 'archived', last_active = ? WHERE name = ?`
|
|
215
254
|
).run(now, changeName);
|
|
255
|
+
// 本地脏度(D-013 / task-04):归档也是本地状态推进
|
|
256
|
+
this.pm._touchLocalModified(cwd, changeName, now);
|
|
216
257
|
});
|
|
217
258
|
}
|
|
218
259
|
}
|
package/src/progress/shared.js
CHANGED
|
@@ -26,8 +26,8 @@ export const STAGE_ORDER = ['scan', 'brainstorm', 'plan', 'execute', 'verify', '
|
|
|
26
26
|
// (consistency-doctor 坑 + prompt-control-debt plan-c 同根因)。
|
|
27
27
|
export const MAIN_FLOW_ORDER = ['brainstorm', 'plan', 'execute', 'verify', 'archive'];
|
|
28
28
|
|
|
29
|
-
// progress 数据版本(v3 = SQLite)
|
|
30
|
-
export const CURRENT_VERSION =
|
|
29
|
+
// progress 数据版本(v3 = SQLite;v4 = changes 加 base_ts / 本地脏度两列,platform-sync §8)
|
|
30
|
+
export const CURRENT_VERSION = 5;
|
|
31
31
|
|
|
32
32
|
// 空阶段骨架
|
|
33
33
|
export function emptyStage() {
|
|
@@ -98,6 +98,8 @@ export class StageMachine {
|
|
|
98
98
|
|
|
99
99
|
// UPDATE changes.last_active
|
|
100
100
|
sqlDb.prepare('UPDATE changes SET last_active = ? WHERE name = ?').run(now, cn);
|
|
101
|
+
// 本地脏度(D-013 / task-04):阶段完成是本地推进
|
|
102
|
+
this.pm._touchLocalModified(cwd, cn, now);
|
|
101
103
|
});
|
|
102
104
|
|
|
103
105
|
// 写 history 文件(保持文件系统,不变)
|
|
@@ -492,6 +494,8 @@ export class StageMachine {
|
|
|
492
494
|
for (const s of VALID_STAGES) {
|
|
493
495
|
sqlDb.prepare(`INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, 'pending')`).run(changeId, s);
|
|
494
496
|
}
|
|
497
|
+
// 本地脏度(D-013 / task-04):重置该变更全部进度是本地推进
|
|
498
|
+
this.pm._touchLocalModified(cwd, changeName);
|
|
495
499
|
}
|
|
496
500
|
});
|
|
497
501
|
console.log(`✅ 已重置变更 ${changeName} 的进度`);
|
|
@@ -507,6 +511,8 @@ export class StageMachine {
|
|
|
507
511
|
const changeId = changeRow.id;
|
|
508
512
|
sqlDb.prepare('DELETE FROM steps WHERE stage_id IN (SELECT id FROM stages WHERE change_id = ?)').run(changeId);
|
|
509
513
|
sqlDb.prepare(`UPDATE stages SET status = 'pending', started_at = NULL, completed_at = NULL WHERE change_id = ?`).run(changeId);
|
|
514
|
+
// 本地脏度(D-013 / task-04):重置进度是本地推进
|
|
515
|
+
this.pm._touchLocalModified(cwd, cn);
|
|
510
516
|
}
|
|
511
517
|
}
|
|
512
518
|
});
|
|
@@ -47,6 +47,9 @@ export class StepStore {
|
|
|
47
47
|
|
|
48
48
|
// UPDATE stages.status 为 in-progress(仅当仍为 pending 时)
|
|
49
49
|
sqlDb.prepare("UPDATE stages SET status = 'in-progress', started_at = ? WHERE change_id = ? AND stage = ? AND status = 'pending'").run(now, changeId, stage);
|
|
50
|
+
|
|
51
|
+
// 本地脏度(D-013 / task-04):切换阶段是本地推进
|
|
52
|
+
this.pm._touchLocalModified(cwd, cn, now);
|
|
50
53
|
});
|
|
51
54
|
|
|
52
55
|
if (!changeFound) {
|
|
@@ -100,6 +103,8 @@ export class StepStore {
|
|
|
100
103
|
VALUES (?, ?, (SELECT COALESCE(MAX(ordering), 0) + 1 FROM steps WHERE stage_id = ?), 'pending')`
|
|
101
104
|
).run(stageId, stepName, stageId);
|
|
102
105
|
tDb.prepare('UPDATE changes SET last_active = ? WHERE name = ?').run(new Date().toISOString(), cn);
|
|
106
|
+
// 本地脏度(D-013 / task-04):添加步骤是本地推进
|
|
107
|
+
this.pm._touchLocalModified(cwd, cn);
|
|
103
108
|
});
|
|
104
109
|
|
|
105
110
|
console.log(`✅ 已添加步骤: ${stage}/${stepName}`);
|
|
@@ -166,6 +171,8 @@ export class StepStore {
|
|
|
166
171
|
}
|
|
167
172
|
|
|
168
173
|
tDb.prepare('UPDATE changes SET last_active = ? WHERE name = ?').run(now, cn);
|
|
174
|
+
// 本地脏度(D-013 / task-04):更新步骤是本地推进
|
|
175
|
+
this.pm._touchLocalModified(cwd, cn, now);
|
|
169
176
|
});
|
|
170
177
|
|
|
171
178
|
if (stageCompletionCandidateId !== null) {
|
|
@@ -227,6 +234,9 @@ export class StepStore {
|
|
|
227
234
|
failed = excluded.failed,
|
|
228
235
|
skipped = excluded.skipped`
|
|
229
236
|
).run(changeId, batchData.total || 0, batchData.completed || 0, batchData.failed || 0, batchData.skipped || 0);
|
|
237
|
+
// 本地脏度(D-013 / task-04):批量进度更新是本地推进
|
|
238
|
+
const nameRow = sqlDb.prepare('SELECT name FROM changes WHERE id = ?').get(changeId);
|
|
239
|
+
if (nameRow) this.pm._touchLocalModified(cwd, nameRow.name);
|
|
230
240
|
});
|
|
231
241
|
}
|
|
232
242
|
|