sillyspec 3.15.0 → 3.15.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.15.0",
3
+ "version": "3.15.2",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
package/src/run.js CHANGED
@@ -10,6 +10,7 @@ import { ProgressManager } from './progress.js'
10
10
  import { stageRegistry, auxiliaryStages } from './stages/index.js'
11
11
  import { buildExecuteSteps } from './stages/execute.js'
12
12
  import { buildPlanSteps } from './stages/plan.js'
13
+ import { formatExecuteSummary } from './worktree-apply.js'
13
14
 
14
15
  /**
15
16
  * 同步触发辅助函数:_write 后 best-effort 同步到平台
@@ -745,7 +746,21 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
745
746
  console.log(`✅ ${stageName} 阶段已完成(${total}/${total} 步)`)
746
747
 
747
748
  if (stageName === 'execute') {
748
- console.log('\n👉 下一步:sillyspec run verify(验证通过后才能归档)')
749
+ // execute run summary:展示真实可得的结构化信息
750
+ try {
751
+ const lastOutput = steps[steps.length - 1]?.output || ''
752
+ const summary = formatExecuteSummary({
753
+ changeName,
754
+ stepsCompleted: total,
755
+ stepsTotal: total,
756
+ agentSummary: lastOutput,
757
+ cwd,
758
+ })
759
+ console.log(`\n${summary}`)
760
+ } catch (e) {
761
+ // summary 失败不影响主流程
762
+ console.log('\n👉 下一步:sillyspec run verify(验证通过后才能归档)')
763
+ }
749
764
  } else if (stageName === 'verify') {
750
765
  console.log('\n👉 下一步:sillyspec run archive(验证通过,可以归档了)')
751
766
  } else if (stageName === 'archive') {
@@ -92,9 +92,17 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
92
92
  // 同时检测 untracked 新文件(git diff 不包含 untracked)
93
93
  let changedFiles;
94
94
  try {
95
- // tracked 文件的变更(modified/deleted)
96
- const trackedRaw = git(worktreePath, `diff --name-only ${diffBase}`);
97
- const trackedFiles = trackedRaw ? trackedRaw.split('\n').filter(Boolean) : [];
95
+ // --name-status 捕获 rename/delete(--name-only 会丢失 rename 源文件)
96
+ const statusRaw = git(worktreePath, `diff --name-status ${diffBase}`);
97
+ const statusFiles = new Set();
98
+ if (statusRaw) {
99
+ for (const line of statusRaw.split('\n').filter(Boolean)) {
100
+ const parts = line.split('\t');
101
+ // R100 old.txt new.txt → 提取两个文件
102
+ if (parts.length >= 2) statusFiles.add(parts[parts.length - 1]);
103
+ if (parts.length >= 3) statusFiles.add(parts[parts.length - 2]);
104
+ }
105
+ }
98
106
 
99
107
  // untracked 新文件(diffBase 中不存在的文件)
100
108
  const untrackedRaw = gitQuiet(worktreePath, `ls-files --others --exclude-standard`);
@@ -102,7 +110,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
102
110
  ? untrackedRaw.split('\n').filter(Boolean).filter(f => !f.startsWith('.sillyspec/') && f !== 'meta.json')
103
111
  : [];
104
112
 
105
- changedFiles = [...new Set([...trackedFiles, ...untrackedFiles])];
113
+ changedFiles = [...new Set([...statusFiles, ...untrackedFiles])];
106
114
  } catch (e) {
107
115
  result.errors.push(`获取变更文件列表失败: ${e.message}`);
108
116
  return result;
@@ -216,8 +224,11 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
216
224
 
217
225
  // 分 tracked 变更和 untracked 新文件生成 patch
218
226
  const trackedFiles = patchFiles.filter(f => {
219
- // untracked 文件在 baseHash 的 tree 中不存在
220
- return gitQuiet(worktreePath, `cat-file -e ${diffBase}:${f}`) !== null;
227
+ // 文件在 diffBase 的 tree 中存在 → tracked(包括 rename 目标可能的情况)
228
+ if (gitQuiet(worktreePath, `cat-file -e ${diffBase}:${f}`) !== null) return true;
229
+ // 文件在工作区 index 中已存在(比如被 git mv 处理过)→ 也视为 tracked
230
+ if (gitQuiet(worktreePath, `ls-files --error-unmatch ${f}`) !== null) return true;
231
+ return false;
221
232
  });
222
233
  const untrackedPatchFiles = patchFiles.filter(f => !trackedFiles.includes(f));
223
234
 
@@ -290,3 +301,91 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
290
301
 
291
302
  return result;
292
303
  }
304
+
305
+ /**
306
+ * 格式化 execute run summary(人类可读)
307
+ *
308
+ * 只展示 CLI 真实掌握的信息,不声称知道 per-task 状态。
309
+ * @param {object} opts
310
+ * @param {string} opts.changeName - 变更名
311
+ * @param {number} opts.stepsCompleted - 已完成步骤数
312
+ * @param {number} opts.stepsTotal - 总步骤数
313
+ * @param {string} opts.agentSummary - Agent 最终输出摘要
314
+ * @param {string} [opts.cwd] - 项目根目录(默认 process.cwd())
315
+ * @returns {string} 格式化的 summary 文本
316
+ */
317
+ export function formatExecuteSummary({ changeName, stepsCompleted, stepsTotal, agentSummary, cwd }) {
318
+ const wm = new WorktreeManager({ cwd });
319
+ const meta = wm.getMeta(changeName);
320
+ const lines = [];
321
+
322
+ const SEPARATOR = '─'.repeat(32);
323
+
324
+ // --- Header ---
325
+ lines.push(`Execute Summary`);
326
+ lines.push(SEPARATOR);
327
+
328
+ // --- Status ---
329
+ if (!meta) {
330
+ // worktree 不存在(可能已 cleanup 或没有用过 worktree)
331
+ lines.push(`Status: COMPLETED`);
332
+ lines.push(`Steps: ${stepsCompleted} / ${stepsTotal}`);
333
+ lines.push(`Apply: N/A`);
334
+ } else {
335
+ const hasBaseline = meta.baselineCommit != null;
336
+ const wtExists = existsSync(meta.worktreePath);
337
+
338
+ const applyStatus = wtExists ? 'pending' : 'applied';
339
+ const baselineCount = meta.baselineFiles?.length || 0;
340
+ const baselineStatus = hasBaseline
341
+ ? `dirty (${baselineCount} baseline file${baselineCount === 1 ? '' : 's'} protected)`
342
+ : 'clean';
343
+
344
+ lines.push(`Status: COMPLETED`);
345
+ lines.push(`Steps: ${stepsCompleted} / ${stepsTotal}`);
346
+ lines.push(`Baseline: ${baselineStatus}`);
347
+ lines.push(`Apply: ${applyStatus}`);
348
+ }
349
+
350
+ // --- Changed files ---
351
+ // 从主工作区 diff 获取(worktree 已 apply)或从 worktree diff 获取
352
+ if (meta && existsSync(meta.worktreePath)) {
353
+ // worktree 还在,用 baselineCommit 或 baseHash 做 diff
354
+ try {
355
+ const diffBase = meta.baselineCommit || meta.baseHash;
356
+ const { execSync: es } = require('child_process');
357
+ const filesRaw = es(`git -C ${meta.worktreePath} diff --name-only ${diffBase} 2>/dev/null`, { encoding: 'utf8' });
358
+ const files = filesRaw ? filesRaw.trim().split('\n').filter(Boolean) : [];
359
+ if (files.length > 0) {
360
+ lines.push(``);
361
+ const maxShow = 10;
362
+ const showFiles = files.slice(0, maxShow);
363
+ const remain = files.length - maxShow;
364
+ lines.push(`Changed Files (${files.length})`);
365
+ showFiles.forEach(f => lines.push(` ${f}`));
366
+ if (remain > 0) {
367
+ lines.push(` ... ${remain} more`);
368
+ }
369
+ }
370
+ } catch {}
371
+ }
372
+
373
+ // --- Agent Summary ---
374
+ if (agentSummary) {
375
+ lines.push(``);
376
+ lines.push(`Agent Summary`);
377
+ // 缩进每行,截断过长内容
378
+ const maxLen = 200;
379
+ const summary = agentSummary.length > maxLen
380
+ ? agentSummary.slice(0, maxLen) + '...'
381
+ : agentSummary;
382
+ summary.split('\n').forEach(l => lines.push(` ${l}`));
383
+ }
384
+
385
+ // --- Next ---
386
+ lines.push(``);
387
+ lines.push(`Next`);
388
+ lines.push(` → sillyspec run verify`);
389
+
390
+ return lines.join('\n');
391
+ }
package/src/worktree.js CHANGED
@@ -302,10 +302,11 @@ export class WorktreeManager {
302
302
  const staged = gitQuiet(mainCwd, 'diff --cached --name-only') || '';
303
303
  if (staged) {
304
304
  try {
305
- const patchContent = execSync(`git diff --cached --binary`, { cwd: mainCwd, encoding: 'utf8', stdio: ['pipe','pipe','pipe'] });
306
- if (patchContent) {
305
+ // Buffer 模式读取,避免二进制 patch UTF-8 解码损坏
306
+ const patchBuf = execSync(`git diff --cached --binary`, { cwd: mainCwd, stdio: ['pipe','pipe','pipe'] });
307
+ if (patchBuf && patchBuf.length > 0) {
307
308
  const patchFile = join(worktreePath, '.sillyspec-baseline-staged.patch');
308
- writeFileSync(patchFile, patchContent);
309
+ writeFileSync(patchFile, patchBuf);
309
310
  git(worktreePath, `apply --binary ${patchFile}`);
310
311
  rmSync(patchFile, { force: true });
311
312
  }
@@ -319,10 +320,11 @@ export class WorktreeManager {
319
320
  const unstaged = gitQuiet(mainCwd, 'diff --name-only') || '';
320
321
  if (unstaged) {
321
322
  try {
322
- const patchContent = execSync(`git diff --binary`, { cwd: mainCwd, encoding: 'utf8', stdio: ['pipe','pipe','pipe'] });
323
- if (patchContent) {
323
+ // Buffer 模式读取,避免二进制 patch UTF-8 解码损坏
324
+ const patchBuf = execSync(`git diff --binary`, { cwd: mainCwd, stdio: ['pipe','pipe','pipe'] });
325
+ if (patchBuf && patchBuf.length > 0) {
324
326
  const patchFile = join(worktreePath, '.sillyspec-baseline-unstaged.patch');
325
- writeFileSync(patchFile, patchContent);
327
+ writeFileSync(patchFile, patchBuf);
326
328
  git(worktreePath, `apply --binary ${patchFile}`);
327
329
  rmSync(patchFile, { force: true });
328
330
  }