sillyspec 3.24.8 → 3.25.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/package.json +1 -1
- package/src/constants.js +13 -0
- package/src/contract-matrix.js +48 -20
- package/src/db.js +28 -8
- package/src/endpoint-extractor.js +92 -6
- package/src/hooks/worktree-guard.js +25 -6
- package/src/index.js +6 -6
- package/src/machine-interface.js +13 -7
- package/src/progress.js +46 -50
- package/src/run/command.js +1011 -0
- package/src/run/complete-handlers.js +873 -0
- package/src/run/complete.js +695 -0
- package/src/run/gates.js +321 -0
- package/src/run/prompt.js +604 -0
- package/src/run/quick-audit.js +102 -0
- package/src/run/scan-profile.js +238 -0
- package/src/run/shared.js +375 -0
- package/src/run/stage.js +414 -0
- package/src/run.js +23 -4278
- package/src/stage-contract.js +11 -11
- package/src/stages/index.js +1 -1
- package/src/verify-postcheck.js +41 -0
- package/src/worktree-apply.js +45 -18
- package/src/worktree.js +30 -13
package/src/stage-contract.js
CHANGED
|
@@ -9,7 +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
|
+
import { SCAN_REQUIRED_DOCS, AUXILIARY_STAGES } from './constants.js'
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* 校验结果
|
|
@@ -661,7 +661,7 @@ export function checkExecuteCodeEvidence(cwd, changeName) {
|
|
|
661
661
|
* 防止"勾选 checkbox = 完成 execute"的谎报路径。
|
|
662
662
|
*/
|
|
663
663
|
function validateExecuteOutputs(cwd, changeName, context = {}) {
|
|
664
|
-
const { specRoot } = context
|
|
664
|
+
const { specRoot, evidence: passedEvidence } = context
|
|
665
665
|
const errors = []
|
|
666
666
|
const warnings = []
|
|
667
667
|
|
|
@@ -674,7 +674,9 @@ function validateExecuteOutputs(cwd, changeName, context = {}) {
|
|
|
674
674
|
const hasTasks = /^\s*[-*]\s*\[[ xX]\]\s*task-\d+/m.test(planContent)
|
|
675
675
|
if (!hasTasks) return { ok: true, errors, warnings }
|
|
676
676
|
|
|
677
|
-
|
|
677
|
+
// W4-G (D-008):evidence 可由调用方(runGate)预先算好传入,避免与 execute-evidence check
|
|
678
|
+
// 重复 spawn git(gate execute 一次省 2 个 git 进程 ≈ 60-200ms on Windows)。未传则内部算(向后兼容)。
|
|
679
|
+
const evidence = passedEvidence || checkExecuteCodeEvidence(cwd, changeName)
|
|
678
680
|
if (evidence.status === 'unchanged') {
|
|
679
681
|
errors.push(`execute 代码变更核验失败:plan.md 声明了任务,但 ${evidence.detail} — 勾选 checkbox 不等于完成实现`)
|
|
680
682
|
} else if (evidence.status === 'unknown') {
|
|
@@ -691,10 +693,8 @@ function validateExecuteOutputs(cwd, changeName, context = {}) {
|
|
|
691
693
|
*/
|
|
692
694
|
const mainFlowStages = ['brainstorm', 'plan', 'execute', 'verify']
|
|
693
695
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
*/
|
|
697
|
-
const auxiliaryStages = ['scan', 'quick', 'explore', 'archive', 'status', 'doctor']
|
|
696
|
+
// 辅助阶段(可独立运行,无严格转换顺序)定义在 constants.js 的 AUXILIARY_STAGES,
|
|
697
|
+
// 与 stages/index.js 共用单一真相源,避免两处逐字重复分叉。
|
|
698
698
|
|
|
699
699
|
/**
|
|
700
700
|
* @type {Object<string, StageContract>}
|
|
@@ -805,7 +805,7 @@ export function checkTransition(fromStage, toStage, options = {}) {
|
|
|
805
805
|
}
|
|
806
806
|
|
|
807
807
|
// 辅助阶段随时可执行(archive 除外:从主流程进入 archive 需要校验)
|
|
808
|
-
if (
|
|
808
|
+
if (AUXILIARY_STAGES.includes(toStage) && toStage !== 'archive') {
|
|
809
809
|
return { allowed: true }
|
|
810
810
|
}
|
|
811
811
|
|
|
@@ -831,14 +831,14 @@ export function checkTransition(fromStage, toStage, options = {}) {
|
|
|
831
831
|
return { allowed: true }
|
|
832
832
|
}
|
|
833
833
|
// 独立运行 archive(无前置)也允许
|
|
834
|
-
if (!fromStage ||
|
|
834
|
+
if (!fromStage || AUXILIARY_STAGES.includes(fromStage)) {
|
|
835
835
|
return { allowed: true }
|
|
836
836
|
}
|
|
837
837
|
return { allowed: false, reason: 'archive 的前置阶段是 verify,不能从 ' + fromStage + ' 跳转' }
|
|
838
838
|
}
|
|
839
839
|
|
|
840
840
|
// 从辅助阶段进入主流程:允许
|
|
841
|
-
if (
|
|
841
|
+
if (AUXILIARY_STAGES.includes(fromStage)) {
|
|
842
842
|
return { allowed: true }
|
|
843
843
|
}
|
|
844
844
|
|
|
@@ -892,4 +892,4 @@ export function runValidators(stageName, cwd, changeName, context = {}) {
|
|
|
892
892
|
/**
|
|
893
893
|
* 获取所有主流程阶段
|
|
894
894
|
*/
|
|
895
|
-
export { mainFlowStages, auxiliaryStages }
|
|
895
|
+
export { mainFlowStages, AUXILIARY_STAGES as auxiliaryStages }
|
package/src/stages/index.js
CHANGED
package/src/verify-postcheck.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { execSync } from 'child_process'
|
|
19
19
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
20
20
|
import { join } from 'path'
|
|
21
|
+
import { verifyApiParity } from './contract-matrix.js'
|
|
21
22
|
|
|
22
23
|
// 测试命令最长执行时间;超时视为失败(防止 CLI 被挂起的测试卡死)
|
|
23
24
|
const TEST_TIMEOUT_MS = Number(process.env.SILLYSPEC_TEST_TIMEOUT_MS) || 10 * 60 * 1000
|
|
@@ -504,3 +505,43 @@ export function printVerifyTestCheck(result) {
|
|
|
504
505
|
console.log(`📄 实测结果已写入: ${result.resultPath}`)
|
|
505
506
|
}
|
|
506
507
|
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* API parity 对账(advisory):前端调用 vs execute 提取的后端 endpoint artifact。
|
|
511
|
+
* 三态:skipped(无 provider artifact → 非全栈项目不打扰)/ warning(missingBackend>0)/ passed。
|
|
512
|
+
* 启发式正则、多假阳源 → advisory 不阻断 verify 完成(与 SillySpec 确定性校验定位一致)。
|
|
513
|
+
*
|
|
514
|
+
* @param {object} opts
|
|
515
|
+
* @param {string} opts.cwd - 扫描前端调用的根(主工作区,verify 时代码在此)
|
|
516
|
+
* @param {string} opts.specBase
|
|
517
|
+
* @param {string|null} [opts.changeName]
|
|
518
|
+
* @param {string|null} [opts.runtimeRoot]
|
|
519
|
+
* @returns {{ status: 'skipped'|'warning'|'passed', missingBackend: Array, unusedBackend: Array, summary: string, reason: string|null }}
|
|
520
|
+
*/
|
|
521
|
+
export function runVerifyParityCheck({ cwd, specBase, changeName = null, runtimeRoot = null }) {
|
|
522
|
+
const r = verifyApiParity(specBase, cwd, runtimeRoot, changeName)
|
|
523
|
+
// 无 provider artifact(execute 未提取 / 非后端项目)→ 不打扰
|
|
524
|
+
if (r.backendCount === 0) {
|
|
525
|
+
return { status: 'skipped', missingBackend: [], unusedBackend: [], summary: r.summary, reason: '无后端契约 artifact(非全栈项目或 execute 未提取端点)' }
|
|
526
|
+
}
|
|
527
|
+
if (r.missingBackend.length > 0) {
|
|
528
|
+
return { status: 'warning', missingBackend: r.missingBackend, unusedBackend: r.unusedBackend, summary: r.summary, reason: null }
|
|
529
|
+
}
|
|
530
|
+
return { status: 'passed', missingBackend: [], unusedBackend: r.unusedBackend, summary: r.summary, reason: null }
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** 打印 parity 对账结果(advisory,不阻断) */
|
|
534
|
+
export function printVerifyParityCheck(result) {
|
|
535
|
+
if (result.status === 'skipped') return // 静默,不打扰非全栈项目
|
|
536
|
+
if (result.status === 'passed') {
|
|
537
|
+
console.log(`\n✅ API parity 对账通过:${result.summary}`)
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
// warning
|
|
541
|
+
console.warn(`\n⚠️ API parity 对账发现 ${result.missingBackend.length} 个前端调用无对应后端端点(advisory,不阻断归档):`)
|
|
542
|
+
for (const m of result.missingBackend.slice(0, 20)) {
|
|
543
|
+
console.warn(` - ${m.method} ${m.path} ← ${m.consumerFile}:${m.consumerLine}`)
|
|
544
|
+
}
|
|
545
|
+
if (result.missingBackend.length > 20) console.warn(` …还有 ${result.missingBackend.length - 20} 个`)
|
|
546
|
+
console.warn(' 提示:检查是否后端漏实现,或前端调用了尚未实现的端点。确认无误可在 design.md 标注豁免。')
|
|
547
|
+
}
|
package/src/worktree-apply.js
CHANGED
|
@@ -49,14 +49,34 @@ export function filterDeliverableFiles(files) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
52
|
+
* 批量获取多个文件在某个 treeish 中的 blob hash(一次 git ls-tree 替代 N 次 rev-parse)。
|
|
53
|
+
*
|
|
54
|
+
* 语义等价于对每个文件调 `git rev-parse <treeish>:<path>`:
|
|
55
|
+
* - 文件在 tree 中 → Map<f, hash>(与 rev-parse 返回的同一 blob hash)
|
|
56
|
+
* - 文件不在 tree 中 → 不在 Map 中(调用方 map.get(f) ?? null 得 null,等同 rev-parse 失败→null)
|
|
57
|
+
* ls-tree 输出 "<mode> <type> <hash>\t<path>"。path 恒为文件路径(来自 git diff/ls-files),
|
|
58
|
+
* 非目录,故不带 -r 也正确——文件在 tree 时 rev-parse treeish:path 与 ls-tree 都给同一 hash,
|
|
59
|
+
* 不在时都不给,等价。(沿用 git() 字符串拼接模式,不引入新的引号/空格破法。)
|
|
60
|
+
*
|
|
61
|
+
* @param {string} cwd
|
|
62
|
+
* @param {string} treeish
|
|
63
|
+
* @param {string[]} files
|
|
64
|
+
* @returns {Map<string, string>} path → blob hash(仅含存在于 tree 中的文件)
|
|
57
65
|
*/
|
|
58
|
-
function
|
|
59
|
-
|
|
66
|
+
function getBlobHashMap(cwd, treeish, files) {
|
|
67
|
+
const map = new Map();
|
|
68
|
+
if (files.length === 0) return map; // 空 pathspec 会让 ls-tree 列出整棵树,必须拦截
|
|
69
|
+
const raw = gitQuiet(cwd, `ls-tree ${treeish} -- ${files.join(' ')}`);
|
|
70
|
+
if (!raw) return map;
|
|
71
|
+
for (const line of raw.split('\n')) {
|
|
72
|
+
if (!line) continue;
|
|
73
|
+
const tabIdx = line.indexOf('\t');
|
|
74
|
+
if (tabIdx === -1) continue;
|
|
75
|
+
const filePath = line.slice(tabIdx + 1);
|
|
76
|
+
const hash = line.slice(0, tabIdx).split(' ')[2]; // "<mode> <type> <hash>"
|
|
77
|
+
if (hash) map.set(filePath, hash);
|
|
78
|
+
}
|
|
79
|
+
return map;
|
|
60
80
|
}
|
|
61
81
|
|
|
62
82
|
/**
|
|
@@ -203,10 +223,14 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
203
223
|
}
|
|
204
224
|
|
|
205
225
|
// 5b. 对比 worktree 的 baseHash 和主工作区 HEAD 中每个清单文件的 blob hash
|
|
226
|
+
// 批量化:两次 ls-tree(worktree 的 baseHash + 主仓库 HEAD)各建一张 path→hash Map,
|
|
227
|
+
// 替代 per-file getFileBlobHash × 2(原 2N spawn → 固定 2)。语义等价见 getBlobHashMap。
|
|
206
228
|
const targetFiles = hasAllowList ? [...allowSet] : changedFiles;
|
|
229
|
+
const wtHashMap = getBlobHashMap(worktreePath, baseHash, targetFiles);
|
|
230
|
+
const mainHashMap = getBlobHashMap(projectRoot, 'HEAD', targetFiles);
|
|
207
231
|
for (const f of targetFiles) {
|
|
208
|
-
const wtBlob =
|
|
209
|
-
const mainBlob =
|
|
232
|
+
const wtBlob = wtHashMap.get(f) ?? null;
|
|
233
|
+
const mainBlob = mainHashMap.get(f) ?? null;
|
|
210
234
|
|
|
211
235
|
// 两者都为 null(文件在 base 时不存在)→ OK
|
|
212
236
|
if (wtBlob === null && mainBlob === null) continue;
|
|
@@ -234,7 +258,6 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
234
258
|
const patchFiles = hasAllowList
|
|
235
259
|
? [...allowSet].filter(f => changedFiles.includes(f))
|
|
236
260
|
: changedFiles;
|
|
237
|
-
const fileArgs = patchFiles.length > 0 ? `-- ${patchFiles.join(' ')}` : '';
|
|
238
261
|
|
|
239
262
|
// 创建临时文件
|
|
240
263
|
const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-patch-'));
|
|
@@ -245,14 +268,18 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
245
268
|
let patchContent = '';
|
|
246
269
|
|
|
247
270
|
// 分 tracked 变更和 untracked 新文件生成 patch
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
271
|
+
// 批量化:一次 ls-tree(diffBase tree 中存在的文件)+ 一次 ls-files(index 中存在的文件)
|
|
272
|
+
// 建集合,替代 per-file cat-file -e / ls-files --error-unmatch(原至多 2N spawn → 固定 2)。
|
|
273
|
+
// 语义等价:cat-file -e diffBase:f 成功 ⟺ f 在 ls-tree diffBase 输出(getBlobHashMap key);
|
|
274
|
+
// ls-files --error-unmatch f 成功 ⟺ f 在 ls-files -- 输出。
|
|
275
|
+
const inTree = getBlobHashMap(worktreePath, diffBase, patchFiles);
|
|
276
|
+
const inIndexList = patchFiles.length > 0
|
|
277
|
+
? (gitQuiet(worktreePath, `ls-files -- ${patchFiles.join(' ')}`) || '').split('\n').filter(Boolean)
|
|
278
|
+
: [];
|
|
279
|
+
const inIndex = new Set(inIndexList);
|
|
280
|
+
const trackedFiles = patchFiles.filter(f => inTree.has(f) || inIndex.has(f));
|
|
281
|
+
const trackedSet = new Set(trackedFiles);
|
|
282
|
+
const untrackedPatchFiles = patchFiles.filter(f => !trackedSet.has(f));
|
|
256
283
|
|
|
257
284
|
// tracked 文件:git diff baseHash
|
|
258
285
|
if (trackedFiles.length > 0) {
|
package/src/worktree.js
CHANGED
|
@@ -13,11 +13,21 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync
|
|
|
13
13
|
import { join, resolve, dirname } from 'path';
|
|
14
14
|
import { createHash } from 'crypto';
|
|
15
15
|
import { provisionDeps, lockfileHash } from './worktree-deps.js';
|
|
16
|
+
import { writeAtomicSync } from './fs-atomic.js';
|
|
17
|
+
|
|
18
|
+
// meta.json 会被 hook 进程与其它 CLI 进程并发读取(worktree-guard / getMeta / create 幽灵判定),
|
|
19
|
+
// 必须原子写:半截 JSON 会让 getMeta 返回 null → 触发幽灵 worktree 强删(可能丢 gitignored 改动)。
|
|
20
|
+
const writeMetaAtomic = (metaPath, meta) => writeAtomicSync(metaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
16
21
|
|
|
17
22
|
const WORKTREES_REL = '.sillyspec/.runtime/worktrees';
|
|
18
23
|
const BRANCH_PREFIX = 'sillyspec/';
|
|
19
24
|
const META_FILE = 'meta.json';
|
|
20
25
|
|
|
26
|
+
// 进程级缓存:cwd → 主仓库根目录。避免每次 new WorktreeManager 都 spawn 一次
|
|
27
|
+
// `git rev-parse --git-common-dir`(Windows 上每次 spawn 约 30-100ms,execute 一次命令
|
|
28
|
+
// 会 new 多个 WorktreeManager)。git-common-dir 在进程内对同一 cwd 稳定,缓存安全。
|
|
29
|
+
const _mainRepoRootByCwd = new Map();
|
|
30
|
+
|
|
21
31
|
/**
|
|
22
32
|
* 检测当前目录的隔离状态
|
|
23
33
|
* 返回 { inWorktree: boolean, inSubmodule: boolean }
|
|
@@ -224,24 +234,27 @@ export class WorktreeManager {
|
|
|
224
234
|
* @private
|
|
225
235
|
*/
|
|
226
236
|
_resolveMainRepoRoot() {
|
|
237
|
+
const cached = _mainRepoRootByCwd.get(this.cwd);
|
|
238
|
+
if (cached !== undefined) return cached;
|
|
239
|
+
|
|
240
|
+
let root = this.cwd;
|
|
227
241
|
try {
|
|
228
242
|
// git-common-dir 在主仓库内 = <main>/.git
|
|
229
243
|
// 在 linked worktree 内 = <main>/.git(git 共享 .git 目录)
|
|
230
244
|
const commonDir = gitQuiet(this.cwd, 'rev-parse --git-common-dir');
|
|
231
|
-
if (!commonDir) return this.cwd;
|
|
232
|
-
|
|
233
245
|
// commonDir 应该是 <main-repo>/.git
|
|
234
246
|
// dirname(commonDir) = <main-repo>
|
|
235
|
-
if (existsSync(commonDir)) {
|
|
247
|
+
if (commonDir && existsSync(commonDir)) {
|
|
236
248
|
const st = statSync(commonDir);
|
|
237
249
|
if (st.isDirectory()) {
|
|
238
|
-
|
|
250
|
+
root = dirname(commonDir);
|
|
239
251
|
}
|
|
240
252
|
}
|
|
241
253
|
} catch (e) {
|
|
242
254
|
// 静默 fallback:主仓库内执行或 git 异常
|
|
243
255
|
}
|
|
244
|
-
|
|
256
|
+
_mainRepoRootByCwd.set(this.cwd, root);
|
|
257
|
+
return root;
|
|
245
258
|
}
|
|
246
259
|
|
|
247
260
|
/**
|
|
@@ -406,8 +419,12 @@ export class WorktreeManager {
|
|
|
406
419
|
createdAt: new Date().toISOString(),
|
|
407
420
|
provisioning: true,
|
|
408
421
|
};
|
|
409
|
-
|
|
410
|
-
} catch {
|
|
422
|
+
writeMetaAtomic(join(worktreePath, META_FILE), placeholderMeta);
|
|
423
|
+
} catch (e) {
|
|
424
|
+
// 占位 meta 是「防 create 中断 → 幽灵强删」的守卫。写失败不能静默:否则中断后
|
|
425
|
+
// getMeta 仍返回 null,下次 create 会把含 gitignored 改动的 worktree 当幽灵删掉。
|
|
426
|
+
console.warn(`⚠️ 占位 meta 写入失败: ${e.message}(若 create 中断,下次可能误判幽灵 worktree)`);
|
|
427
|
+
}
|
|
411
428
|
|
|
412
429
|
// 5.5 base 同步检测(只读:不 ff、不改 HEAD;best-effort fetch 失败降级用缓存)。
|
|
413
430
|
// 旧逻辑 fetch + merge --ff-only:分叉时 ff 失败被静默吞(syncStatus=failed),成功时
|
|
@@ -460,7 +477,7 @@ export class WorktreeManager {
|
|
|
460
477
|
};
|
|
461
478
|
|
|
462
479
|
const metaPath = join(worktreePath, META_FILE);
|
|
463
|
-
|
|
480
|
+
writeMetaAtomic(metaPath, meta);
|
|
464
481
|
|
|
465
482
|
return { branch, worktreePath, baseHash, mode: meta.mode, syncDiagnostic };
|
|
466
483
|
}
|
|
@@ -491,7 +508,7 @@ export class WorktreeManager {
|
|
|
491
508
|
if (!existsSync(this.worktreeBase)) mkdirSync(this.worktreeBase, { recursive: true })
|
|
492
509
|
const metaDir = join(this.worktreeBase, name)
|
|
493
510
|
if (!existsSync(metaDir)) mkdirSync(metaDir, { recursive: true })
|
|
494
|
-
|
|
511
|
+
writeMetaAtomic(join(metaDir, META_FILE), meta)
|
|
495
512
|
console.log(`🔗 native-worktree meta 已恢复: ${metaDir}/meta.json`)
|
|
496
513
|
return { branch: meta.branch, worktreePath, baseHash, mode: meta.mode }
|
|
497
514
|
}
|
|
@@ -544,7 +561,7 @@ export class WorktreeManager {
|
|
|
544
561
|
if (!existsSync(this.worktreeBase)) mkdirSync(this.worktreeBase, { recursive: true })
|
|
545
562
|
const metaDir = join(this.worktreeBase, name)
|
|
546
563
|
if (!existsSync(metaDir)) mkdirSync(metaDir, { recursive: true })
|
|
547
|
-
|
|
564
|
+
writeMetaAtomic(join(metaDir, META_FILE), meta)
|
|
548
565
|
return { branch: meta.branch, worktreePath, baseHash, mode: meta.mode }
|
|
549
566
|
}
|
|
550
567
|
|
|
@@ -588,7 +605,7 @@ export class WorktreeManager {
|
|
|
588
605
|
if (!existsSync(metaDir)) {
|
|
589
606
|
mkdirSync(metaDir, { recursive: true });
|
|
590
607
|
}
|
|
591
|
-
|
|
608
|
+
writeMetaAtomic(metaPath, meta);
|
|
592
609
|
|
|
593
610
|
return { branch: meta.branch, worktreePath, baseHash, mode: meta.mode };
|
|
594
611
|
}
|
|
@@ -809,7 +826,7 @@ export class WorktreeManager {
|
|
|
809
826
|
const deps = provisionDeps(wtPath, this.cwd, { specBase: join(this.cwd, '.sillyspec') }) || {};
|
|
810
827
|
const metaPath = join(this.getWorktreePath(name), META_FILE);
|
|
811
828
|
const meta = this.getMeta(name) || {};
|
|
812
|
-
|
|
829
|
+
writeMetaAtomic(metaPath, { ...meta, ...deps });
|
|
813
830
|
return { ok: true, msg: `re-provisioned ${name}: depsStatus=${deps.depsStatus}` };
|
|
814
831
|
} catch (e) {
|
|
815
832
|
return { ok: false, msg: `re-provision failed for ${name}: ${e.message}` };
|
|
@@ -1086,7 +1103,7 @@ export class WorktreeManager {
|
|
|
1086
1103
|
const dst = join(worktreePath, f);
|
|
1087
1104
|
if (existsSync(src)) {
|
|
1088
1105
|
mkdirSync(dirname(dst), { recursive: true });
|
|
1089
|
-
try { writeFileSync(dst, readFileSync(src)); files.push(f); } catch {}
|
|
1106
|
+
try { writeFileSync(dst, readFileSync(src)); files.push(f); } catch (e) { errors.push(`untracked ${f}: ${e.message}`); }
|
|
1090
1107
|
}
|
|
1091
1108
|
}
|
|
1092
1109
|
}
|