sillyspec 3.25.11 → 3.26.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/.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/package.json +1 -1
- package/src/db.js +10 -2
- package/src/dispatch/probe.js +11 -9
- package/src/index.js +151 -3
- package/src/local-detect.js +120 -110
- package/src/progress/change-registry.js +15 -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 +296 -6
- package/src/quicklog.js +46 -7
- package/src/run/command.js +13 -2
- package/src/run/complete-handlers.js +30 -7
- package/src/run/complete.js +17 -4
- package/src/run/gates.js +13 -2
- package/src/run/shared.js +74 -0
- 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
|
@@ -114,7 +114,7 @@ execute 还有**第二道**独立的 stage 级审查:除逐 task review.json
|
|
|
114
114
|
|
|
115
115
|
## 派发模式(SillyHub MCP,可选)
|
|
116
116
|
|
|
117
|
-
`execute` Wave 内的子代理默认用本机 Agent tool 执行。若消费方配置了 SillyHub MCP(`SILLYHUB_MCP_URL` / `SILLYHUB_MCP_TOKEN
|
|
117
|
+
`execute` Wave 内的子代理默认用本机 Agent tool 执行。若消费方配置了 SillyHub MCP(`local.yaml` 的 `mcp` 段写 `mcp.url` / `mcp.token`——可由 `sillyspec platform connect` 同源写入或手填;或环境变量 `SILLYHUB_MCP_URL` / `SILLYHUB_MCP_TOKEN` 作回退),Wave 步骤 prompt 运行时可能注入一段 SillyHub 派发指令——**出现就照其中的指令执行**(创建 mission / dispatch_worker / 轮询结果 / 本机兜底),**没出现就用本机 Agent tool**。未配置 MCP 时完全不注入,行为与无此机制一致。
|
|
118
118
|
|
|
119
119
|
可选:`sillyspec dispatch probe` 查看 SillyHub 是否可用。
|
|
120
120
|
|
|
@@ -49,6 +49,7 @@ sillyspec run quick --reopen --from-step N # 重新打开已完成阶段修
|
|
|
49
49
|
|---|---|
|
|
50
50
|
| `--linked-changes none\|a,b` | **显式关联变更(取代 `--change`,推荐)**。none=不关联,a,b=关联列表 |
|
|
51
51
|
| `--files a.js,b.js` | 显式声明本次允许修改的文件(边界保护) |
|
|
52
|
+
| `--file-notes "p::注 \|\| p::注"` | quick `--done` 用:QUICKLOG「文件:」行落盘为多行带括注 bullet(省事后手改文件行)。格式 `path::括注`,`\|\|` 分隔多条 |
|
|
52
53
|
| `--allow-new` | 允许新增文件(默认禁止,防意外创建) |
|
|
53
54
|
| `--force-baseline` | 允许覆盖 baseline 受保护文件 / 压制 `.sillyspec/` 危险判定(危险,慎用) |
|
|
54
55
|
| `--confirm` | ⚠️ 仅打印变更概览,**不解锁 blocked**(blocked 仍 exit 1)。真正解锁用 `--force-baseline`/`--allow-new` |
|
|
@@ -88,7 +89,8 @@ sillyspec run quick --files src/phone.ts,src/phone.test.ts
|
|
|
88
89
|
- quick 直接在主工作区改代码(不创建 worktree),范围必须小且明确
|
|
89
90
|
- 完成后立即 `--done`,不跳过
|
|
90
91
|
- QUICKLOG 记录的**骨架由 CLI 接管**:启动时 CLI 自动分配 ql-ID 并在 `.sillyspec/quicklog/QUICKLOG-<user>.md` 写「进行中」条目(含关联变更 tasks.md),`--done` 时 CLI 自动翻「已完成」+ 勾选 task + 回填文件路径。ql-ID 分配/状态/task 你无需手写,只需用注入的 `<quicklog-id>` 在模块文档变更索引引用
|
|
91
|
-
- **QUICKLOG
|
|
92
|
+
- **QUICKLOG 落盘已结构化(`--done` 后按需核对,多数无需手改)**:CLI 已落盘结构化条目——标题从 `--output` 的「需求:」自动提取、正文四字段自动分行、文件行用 `--file-notes` 时为多行带括注。`--done` 后只需核对:标题弱才改(禁留 `(quick 任务)` 占位);没用 `--file-notes` 时文件行是单行、可事后补括注(参照同文件早期丰富条目);正文 `需求:`/`根因:`/`方案:`/`结果:` 四段按需充实(禁只留一段「结果:」)。一条 quick = 一条独立 ql,不追加到旧条目
|
|
93
|
+
- **收尾顺序(模块文档在 `--done` 前,QUICKLOG 在 `--done` 后,别记混)**:① 命中模块→改模块文档→`git add`;② `sillyspec run quick --done --change <id> --output "四字段" [--file-notes "..."]`(CLI 自动翻完成 + 勾 task + 落盘 QUICKLOG 标题/文件/正文);③ 核对 QUICKLOG 标题(CLI 已从「需求:」提取,仅弱标题才改)→若改了再 `git add`
|
|
92
94
|
- **最后一步 `--done --output` 必须按结构化结果模板给全四字段**(逐项一句话):`需求:… 根因:… 方案:… 结果:…`。这是 QUICKLOG「结果:」归档的唯一来源;CLI 校验缺字段会拒绝 `--done`(exit 1),补全后重跑即可。前两个 step 的 `--output` 是中间摘要,不用此模板
|
|
93
95
|
- **禁止**在没有运行 CLI 的情况下自行决定流程
|
|
94
96
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2026-08-10T17:08:53.237Z
|
package/package.json
CHANGED
package/src/db.js
CHANGED
|
@@ -7,7 +7,7 @@ import { dirname } from 'path';
|
|
|
7
7
|
// 每次 new ProgressManager 都过 init,靠版本戳跳过建表省开销)。
|
|
8
8
|
// better-sqlite3 是原生 SQLite 引擎,打开即持久化(不像 sql.js 纯内存需整库 export 落盘),
|
|
9
9
|
// _createSchema 内 DDL 直接落盘,无需额外 _save。
|
|
10
|
-
const DB_SCHEMA_VERSION =
|
|
10
|
+
const DB_SCHEMA_VERSION = 4;
|
|
11
11
|
|
|
12
12
|
// SQLITE_BUSY 应用层有限重试(R-08 / NFR-03):WAL 单写者模型,并发写第二者在
|
|
13
13
|
// busy_timeout=5000(init PRAGMA)后抛 SQLITE_BUSY。busy_timeout 已在引擎层处理大部分等待;
|
|
@@ -200,7 +200,7 @@ export class DB {
|
|
|
200
200
|
CREATE TABLE IF NOT EXISTS project (
|
|
201
201
|
id INTEGER PRIMARY KEY DEFAULT 1,
|
|
202
202
|
name TEXT NOT NULL,
|
|
203
|
-
schema_version INTEGER DEFAULT
|
|
203
|
+
schema_version INTEGER DEFAULT 4,
|
|
204
204
|
created_at TEXT NOT NULL,
|
|
205
205
|
updated_at TEXT NOT NULL
|
|
206
206
|
)
|
|
@@ -302,6 +302,14 @@ export class DB {
|
|
|
302
302
|
this._migrateAddColumn('stages', 'reopened_from_step', 'TEXT');
|
|
303
303
|
this._migrateAddColumn('stages', 'reopened_at', 'TEXT');
|
|
304
304
|
this._migrateAddColumn('stages', 'stale_reason', 'TEXT');
|
|
305
|
+
|
|
306
|
+
// Platform sync support(2026-08-10-platform-progress-sync §8 / D-012):
|
|
307
|
+
// changes 表加 base_ts(last_synced_platform_ts)与本地脏度(last_local_modified_ts)两列,
|
|
308
|
+
// 为 serializeForSync()/import() 序列化与 base_ts 乐观锁冲突检测提供数据载体。
|
|
309
|
+
// NULL 语义(gap8):last_local_modified_ts NULL=本地无脏度(pull 不判冲突直接 import);
|
|
310
|
+
// last_synced_platform_ts NULL=首次同步(base_ts NULL 平台接受首次 push)。
|
|
311
|
+
this._migrateAddColumn('changes', 'last_synced_platform_ts', 'TEXT');
|
|
312
|
+
this._migrateAddColumn('changes', 'last_local_modified_ts', 'TEXT');
|
|
305
313
|
}
|
|
306
314
|
|
|
307
315
|
/**
|
package/src/dispatch/probe.js
CHANGED
|
@@ -16,6 +16,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
16
16
|
import { join, relative, isAbsolute, sep } from 'node:path';
|
|
17
17
|
import jsYaml from 'js-yaml';
|
|
18
18
|
import { SillyHubMcpClient } from '../sillyhub-mcp/client.js';
|
|
19
|
+
import { readMcpConfig } from '../sillyhub-mcp/config.js';
|
|
19
20
|
import { setPathAProbeResult } from './backends/sillyhub-mcp.js';
|
|
20
21
|
|
|
21
22
|
/** 默认负面缓存 TTL(毫秒)。可被 local.yaml dispatch.probe_ttl_ms 或 ttlMs 参数覆盖。 */
|
|
@@ -57,11 +58,11 @@ function resolveTtl(ttlMs, cwd) {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
|
-
* 配置指纹(负面缓存 key)。用 env URL 标识 daemon 身份;
|
|
61
|
-
* token 轮换后最迟 TTL 内自动失效重探。
|
|
61
|
+
* 配置指纹(负面缓存 key)。用 local.yaml mcp 段或 env 的 URL 标识 daemon 身份;
|
|
62
|
+
* token 不入 key(避免敏感信息驻留缓存,保密语义不变)。token 轮换后最迟 TTL 内自动失效重探。
|
|
62
63
|
*/
|
|
63
|
-
function configFingerprint() {
|
|
64
|
-
return
|
|
64
|
+
function configFingerprint(cwd) {
|
|
65
|
+
return readMcpConfig(cwd)?.url || '';
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
/**
|
|
@@ -133,7 +134,7 @@ async function preheatPathAProbe(cli) {
|
|
|
133
134
|
* 5. 全通过 → available=true(不缓存正面结果)
|
|
134
135
|
*
|
|
135
136
|
* @param {object} [opts]
|
|
136
|
-
* @param {object} [opts.client] - SillyHubMcpClient 实例;缺省 new SillyHubMcpClient()
|
|
137
|
+
* @param {object} [opts.client] - SillyHubMcpClient 实例;缺省 new SillyHubMcpClient()(经 readMcpConfig 读 local.yaml mcp 段 + env)
|
|
137
138
|
* @param {string} [opts.worktreePath] - SillySpec worktree 绝对路径(root_path 校验用)
|
|
138
139
|
* @param {string} [opts.rootPath] - daemon ws.root_path;未传(undefined)时 best-effort 从 daemon
|
|
139
140
|
* 拿(task-12 #5),拿不到则跳过越界校验(不判 unavailable)
|
|
@@ -141,13 +142,14 @@ async function preheatPathAProbe(cli) {
|
|
|
141
142
|
* @returns {Promise<{ available: boolean, reason?: string }>}
|
|
142
143
|
*/
|
|
143
144
|
export async function probeSillyHub({ client, worktreePath, rootPath, ttlMs } = {}) {
|
|
144
|
-
// 1.
|
|
145
|
-
|
|
145
|
+
// 1. 同步快速路径:无 local.yaml mcp 段且 env 缺凭据即 unavailable(不发网络,零回归关键)。
|
|
146
|
+
// readMcpConfig 纯 fs + env 读(best-effort 不抛不发网络),返回 null = 两源都缺 → no-config。
|
|
147
|
+
if (!readMcpConfig(process.cwd())) {
|
|
146
148
|
return { available: false, reason: 'no-config' };
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
// 2. 负面缓存命中(未过 TTL)→ 直接返回,不发网络(R-06 抖动期免反复探测)
|
|
150
|
-
const fp = configFingerprint();
|
|
152
|
+
const fp = configFingerprint(process.cwd());
|
|
151
153
|
const cached = negativeCache.get(fp);
|
|
152
154
|
if (cached && cached.expiresAt > Date.now()) {
|
|
153
155
|
return cached.result;
|
|
@@ -155,7 +157,7 @@ export async function probeSillyHub({ client, worktreePath, rootPath, ttlMs } =
|
|
|
155
157
|
|
|
156
158
|
// 3. 连通性探测:client.probeDaemon 自身 best-effort 不抛;再兜一层 try/catch,
|
|
157
159
|
// 即便 client 契约异常抛出也保守判 unavailable(铁律:绝不抛穿 execute)
|
|
158
|
-
const cli = client || new SillyHubMcpClient();
|
|
160
|
+
const cli = client || new SillyHubMcpClient({ cwd: process.cwd() });
|
|
159
161
|
let reachable;
|
|
160
162
|
try {
|
|
161
163
|
reachable = await cli.probeDaemon();
|
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
|
+
}
|