sillyspec 3.24.7 → 3.24.8
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/constants.js +12 -0
- package/src/fs-atomic.js +69 -0
- package/src/index.js +5 -6
- package/src/modules.js +2 -2
- package/src/progress.js +12 -7
- package/src/run.js +28 -10
- package/src/scan-postcheck.js +2 -10
- package/src/stage-contract.js +2 -9
- package/src/stages/brainstorm-auto.js +1 -1
- package/src/stages/brainstorm.js +571 -571
- package/src/stages/doctor.js +5 -5
- package/src/stages/execute.js +1 -1
- package/src/stages/plan.js +2 -2
- package/src/stages/propose.js +1 -1
- package/src/stages/status.js +1 -1
- package/src/stages/verify.js +2 -0
package/package.json
CHANGED
package/src/constants.js
CHANGED
|
@@ -68,3 +68,15 @@ export function isPointerStale(pointer) {
|
|
|
68
68
|
export function isPointerCorrupted(pointer) {
|
|
69
69
|
return !pointer || !pointer.specRoot || !pointer.savedAt
|
|
70
70
|
}
|
|
71
|
+
|
|
72
|
+
// ── scan 必需文档清单(stage-contract validator 与 scan-postcheck 共用单一真相源)──
|
|
73
|
+
// 曾分两处硬编码同一份清单,加新文档需同步改两处,易分叉。现收敛到此处一处。
|
|
74
|
+
export const SCAN_REQUIRED_DOCS = Object.freeze([
|
|
75
|
+
'ARCHITECTURE.md',
|
|
76
|
+
'CONVENTIONS.md',
|
|
77
|
+
'STRUCTURE.md',
|
|
78
|
+
'INTEGRATIONS.md',
|
|
79
|
+
'TESTING.md',
|
|
80
|
+
'CONCERNS.md',
|
|
81
|
+
'PROJECT.md',
|
|
82
|
+
])
|
package/src/fs-atomic.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fs-atomic.js — 原子文件写 + Windows 友好的 rename 重试
|
|
3
|
+
*
|
|
4
|
+
* 用于 .runtime/*.json、pointer、guard.json 等「会被其他进程 / hook 读取」的配置文件。
|
|
5
|
+
* 保证读者要么看到旧完整内容、要么看到新完整内容,不会读到半截 JSON。
|
|
6
|
+
*
|
|
7
|
+
* 注意:db.js 有自己的 _atomicWriteSync(含 .bak 备份),那是 DB 专属持久化逻辑,
|
|
8
|
+
* 已经过稳定性验证,此处不复用、不替换——保持 db.js 零改动。
|
|
9
|
+
*/
|
|
10
|
+
import { writeFileSync, renameSync, unlinkSync } from 'fs';
|
|
11
|
+
import { dirname, basename, join } from 'path';
|
|
12
|
+
|
|
13
|
+
// Windows 上杀毒 / 索引 / IDE 占用文件时,rename 偶发这些错误,短退避重试即可
|
|
14
|
+
const RENAME_RETRY_CODES = ['EPERM', 'EBUSY', 'EACCES', 'ENOTEMPTY'];
|
|
15
|
+
|
|
16
|
+
/** 同步退避:优先 Atomics.wait,不可用时退化忙等(仅在 rename 冲突的极端情况触发)。 */
|
|
17
|
+
function sleepSync(ms) {
|
|
18
|
+
try {
|
|
19
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
20
|
+
} catch {
|
|
21
|
+
const end = Date.now() + ms;
|
|
22
|
+
while (Date.now() < end) { /* busy wait */ }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* rename 带退避重试,覆盖 Windows 偶发的 EPERM/EBUSY/EACCES/ENOTEMPTY。
|
|
28
|
+
* 目录 rename 同样适用(归档移动目录时可复用)。
|
|
29
|
+
* @param {string} from
|
|
30
|
+
* @param {string} to
|
|
31
|
+
* @param {number} [retries=5]
|
|
32
|
+
*/
|
|
33
|
+
export function renameSyncRetry(from, to, retries = 5) {
|
|
34
|
+
let lastErr;
|
|
35
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
36
|
+
try {
|
|
37
|
+
renameSync(from, to);
|
|
38
|
+
return;
|
|
39
|
+
} catch (err) {
|
|
40
|
+
lastErr = err;
|
|
41
|
+
const code = err && err.code;
|
|
42
|
+
// 非可重试错误码立即抛出,不浪费时间
|
|
43
|
+
if (!code || !RENAME_RETRY_CODES.includes(code)) throw err;
|
|
44
|
+
if (attempt < retries) sleepSync(15 * (attempt + 1));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
throw lastErr;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 原子写文本文件:同目录 tmp 写入 → rename 覆盖目标(带 Windows 退避重试)。
|
|
52
|
+
* - tmp 名含 pid,避免多进程并发写同一目标时 tmp 名碰撞(如多会话写 gate-status.json)。
|
|
53
|
+
* - rename 失败时清理 tmp,避免留下孤儿 .tmp 文件。
|
|
54
|
+
* - Node 的 rename 在 Windows 上用 MoveFileEx(REPLACE_EXISTING),可覆盖已存在目标;
|
|
55
|
+
* EPERM 仅在文件被其他进程锁定时出现,由 renameSyncRetry 退避重试。
|
|
56
|
+
* @param {string} filePath - 目标文件绝对路径
|
|
57
|
+
* @param {string} content - 文件内容
|
|
58
|
+
*/
|
|
59
|
+
export function writeAtomicSync(filePath, content) {
|
|
60
|
+
const dir = dirname(filePath);
|
|
61
|
+
const tmpPath = join(dir, `.${basename(filePath)}.${process.pid}.tmp`);
|
|
62
|
+
writeFileSync(tmpPath, content);
|
|
63
|
+
try {
|
|
64
|
+
renameSyncRetry(tmpPath, filePath);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
try { unlinkSync(tmpPath); } catch { /* tmp 可能已被 rename 走,忽略 */ }
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/index.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* 状态管理通过 sillyspec.db(SQLite)完成,使用 `sillyspec progress` 命令。
|
|
8
8
|
*/
|
|
9
9
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs';
|
|
10
|
+
import { writeAtomicSync } from './fs-atomic.js';
|
|
10
11
|
import { join, resolve } from 'path';
|
|
11
12
|
import { cmdInit, getVersion } from './init.js';
|
|
12
13
|
import { ProgressManager, resolvePlatformSpecDir } from './progress.js';
|
|
@@ -280,7 +281,7 @@ async function main() {
|
|
|
280
281
|
}
|
|
281
282
|
case 'batch': {
|
|
282
283
|
if (filteredArgs.includes('--status')) {
|
|
283
|
-
const bp = pm.readBatchProgress(dir, progChangeName);
|
|
284
|
+
const bp = await pm.readBatchProgress(dir, progChangeName);
|
|
284
285
|
if (!bp) { console.log('📭 无批量进度数据'); break; }
|
|
285
286
|
const line = pm._renderBatchProgress(bp);
|
|
286
287
|
console.log(line || '📭 无批量进度数据');
|
|
@@ -299,7 +300,7 @@ async function main() {
|
|
|
299
300
|
console.log(' sillyspec progress batch --status');
|
|
300
301
|
break;
|
|
301
302
|
}
|
|
302
|
-
pm.updateBatchProgress(dir, batchData, progChangeName);
|
|
303
|
+
await pm.updateBatchProgress(dir, batchData, progChangeName);
|
|
303
304
|
console.log('✅ 批量进度已更新');
|
|
304
305
|
}
|
|
305
306
|
break;
|
|
@@ -1171,11 +1172,9 @@ SillySpec modules — 模块文档管理
|
|
|
1171
1172
|
lines.push('# order-service: "order/"');
|
|
1172
1173
|
const yamlText = lines.join('\n') + '\n';
|
|
1173
1174
|
|
|
1174
|
-
//
|
|
1175
|
+
// 原子写(tmp + rename + Windows EPERM 重试),避免半截 local.yaml
|
|
1175
1176
|
mkdirSync(specRoot, { recursive: true });
|
|
1176
|
-
|
|
1177
|
-
writeFileSync(tmpPath, yamlText, 'utf8');
|
|
1178
|
-
renameSync(tmpPath, localYamlPath);
|
|
1177
|
+
writeAtomicSync(localYamlPath, yamlText);
|
|
1179
1178
|
|
|
1180
1179
|
console.log(`✅ 已生成 local.yaml (type: ${detected.project.type})`);
|
|
1181
1180
|
console.log(` 路径: ${localYamlPath}`);
|
package/src/modules.js
CHANGED
|
@@ -79,7 +79,7 @@ export async function rebuildModuleMap(cwd) {
|
|
|
79
79
|
const content = readFileSync(join(modulesDir, f), 'utf8');
|
|
80
80
|
const fm = parseModuleCardFrontmatter(content);
|
|
81
81
|
const moduleId = fm.module_id || parseModuleIdFromFilename(f);
|
|
82
|
-
cards.push({ filename: f, moduleId });
|
|
82
|
+
cards.push({ filename: f, moduleId, content });
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -134,7 +134,7 @@ export async function rebuildModuleMap(cwd) {
|
|
|
134
134
|
|
|
135
135
|
for (const moduleId of allModuleIds) {
|
|
136
136
|
const card = cards.find(c => c.moduleId === moduleId);
|
|
137
|
-
const cardContent = card
|
|
137
|
+
const cardContent = card?.content || '';
|
|
138
138
|
|
|
139
139
|
// 从模块卡片提取 context index 字段
|
|
140
140
|
const role = extractSection(cardContent, '定位') || '';
|
package/src/progress.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, appendFileSync } from 'fs';
|
|
14
14
|
import { join, basename, dirname, resolve } from 'path';
|
|
15
15
|
import { DB } from './db.js';
|
|
16
|
+
import { writeAtomicSync } from './fs-atomic.js';
|
|
16
17
|
|
|
17
18
|
// 默认规范目录名(相对于 cwd)
|
|
18
19
|
const SPEC_DIR_NAME = '.sillyspec';
|
|
@@ -156,7 +157,14 @@ export class ProgressManager {
|
|
|
156
157
|
/** 获取 specDir(优先自定义,否则向上查找含 .sillyspec 的目录,fallback 到 cwd/.sillyspec) */
|
|
157
158
|
_getSpecDir(cwd) {
|
|
158
159
|
if (this._customSpecDir) return this._customSpecDir;
|
|
159
|
-
|
|
160
|
+
// specDir 对给定 cwd 在进程生命周期内不变(.sillyspec 目录不会被移动),
|
|
161
|
+
// 按 cwd 缓存,避免每次路径解析都向上 existsSync 遍历目录树(热路径)。
|
|
162
|
+
if (!this._specDirCache) this._specDirCache = new Map();
|
|
163
|
+
const cached = this._specDirCache.get(cwd);
|
|
164
|
+
if (cached) return cached;
|
|
165
|
+
const resolved = resolveSpecDir(cwd);
|
|
166
|
+
this._specDirCache.set(cwd, resolved);
|
|
167
|
+
return resolved;
|
|
160
168
|
}
|
|
161
169
|
|
|
162
170
|
_runtimePath(cwd, ...parts) {
|
|
@@ -1064,7 +1072,7 @@ export class ProgressManager {
|
|
|
1064
1072
|
mkdirSync(historyDir, { recursive: true });
|
|
1065
1073
|
const ts = now.replace(/[:.TZ-]/g, '');
|
|
1066
1074
|
const stageData = data.stages[stage];
|
|
1067
|
-
|
|
1075
|
+
writeAtomicSync(
|
|
1068
1076
|
join(historyDir, `${cn}-${stage}-${ts}.json`),
|
|
1069
1077
|
JSON.stringify({ change: cn, stage, data: stageData, completedAt: now }, null, 2) + '\n'
|
|
1070
1078
|
);
|
|
@@ -1666,8 +1674,7 @@ export class ProgressManager {
|
|
|
1666
1674
|
const changeId = changeRow[0].values[0][0];
|
|
1667
1675
|
sqlDb.run('DELETE FROM steps WHERE stage_id IN (SELECT id FROM stages WHERE change_id = ?)', [changeId]);
|
|
1668
1676
|
sqlDb.run('DELETE FROM stages WHERE change_id = ?', [changeId]);
|
|
1669
|
-
|
|
1670
|
-
// 重新插入所有阶段
|
|
1677
|
+
// 重新插入所有阶段(注意:上方已 DELETE stages,无需再 UPDATE)
|
|
1671
1678
|
for (const s of VALID_STAGES) {
|
|
1672
1679
|
sqlDb.run('INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, "pending")', [changeId, s]);
|
|
1673
1680
|
}
|
|
@@ -1865,9 +1872,7 @@ export class ProgressManager {
|
|
|
1865
1872
|
updatedAt: new Date().toISOString(),
|
|
1866
1873
|
...(hasNoWorktree ? { noWorktree: true } : {}),
|
|
1867
1874
|
};
|
|
1868
|
-
|
|
1869
|
-
writeFileSync(tmpPath, JSON.stringify(gateData, null, 2) + '\n');
|
|
1870
|
-
renameSync(tmpPath, gatePath);
|
|
1875
|
+
writeAtomicSync(gatePath, JSON.stringify(gateData, null, 2) + '\n');
|
|
1871
1876
|
} catch (err) {
|
|
1872
1877
|
console.warn('⚠️ 写入 gate-status.json 失败:', err.message);
|
|
1873
1878
|
}
|
package/src/run.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { basename, join, resolve, dirname, relative, isAbsolute } from 'path'
|
|
8
8
|
import { existsSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, readFileSync, rmSync, statSync } from 'fs'
|
|
9
|
+
import { writeAtomicSync, renameSyncRetry } from './fs-atomic.js'
|
|
9
10
|
import { randomBytes, randomUUID } from 'crypto'
|
|
10
11
|
import { createRequire } from 'module'
|
|
11
12
|
const require = createRequire(import.meta.url)
|
|
@@ -1245,12 +1246,15 @@ async function executeScanPreflight(cwd, platformOpts, scanProfile) {
|
|
|
1245
1246
|
const projectName = basename(cwd)
|
|
1246
1247
|
console.log(` 📁 项目: ${projectName}`)
|
|
1247
1248
|
console.log(` 📊 Profile: ${scanProfile.mode} (${scanProfile.reason})`)
|
|
1248
|
-
//
|
|
1249
|
+
// 快速列出顶层结构(readdirSync 跨平台,避免 spawn shell + 5 个 grep 进程)
|
|
1249
1250
|
try {
|
|
1250
|
-
const
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
|
|
1251
|
+
const skip = new Set(['node_modules', '.git', '.sillyspec', '.claude'])
|
|
1252
|
+
const dirNames = readdirSync(cwd, { withFileTypes: true })
|
|
1253
|
+
.filter(d => d.isDirectory() && !skip.has(d.name) && !d.name.startsWith('.'))
|
|
1254
|
+
.slice(0, 20)
|
|
1255
|
+
.map(d => d.name)
|
|
1256
|
+
if (dirNames.length) {
|
|
1257
|
+
console.log(` 📂 目录: ${dirNames.join(', ')}`)
|
|
1254
1258
|
}
|
|
1255
1259
|
} catch {}
|
|
1256
1260
|
console.log(` ✅ Preflight 完成,准备生成核心文档\n`)
|
|
@@ -2199,7 +2203,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
2199
2203
|
// 写入 .runtime/quick-sessions/<sessionId>/guard.json 供 worktree-guard hook 读取
|
|
2200
2204
|
// (D-002:按 session 存,多会话各自 guard 不互覆盖。runStage 作用域内 sessionId == changeName == quick-<uuid8>,见 §4.4/4.5)
|
|
2201
2205
|
mkdirSync(sessionGuardDir, { recursive: true })
|
|
2202
|
-
|
|
2206
|
+
writeAtomicSync(guardFile, JSON.stringify(progress.quickGuard, null, 2))
|
|
2203
2207
|
const parts = [`${baselineFiles.length} 个已有脏文件`]
|
|
2204
2208
|
if (allowedFiles.length > 0) parts.push(`${allowedFiles.length} 个 allowedFiles`)
|
|
2205
2209
|
if (allowNew) parts.push('允许新增文件')
|
|
@@ -2383,7 +2387,13 @@ async function archiveChangeDirectory(pm, cwd, progress, specBase) {
|
|
|
2383
2387
|
process.exit(1)
|
|
2384
2388
|
}
|
|
2385
2389
|
mkdirSync(archiveDir, { recursive: true })
|
|
2386
|
-
|
|
2390
|
+
try {
|
|
2391
|
+
renameSyncRetry(srcDir, destDir)
|
|
2392
|
+
} catch (e) {
|
|
2393
|
+
console.error(`❌ 归档失败:移动变更目录时出错(${e.message})`)
|
|
2394
|
+
console.error(` 常见原因:变更目录内文件被 IDE / 杀毒 / 索引占用,已重试 5 次仍失败。请关闭相关程序后重试。`)
|
|
2395
|
+
process.exit(1)
|
|
2396
|
+
}
|
|
2387
2397
|
|
|
2388
2398
|
if (!existsSync(destDir) || existsSync(srcDir)) {
|
|
2389
2399
|
console.error('❌ 归档校验失败:移动操作异常')
|
|
@@ -2749,7 +2759,11 @@ async function enforceDepsGate(stageName, cwd, changeName, step, steps, currentI
|
|
|
2749
2759
|
const { WorktreeManager } = await import('./worktree.js')
|
|
2750
2760
|
wm = new WorktreeManager({ cwd })
|
|
2751
2761
|
meta = wm.getMeta(changeName)
|
|
2752
|
-
} catch {
|
|
2762
|
+
} catch (e) {
|
|
2763
|
+
// WorktreeManager 构造/读取失败(git 不可用 / worktree 元数据损坏)不阻断 deps gate;
|
|
2764
|
+
// 下游已有物理目录存在性判定兜底(G2/R3),这里 warn 留下根因线索,避免静默误诊。
|
|
2765
|
+
console.warn(`⚠️ worktree meta 读取失败(deps gate 将走物理目录判定): ${e.message}`)
|
|
2766
|
+
}
|
|
2753
2767
|
const depsStatus = meta?.depsStatus
|
|
2754
2768
|
if (['linked', 'installed', 'n/a'].includes(depsStatus)) return true
|
|
2755
2769
|
const changeDir = changeName ? join(specBase, 'changes', changeName) : null
|
|
@@ -3402,8 +3416,12 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
3402
3416
|
pointer.status = POINTER_STATUS.SCAN_COMPLETED
|
|
3403
3417
|
pointer.completedAt = new Date().toISOString()
|
|
3404
3418
|
pointer.scanStatus = postResult.status
|
|
3405
|
-
|
|
3406
|
-
} catch {
|
|
3419
|
+
writeAtomicSync(pointerPath, JSON.stringify(pointer, null, 2) + '\n')
|
|
3420
|
+
} catch (e) {
|
|
3421
|
+
// 不阻断 scan 主流程,但暴露失败——pointer 写失败会让平台看不到 scan_completed,
|
|
3422
|
+
// 与项目 fail-loud 原则一致:宁可可见地 warn,也不静默吞错。
|
|
3423
|
+
console.warn(`⚠️ 更新平台指针状态失败(scan_completed 可能未落盘): ${e.message}`)
|
|
3424
|
+
}
|
|
3407
3425
|
|
|
3408
3426
|
// failed_post_check 时强制阻止 clean success
|
|
3409
3427
|
if (postResult.status === 'failed_post_check') {
|
package/src/scan-postcheck.js
CHANGED
|
@@ -7,17 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync } from 'fs'
|
|
9
9
|
import { join, basename } from 'path'
|
|
10
|
-
import { SCAN_STATUS, CHECK_SEVERITY } from './constants.js'
|
|
10
|
+
import { SCAN_STATUS, CHECK_SEVERITY, SCAN_REQUIRED_DOCS } from './constants.js'
|
|
11
11
|
|
|
12
|
-
const REQUIRED_SCAN_DOCS =
|
|
13
|
-
'ARCHITECTURE.md',
|
|
14
|
-
'CONVENTIONS.md',
|
|
15
|
-
'STRUCTURE.md',
|
|
16
|
-
'INTEGRATIONS.md',
|
|
17
|
-
'TESTING.md',
|
|
18
|
-
'CONCERNS.md',
|
|
19
|
-
'PROJECT.md',
|
|
20
|
-
]
|
|
12
|
+
const REQUIRED_SCAN_DOCS = SCAN_REQUIRED_DOCS
|
|
21
13
|
|
|
22
14
|
/**
|
|
23
15
|
* @param {object} opts
|
package/src/stage-contract.js
CHANGED
|
@@ -9,6 +9,7 @@ import { existsSync, readdirSync, readFileSync } from 'fs'
|
|
|
9
9
|
import { join, basename } from 'path'
|
|
10
10
|
import { execFileSync } from 'child_process'
|
|
11
11
|
import { detectChangeRisk, checkIntegrationEvidence } from './change-risk-profile.js'
|
|
12
|
+
import { SCAN_REQUIRED_DOCS } from './constants.js'
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* 校验结果
|
|
@@ -215,15 +216,7 @@ function validateScanOutputs(cwd, changeName, context = {}) {
|
|
|
215
216
|
? join(specBase, 'docs', projectName, 'scan')
|
|
216
217
|
: join(specBase, 'docs', 'scan')
|
|
217
218
|
|
|
218
|
-
const requiredDocs =
|
|
219
|
-
'ARCHITECTURE.md',
|
|
220
|
-
'CONVENTIONS.md',
|
|
221
|
-
'STRUCTURE.md',
|
|
222
|
-
'INTEGRATIONS.md',
|
|
223
|
-
'TESTING.md',
|
|
224
|
-
'CONCERNS.md',
|
|
225
|
-
'PROJECT.md',
|
|
226
|
-
]
|
|
219
|
+
const requiredDocs = SCAN_REQUIRED_DOCS
|
|
227
220
|
|
|
228
221
|
const errors = []
|
|
229
222
|
const warnings = []
|
|
@@ -25,7 +25,7 @@ export const definition = {
|
|
|
25
25
|
3. **检查变更名称**:如果当前变更名是自动生成的(如 \`2026-06-02-new-change-a3f2b7c1\`),直接重命名为有意义名称,然后运行 \`sillyspec change-rename <旧名> <新名>\`
|
|
26
26
|
|
|
27
27
|
### 加载上下文
|
|
28
|
-
4.
|
|
28
|
+
4. 读取项目总览 \`.sillyspec/docs/<project>/scan/PROJECT.md\`(如存在)+ 共享规范 + 子项目上下文
|
|
29
29
|
5. 加载项目信息:\`cat .sillyspec/projects/*.yaml 2>/dev/null\`
|
|
30
30
|
6. 加载本地配置:\`cat .sillyspec/local.yaml 2>/dev/null\`
|
|
31
31
|
7. 棕地项目:读取 scan 下的 STRUCTURE.md、CONVENTIONS.md、ARCHITECTURE.md
|