sillyspec 3.11.9 → 3.11.11
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/package.json +1 -1
- package/src/hooks/worktree-guard.js +130 -11
- package/src/worktree.js +30 -0
package/package.json
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 三重门禁:stageGate × locationGate × fileGate
|
|
5
5
|
* 纯判断模块,不做实际的 hook 注入。
|
|
6
|
+
*
|
|
7
|
+
* P0 优化:
|
|
8
|
+
* - 阶段检测 fallback:gate-status.json → progress.json currentStage
|
|
9
|
+
* - 拦截提示针对每个阶段给出具体修复建议
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
import { existsSync, readFileSync } from 'fs'
|
|
@@ -39,6 +43,41 @@ const DANGER_STASH_ACTIONS = new Set(['drop', 'clear', 'pop'])
|
|
|
39
43
|
/** 危险命令前缀 */
|
|
40
44
|
const DANGER_PREFIXES = ['sudo', 'rm -rf', 'rm -r', 'rmdir']
|
|
41
45
|
|
|
46
|
+
// ── 阶段 → 拦截提示映射 ──
|
|
47
|
+
|
|
48
|
+
const STAGE_HINTS = {
|
|
49
|
+
'(none)': [
|
|
50
|
+
'没有检测到活跃的 SillySpec 流程。',
|
|
51
|
+
'你需要先启动一个任务流程才能修改源码:',
|
|
52
|
+
'',
|
|
53
|
+
' 小改动(≤3 文件):sillyspec run quick "任务描述"',
|
|
54
|
+
' 大改动(>3 文件):sillyspec run brainstorm → plan → execute',
|
|
55
|
+
' 全自动模式: sillyspec run auto "任务描述"',
|
|
56
|
+
],
|
|
57
|
+
'brainstorm': [
|
|
58
|
+
'当前在 brainstorm(需求分析)阶段,这个阶段只写文档,不写代码。',
|
|
59
|
+
'完成 brainstorm 后,流程会自动推进到 plan → execute。',
|
|
60
|
+
'execute 阶段才允许写代码。',
|
|
61
|
+
],
|
|
62
|
+
'plan': [
|
|
63
|
+
'当前在 plan(计划制定)阶段,这个阶段只写计划文档和任务蓝图,不写代码。',
|
|
64
|
+
'完成 plan 后,运行 sillyspec run execute 进入执行阶段。',
|
|
65
|
+
],
|
|
66
|
+
'verify': [
|
|
67
|
+
'当前在 verify(验证)阶段,只做代码审查和测试验证,不修改源码。',
|
|
68
|
+
'如需修改,请先回到 execute 阶段或使用 quick 模式:',
|
|
69
|
+
' sillyspec run quick "修改描述"',
|
|
70
|
+
],
|
|
71
|
+
'archive': [
|
|
72
|
+
'当前在 archive(归档)阶段,不修改源码。',
|
|
73
|
+
'如需修改,请开启新变更:sillyspec run quick "修改描述"',
|
|
74
|
+
],
|
|
75
|
+
'explore': [
|
|
76
|
+
'当前在 explore(探索)阶段,只读不写。',
|
|
77
|
+
'确认方案后使用:sillyspec run brainstorm 或 sillyspec run quick',
|
|
78
|
+
],
|
|
79
|
+
}
|
|
80
|
+
|
|
42
81
|
// ── 辅助函数 ──
|
|
43
82
|
|
|
44
83
|
function resolveWorktreeDir(cwd) {
|
|
@@ -60,6 +99,46 @@ function readGateStatus(cwd) {
|
|
|
60
99
|
}
|
|
61
100
|
}
|
|
62
101
|
|
|
102
|
+
/**
|
|
103
|
+
* 从 progress.json fallback 读取 currentStage
|
|
104
|
+
* 优先级:gate-status.json > 全局 progress.json > 变更级 progress.json
|
|
105
|
+
* @param {string} cwd
|
|
106
|
+
* @returns {string|null} 阶段名,null 表示无法确定
|
|
107
|
+
*/
|
|
108
|
+
function readCurrentStage(cwd) {
|
|
109
|
+
// 1. gate-status.json(权威来源)
|
|
110
|
+
const gateStatus = readGateStatus(cwd)
|
|
111
|
+
if (gateStatus && gateStatus.stage) return gateStatus.stage
|
|
112
|
+
|
|
113
|
+
// 2. 全局 progress.json
|
|
114
|
+
const globalProgress = path.join(cwd, '.sillyspec', '.runtime', 'progress.json')
|
|
115
|
+
if (existsSync(globalProgress)) {
|
|
116
|
+
try {
|
|
117
|
+
const data = JSON.parse(readFileSync(globalProgress, 'utf8'))
|
|
118
|
+
if (data.currentStage) return data.currentStage
|
|
119
|
+
} catch { /* skip */ }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 3. 变更级 progress.json(从 global.json 找到活跃变更)
|
|
123
|
+
const globalFile = path.join(cwd, '.sillyspec', '.runtime', 'global.json')
|
|
124
|
+
if (existsSync(globalFile)) {
|
|
125
|
+
try {
|
|
126
|
+
const global = JSON.parse(readFileSync(globalFile, 'utf8'))
|
|
127
|
+
const changesDir = path.join(cwd, '.sillyspec', 'changes')
|
|
128
|
+
for (const cn of (global.activeChanges || [])) {
|
|
129
|
+
const pp = path.join(changesDir, cn, 'progress.json')
|
|
130
|
+
if (!existsSync(pp)) continue
|
|
131
|
+
try {
|
|
132
|
+
const data = JSON.parse(readFileSync(pp, 'utf8'))
|
|
133
|
+
if (data.currentStage) return data.currentStage
|
|
134
|
+
} catch { /* skip */ }
|
|
135
|
+
}
|
|
136
|
+
} catch { /* skip */ }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
|
|
63
142
|
/**
|
|
64
143
|
* 检查当前变更是否处于 noWorktree 模式
|
|
65
144
|
* @param {string} cwd
|
|
@@ -153,7 +232,6 @@ function loadLocalConfig(cwd) {
|
|
|
153
232
|
for (const p of candidates) {
|
|
154
233
|
if (!existsSync(p)) continue
|
|
155
234
|
try {
|
|
156
|
-
// 简单 YAML 解析(只处理顶层键值对和简单数组)
|
|
157
235
|
const content = readFileSync(p, 'utf8')
|
|
158
236
|
return parseSimpleYaml(content)
|
|
159
237
|
} catch {
|
|
@@ -327,6 +405,16 @@ function matchDangerBlacklist(command) {
|
|
|
327
405
|
return parts.some(p => isSingleCommandDangerous(p))
|
|
328
406
|
}
|
|
329
407
|
|
|
408
|
+
/**
|
|
409
|
+
* 构建阶段拦截提示
|
|
410
|
+
* @param {string} stage
|
|
411
|
+
* @returns {string}
|
|
412
|
+
*/
|
|
413
|
+
function buildStageHint(stage) {
|
|
414
|
+
const hint = STAGE_HINTS[stage] || STAGE_HINTS['(none)']
|
|
415
|
+
return hint.join('\n')
|
|
416
|
+
}
|
|
417
|
+
|
|
330
418
|
// ── 公共接口 ──
|
|
331
419
|
|
|
332
420
|
/**
|
|
@@ -336,6 +424,10 @@ function matchDangerBlacklist(command) {
|
|
|
336
424
|
* - noWorktree 模式下,execute/quick 阶段不允许源码写入(没有隔离环境)
|
|
337
425
|
* - 除非同时设置 SILLYSPEC_DISABLE_HOOKS=1
|
|
338
426
|
*
|
|
427
|
+
* P0 优化:
|
|
428
|
+
* - 使用 readCurrentStage() fallback 读取阶段(gate-status → progress)
|
|
429
|
+
* - 拦截提示按阶段给出具体修复建议
|
|
430
|
+
*
|
|
339
431
|
* @param {string} filePath - 目标文件绝对路径
|
|
340
432
|
* @param {string} cwd - 当前工作目录
|
|
341
433
|
* @returns {{ blocked: boolean, reason?: string }}
|
|
@@ -348,11 +440,15 @@ export function shouldBlockWrite(filePath, cwd) {
|
|
|
348
440
|
// 1. 文件门禁:文档类/配置类始终放行
|
|
349
441
|
if (matchFileWhitelist(absPath)) return { blocked: false }
|
|
350
442
|
|
|
351
|
-
// 2.
|
|
443
|
+
// 2. 阶段门禁(使用 fallback 读取)
|
|
352
444
|
const effectiveCwd = cwd || process.cwd()
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
445
|
+
const stage = readCurrentStage(effectiveCwd) || '(none)'
|
|
446
|
+
|
|
447
|
+
if (!ALLOWED_STAGES.includes(stage)) {
|
|
448
|
+
return {
|
|
449
|
+
blocked: true,
|
|
450
|
+
reason: buildStageHint(stage)
|
|
451
|
+
}
|
|
356
452
|
}
|
|
357
453
|
|
|
358
454
|
// 3. 位置门禁
|
|
@@ -360,10 +456,30 @@ export function shouldBlockWrite(filePath, cwd) {
|
|
|
360
456
|
|
|
361
457
|
// noWorktree 模式:无隔离环境,禁止源码写入(降级到更严格)
|
|
362
458
|
if (isNoWorktreeMode(effectiveCwd)) {
|
|
363
|
-
return {
|
|
459
|
+
return {
|
|
460
|
+
blocked: true,
|
|
461
|
+
reason: [
|
|
462
|
+
'当前处于 --no-worktree 降级模式,不允许源码写入。',
|
|
463
|
+
'如需修改源码,请移除 --no-worktree 标志重新执行。',
|
|
464
|
+
'紧急情况可设置 SILLYSPEC_DISABLE_HOOKS=1 绕过限制。',
|
|
465
|
+
].join('\n')
|
|
466
|
+
}
|
|
364
467
|
}
|
|
365
468
|
|
|
366
|
-
return {
|
|
469
|
+
return {
|
|
470
|
+
blocked: true,
|
|
471
|
+
reason: [
|
|
472
|
+
'源码修改只能在 worktree 隔离环境中进行。',
|
|
473
|
+
'',
|
|
474
|
+
'你可能需要:',
|
|
475
|
+
' 1. 确认 worktree 已创建:sillyspec worktree list',
|
|
476
|
+
' 2. 如未创建,先创建:sillyspec worktree create <变更名>',
|
|
477
|
+
' 3. 在 worktree 目录中工作(子代理的 cwd 设为 worktree 路径)',
|
|
478
|
+
'',
|
|
479
|
+
'如果你在 execute 阶段,通常会自动创建 worktree。',
|
|
480
|
+
'检查是否跳过了 "创建 worktree" 步骤。',
|
|
481
|
+
].join('\n')
|
|
482
|
+
}
|
|
367
483
|
}
|
|
368
484
|
|
|
369
485
|
/**
|
|
@@ -380,16 +496,19 @@ export function shouldBlockBash(command, cwd) {
|
|
|
380
496
|
// cwd 在 worktree 内 → 全部放行
|
|
381
497
|
if (isInsideWorktree(effectiveCwd)) return { blocked: false }
|
|
382
498
|
|
|
383
|
-
//
|
|
384
|
-
const
|
|
385
|
-
const stageOk =
|
|
499
|
+
// 阶段门禁(使用 fallback 读取)
|
|
500
|
+
const stage = readCurrentStage(effectiveCwd) || '(none)'
|
|
501
|
+
const stageOk = ALLOWED_STAGES.includes(stage)
|
|
386
502
|
|
|
387
503
|
if (!stageOk) {
|
|
388
504
|
// 非 execute/quick 阶段,只允许只读白名单
|
|
389
505
|
const localConfig = loadLocalConfig(effectiveCwd)
|
|
390
506
|
const extraReadonly = localConfig.worktreeHook?.readonlyCommands || localConfig['worktree-hook']?.readonlyCommands || []
|
|
391
507
|
if (matchReadonlyWhitelist(command, extraReadonly)) return { blocked: false }
|
|
392
|
-
return {
|
|
508
|
+
return {
|
|
509
|
+
blocked: true,
|
|
510
|
+
reason: buildStageHint(stage)
|
|
511
|
+
}
|
|
393
512
|
}
|
|
394
513
|
|
|
395
514
|
// execute/quick 阶段 + 主工作区
|
package/src/worktree.js
CHANGED
|
@@ -141,12 +141,42 @@ export class WorktreeManager {
|
|
|
141
141
|
throw new Error(`git worktree add 失败: ${e.stderr || e.message}`);
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
// 5.5 自动同步远程最新代码(防止 worktree 基于过时的 commit)
|
|
145
|
+
try {
|
|
146
|
+
// 先 fetch origin
|
|
147
|
+
gitQuiet(worktreePath, 'fetch origin');
|
|
148
|
+
|
|
149
|
+
// 尝试 merge origin/main(或 origin/master)到 worktree 分支
|
|
150
|
+
const defaultBranch = gitQuiet(this.cwd, 'symbolic-ref refs/remotes/origin/HEAD --short')?.replace('origin/', '')
|
|
151
|
+
|| gitQuiet(this.cwd, 'rev-parse --abbrev-ref origin/main') ? 'main'
|
|
152
|
+
: gitQuiet(this.cwd, 'rev-parse --abbrev-ref origin/master') ? 'master'
|
|
153
|
+
: null;
|
|
154
|
+
|
|
155
|
+
if (defaultBranch) {
|
|
156
|
+
// 检查 worktree 是否落后于远程
|
|
157
|
+
const localHead = gitQuiet(worktreePath, 'rev-parse HEAD');
|
|
158
|
+
const remoteHead = gitQuiet(worktreePath, `rev-parse origin/${defaultBranch}`);
|
|
159
|
+
|
|
160
|
+
if (localHead && remoteHead && localHead !== remoteHead) {
|
|
161
|
+
// 检查是否有共同祖先(避免完全不相关的分支强行 merge)
|
|
162
|
+
const mergeBase = gitQuiet(worktreePath, `merge-base ${localHead} origin/${defaultBranch}`);
|
|
163
|
+
if (mergeBase) {
|
|
164
|
+
git(worktreePath, `merge origin/${defaultBranch} --ff-only`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
// fetch/merge 失败不影响 worktree 创建,只记录警告
|
|
170
|
+
}
|
|
171
|
+
|
|
144
172
|
// 6. 写入 meta.json
|
|
145
173
|
const meta = {
|
|
146
174
|
changeName: name,
|
|
147
175
|
branch,
|
|
148
176
|
baseBranch,
|
|
149
177
|
baseHash,
|
|
178
|
+
// actualBaseHash 记录 fetch+merge 后的实际 HEAD(可能与 baseHash 不同)
|
|
179
|
+
actualBaseHash: gitQuiet(worktreePath, 'rev-parse HEAD') || baseHash,
|
|
150
180
|
createdAt: new Date().toISOString(),
|
|
151
181
|
worktreePath,
|
|
152
182
|
};
|