sillyspec 3.27.2 → 3.27.5
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 +8 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +2 -1
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-archive/SKILL.md +87 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-auto/SKILL.md +93 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-commit/SKILL.md +67 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-continue/SKILL.md +47 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-execute/SKILL.md +214 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-explore/SKILL.md +102 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-export/SKILL.md +57 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-init/SKILL.md +177 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-plan/SKILL.md +156 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-quick/SKILL.md +116 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-resume/SKILL.md +70 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-scan/SKILL.md +79 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-state/SKILL.md +64 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-status/SKILL.md +54 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-verify/SKILL.md +87 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-workspace/SKILL.md +175 -0
- package/.claude/worktrees/sillyspec-data-model-research/.gitattributes +2 -0
- package/.claude/worktrees/sillyspec-data-model-research/.husky/pre-push +20 -0
- package/.claude/worktrees/sillyspec-data-model-research/README.md +203 -0
- package/.claude/worktrees/sillyspec-data-model-research/SKILL.md +91 -0
- package/.claude/worktrees/sillyspec-data-model-research/bin/sillyspec.js +2 -0
- package/.claude/worktrees/sillyspec-data-model-research/logo.jpg +0 -0
- package/.claude/worktrees/sillyspec-data-model-research/package-lock.json +977 -0
- package/.claude/worktrees/sillyspec-data-model-research/package.json +39 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/favicon.jpg +0 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/index.html +16 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/logo.jpg +0 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-dashboard.html +836 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-overview.html +256 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/package.json +25 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/executor.js +95 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/index.js +679 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/parser.js +526 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/watcher.js +357 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/agent-session-log.js +803 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/change-list.js +290 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/change-risk-profile.js +270 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/check-primitives.js +65 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/classify-change.js +109 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/commit-suggest.js +202 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/config-cat.js +88 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/config-schema.js +328 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/constants.js +97 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/contract-matrix.js +480 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/datetime.js +20 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/db-engine.js +55 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/db.js +381 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/decision-distill.js +517 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/local-agent.js +155 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/sillyhub-mcp.js +245 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/probe.js +246 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/strategy.js +121 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/docs-check.js +825 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/docs-debt.js +393 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/docs-gate.js +107 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/doctor-diagnostics.js +1099 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/endpoint-extractor.js +420 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/foreign-declared.js +78 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/fs-atomic.js +73 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/git-helper.js +150 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/hooks/claude-pre-tool-use.cjs +130 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/hooks/worktree-guard.js +892 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/index.js +2798 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/init.js +626 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/knowledge-match.js +231 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/local-detect.js +128 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/local-register.js +104 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/machine-interface.js +543 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/migrate.js +127 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/module-impact.js +166 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/module-resolve.js +222 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/modules.js +580 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/change-registry.js +336 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/consistency-doctor.js +355 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/shared.js +36 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/stage-machine.js +594 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/step-store.js +265 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress.js +1304 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/quick-recommend.js +115 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/quicklog.js +852 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/review-tier.js +77 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/command.js +1595 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/complete-handlers.js +1552 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/complete.js +1259 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/concurrent-detect.js +156 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/gates.js +1084 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/multi-repo-context.js +208 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/next.js +128 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/prompt.js +843 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/quick-audit.js +186 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/scan-profile.js +269 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/shared.js +1562 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/stage.js +584 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run.js +23 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/scan-diff.js +388 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/scan-postcheck.js +512 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/scan-staleness.js +95 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/setup.js +414 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/client.js +597 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/config.js +62 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/spec-dir-typo.js +50 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/spec-sync.js +358 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-engine.js +171 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-spec.js +550 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract.js +889 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-review.js +597 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/archive.js +199 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm-auto.js +236 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm.js +617 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/cmd-existence.js +145 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/doctor.js +540 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/execute.js +1275 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/explore.js +43 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/index.js +29 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/knowledge.js +514 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan-postcheck.js +1344 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan.js +650 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/quick.js +167 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/scan.js +608 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/status.js +64 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/verify.js +246 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/sync.js +1470 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/task-review.js +1498 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/taskcard.js +210 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/verify-postcheck.js +1718 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/verify-probes.js +277 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/version.js +19 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/workflow.js +600 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/workspace.js +125 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/worktree-apply.js +1446 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/worktree-deps.js +576 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/worktree.js +1715 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/claude-instruction.md +23 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/taskcard-rules.md +28 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/testcase-design.md +12 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/verify-probes.md +32 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/archive-impact.yaml +79 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/scan-docs.yaml +132 -0
- package/eval/README.md +86 -0
- package/eval/eval.config.json +28 -0
- package/eval/lib/adapters/cli.mjs +63 -0
- package/eval/lib/adapters/dryrun.mjs +27 -0
- package/eval/lib/config.mjs +82 -0
- package/eval/lib/db.mjs +76 -0
- package/eval/lib/report.mjs +120 -0
- package/eval/lib/runner.mjs +157 -0
- package/eval/lib/tasks.mjs +73 -0
- package/eval/lib/util.mjs +107 -0
- package/eval/lib/workdir.mjs +30 -0
- package/eval/run.mjs +83 -0
- package/eval/tasks/001-slugify/files/package.json +6 -0
- package/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
- package/eval/tasks/001-slugify/task.json +6 -0
- package/eval/tasks/001-slugify/verify.mjs +28 -0
- package/eval/tasks/002-each-day/files/package.json +6 -0
- package/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
- package/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
- package/eval/tasks/002-each-day/task.json +6 -0
- package/eval/tasks/002-each-day/verify.mjs +31 -0
- package/eval/tasks/003-word-report/files/data/notes.txt +5 -0
- package/eval/tasks/003-word-report/files/package.json +6 -0
- package/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
- package/eval/tasks/003-word-report/task.json +6 -0
- package/eval/tasks/003-word-report/verify.mjs +38 -0
- package/package.json +4 -2
- package/src/agent-session-log.js +45 -5
- package/src/change-risk-profile.js +83 -14
- package/src/config-schema.js +18 -3
- package/src/contract-matrix.js +90 -29
- package/src/datetime.js +20 -0
- package/src/decision-distill.js +517 -0
- package/src/dispatch/backends/local-agent.js +4 -4
- package/src/docs-check.js +227 -0
- package/src/docs-debt.js +187 -24
- package/src/doctor-diagnostics.js +102 -2
- package/src/foreign-declared.js +78 -0
- package/src/index.js +89 -9
- package/src/init.js +9 -7
- package/src/knowledge-match.js +107 -6
- package/src/local-register.js +7 -1
- package/src/module-resolve.js +12 -1
- package/src/modules.js +2 -1
- package/src/plan-adopt-waves.js +126 -0
- package/src/quicklog.js +50 -4
- package/src/run/command.js +122 -43
- package/src/run/complete-handlers.js +48 -19
- package/src/run/complete.js +5 -2
- package/src/run/gates.js +87 -22
- package/src/run/prompt.js +84 -42
- package/src/run/quick-audit.js +11 -0
- package/src/run/shared.js +152 -10
- package/src/run.js +2 -2
- package/src/scan-postcheck.js +30 -17
- package/src/stage-contract-spec.js +2 -1
- package/src/stage-contract.js +68 -4
- package/src/stages/archive.js +53 -12
- package/src/stages/brainstorm-auto.js +7 -0
- package/src/stages/brainstorm.js +11 -0
- package/src/stages/doctor.js +157 -1
- package/src/stages/execute.js +32 -6
- package/src/stages/plan-postcheck.js +76 -17
- package/src/stages/plan.js +1 -0
- package/src/stages/quick.js +6 -1
- package/src/stages/verify.js +31 -14
- package/src/sync.js +41 -3
- package/src/task-review.js +2 -2
- package/src/taskcard-placeholders.js +21 -0
- package/src/taskcard.js +10 -1
- package/src/verify-postcheck.js +315 -13
- package/src/verify-probes.js +46 -4
- package/src/worktree-apply.js +184 -7
- package/src/worktree-deps.js +57 -0
- package/src/worktree.js +201 -58
- package/templates/prompts/taskcard-rules.md +1 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// W6 Step9 共享常量:progress.js facade 与 src/progress/* 子模块共用,避免 facade↔子模块循环引用。
|
|
2
|
+
// 9a: STAGE_ORDER/MAIN_FLOW_ORDER;9c: VALID_STAGES/STAGE_LABELS;9d: SPEC_DIR_NAME/CURRENT_VERSION/emptyStage。
|
|
3
|
+
|
|
4
|
+
export const SPEC_DIR_NAME = '.sillyspec';
|
|
5
|
+
|
|
6
|
+
// 合法阶段名(含辅助阶段 scan/quick/explore)
|
|
7
|
+
export const VALID_STAGES = ['scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore'];
|
|
8
|
+
|
|
9
|
+
// 阶段展示文案
|
|
10
|
+
export const STAGE_LABELS = {
|
|
11
|
+
brainstorm: '🧠 需求探索',
|
|
12
|
+
plan: '📐 实现计划',
|
|
13
|
+
execute: '⚡ 波次执行',
|
|
14
|
+
verify: '🔍 验证确认',
|
|
15
|
+
scan: '🔍 代码扫描',
|
|
16
|
+
quick: '⚡ 快速任务',
|
|
17
|
+
explore: '🧭 自由探索',
|
|
18
|
+
archive: '📦 归档变更',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// 完整阶段顺序(含 scan),仅用于展示/迭代顺序
|
|
22
|
+
export const STAGE_ORDER = ['scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive'];
|
|
23
|
+
// 主流程阶段(不含 scan/quick/explore 等辅助阶段)—— 用于下游 cascade / 一致性上下游判定。
|
|
24
|
+
// scan 是 auxiliary(AUXILIARY_STAGES,按需显式跑、可 failed_post_check、永不要求 completed),
|
|
25
|
+
// 把它算进主流程上下游会让「scan stale/revising」误报 brainstorm/plan/execute 不该 completed
|
|
26
|
+
// (consistency-doctor 坑 + prompt-control-debt plan-c 同根因)。
|
|
27
|
+
export const MAIN_FLOW_ORDER = ['brainstorm', 'plan', 'execute', 'verify', 'archive'];
|
|
28
|
+
|
|
29
|
+
// progress 数据版本(v3 = SQLite;v4 = changes 加 base_ts / 本地脏度两列,platform-sync §8)
|
|
30
|
+
export const CURRENT_VERSION = 5;
|
|
31
|
+
|
|
32
|
+
// 空阶段骨架
|
|
33
|
+
export function emptyStage() {
|
|
34
|
+
return { status: 'pending', steps: [], startedAt: null, completedAt: null };
|
|
35
|
+
}
|
|
36
|
+
|
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
// W6 Step9d: 阶段状态机 —— completeStage/reopen/reset/validate/show/status + 产物校验 + 下游级联 +
|
|
2
|
+
// 显示辅助。从 src/progress.js 单体 ProgressManager 抽出(Step9 最后一步)。持有 pm 引用(构造注入),
|
|
3
|
+
// 调 pm.read/_write/_ensureDB/_getSpecDir/readGlobal/_runtimePath/listChanges/_appendAuditLog/
|
|
4
|
+
// _renderBatchProgress(persistence-core + 其他组 delegate)。组内方法互调保持 this.X(同 class)。
|
|
5
|
+
// completeStage 五层(resolve/validate/force/tx/history/print)整体搬迁,不拆流水线(保行为、最低风险)。
|
|
6
|
+
import { mkdirSync, existsSync } from 'fs';
|
|
7
|
+
import { join, resolve, basename } from 'path';
|
|
8
|
+
import { writeAtomicSync } from '../fs-atomic.js';
|
|
9
|
+
import { runValidators } from '../stage-contract.js';
|
|
10
|
+
import { VALID_STAGES, STAGE_LABELS, STAGE_ORDER, MAIN_FLOW_ORDER, SPEC_DIR_NAME, CURRENT_VERSION, emptyStage } from './shared.js';
|
|
11
|
+
|
|
12
|
+
export class StageMachine {
|
|
13
|
+
constructor(pm) {
|
|
14
|
+
this.pm = pm;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 阶段产物校验门:progress complete-stage / update-step 自动完成时复用
|
|
19
|
+
* stage-contract 的 validator,防止零产物阶段被直接标 completed。
|
|
20
|
+
*/
|
|
21
|
+
_validateStageArtifacts(cwd, stage, changeName) {
|
|
22
|
+
const specDir = this.pm._getSpecDir(cwd);
|
|
23
|
+
const defaultSpec = join(resolve(cwd), SPEC_DIR_NAME);
|
|
24
|
+
const specRoot = resolve(specDir) === defaultSpec ? null : specDir;
|
|
25
|
+
let projectName = null;
|
|
26
|
+
try {
|
|
27
|
+
const g = this.pm.readGlobal(cwd);
|
|
28
|
+
projectName = g?.project || null;
|
|
29
|
+
} catch {}
|
|
30
|
+
return runValidators(stage, cwd, changeName, {
|
|
31
|
+
projectName: projectName || basename(resolve(cwd)),
|
|
32
|
+
specRoot,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
completeStage(cwd, stage, changeName = null, opts = {}) {
|
|
37
|
+
const { force = false } = opts;
|
|
38
|
+
if (!VALID_STAGES.includes(stage)) {
|
|
39
|
+
console.log(`❌ 未知阶段: ${stage}`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const db = this.pm._ensureDB(cwd);
|
|
44
|
+
const now = new Date().toISOString();
|
|
45
|
+
|
|
46
|
+
// 获取变更名
|
|
47
|
+
let cn = changeName;
|
|
48
|
+
if (!cn) {
|
|
49
|
+
const changes = this.pm.listChanges(cwd);
|
|
50
|
+
if (changes.length === 1) cn = changes[0];
|
|
51
|
+
if (!cn) { console.log('❌ 无法确定当前变更,请指定 --change <name>'); return; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── stale 拒绝门(W1 改动点 2):complete-stage 不再静默回填 stale 步骤 ──
|
|
55
|
+
// 放在产物校验门之前——stale 是更具体的拒绝原因(产物缺失时旧序会被产物门先拦走,
|
|
56
|
+
// stale 门永远执行不到);无 --force 拒绝并列名,有 --force 放行并把 stale 步骤名并入审计。
|
|
57
|
+
let staleBackfillNote = null;
|
|
58
|
+
{
|
|
59
|
+
const sqlDbForStaleCheck = db.getDb();
|
|
60
|
+
const changeRowForStale = sqlDbForStaleCheck.prepare('SELECT id FROM changes WHERE name = ?').get(cn);
|
|
61
|
+
if (changeRowForStale !== undefined) {
|
|
62
|
+
const stageRowForStale = sqlDbForStaleCheck.prepare('SELECT id FROM stages WHERE change_id = ? AND stage = ?').get(changeRowForStale.id, stage);
|
|
63
|
+
if (stageRowForStale !== undefined) {
|
|
64
|
+
const staleSteps = sqlDbForStaleCheck.prepare('SELECT name FROM steps WHERE stage_id = ? AND status = ?').all(stageRowForStale.id, 'stale');
|
|
65
|
+
if (staleSteps.length > 0) {
|
|
66
|
+
if (!force) {
|
|
67
|
+
console.error(`❌ complete-stage 被拒绝:阶段 ${stage} 存在 ${staleSteps.length} 个 stale 步骤`);
|
|
68
|
+
for (const st of staleSteps) console.error(` - ${st.name}`);
|
|
69
|
+
console.error(` stale 步骤需真实执行推进(sillyspec run ${stage} 逐个完成),或确认方案未变后用 --force 强制回填(将记录审计日志)。`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
staleBackfillNote = `stale 步骤回填: ${staleSteps.map(s => s.name).join(', ')}`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── 产物校验门:complete-stage 不再是零校验后门 ──
|
|
79
|
+
// 与 run.js completeStep 的 validator 同源;--force 为显式逃生口(留审计)。
|
|
80
|
+
const validation = this._validateStageArtifacts(cwd, stage, cn);
|
|
81
|
+
if (staleBackfillNote) {
|
|
82
|
+
validation.errors = [...(validation.errors || []), staleBackfillNote];
|
|
83
|
+
validation.ok = false;
|
|
84
|
+
}
|
|
85
|
+
if (!validation.ok) {
|
|
86
|
+
if (!force) {
|
|
87
|
+
console.error(`❌ complete-stage 被拒绝:阶段 ${stage} 产物校验未通过`);
|
|
88
|
+
for (const err of validation.errors) console.error(` - ${err}`);
|
|
89
|
+
console.error(` 请修复产物后重试,或走正常流程 sillyspec run ${stage} --done。`);
|
|
90
|
+
console.error(` 确需强制标记(如 doctor 修复),使用 --force(将记录审计日志)。`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
console.warn(`⚠️ --force 强制完成阶段 ${stage}(校验未通过,已记录审计日志)`);
|
|
94
|
+
for (const err of validation.errors) console.warn(` - ${err}`);
|
|
95
|
+
this.pm._appendAuditLog(cwd, {
|
|
96
|
+
action: 'complete-stage --force',
|
|
97
|
+
stage,
|
|
98
|
+
change: cn,
|
|
99
|
+
validationErrors: validation.errors,
|
|
100
|
+
});
|
|
101
|
+
} else if (force) {
|
|
102
|
+
// 校验通过但仍显式 --force:也留一条审计记录,保持行为可追溯
|
|
103
|
+
this.pm._appendAuditLog(cwd, { action: 'complete-stage --force', stage, change: cn, validationErrors: [] });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
db.transaction(() => {
|
|
107
|
+
const sqlDb = db.getDb();
|
|
108
|
+
const changeRow = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(cn);
|
|
109
|
+
if (changeRow === undefined) return;
|
|
110
|
+
const changeId = changeRow.id;
|
|
111
|
+
|
|
112
|
+
// 确保 stages 行存在(阶段不存在时自动创建)
|
|
113
|
+
sqlDb.prepare(`INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, 'pending')`).run(changeId, stage);
|
|
114
|
+
|
|
115
|
+
// UPDATE stages.status=completed + completed_at
|
|
116
|
+
sqlDb.prepare(`UPDATE stages SET status = 'completed', completed_at = ? WHERE change_id = ? AND stage = ?`).run(now, changeId, stage);
|
|
117
|
+
|
|
118
|
+
// 将该阶段所有 pending 步骤标记为 completed
|
|
119
|
+
const stageRow = sqlDb.prepare('SELECT id FROM stages WHERE change_id = ? AND stage = ?').get(changeId, stage);
|
|
120
|
+
if (stageRow !== undefined) {
|
|
121
|
+
const stageId = stageRow.id;
|
|
122
|
+
sqlDb.prepare(
|
|
123
|
+
`UPDATE steps SET status = 'completed', completed_at = ? WHERE stage_id = ? AND status IN ('pending', 'stale')` // stale 一并回填(reopen --done 场景,坑 brainstorm-reopen-step-state-desync;--force 时放行)
|
|
124
|
+
).run(now, stageId);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// UPDATE changes.last_active
|
|
128
|
+
sqlDb.prepare('UPDATE changes SET last_active = ? WHERE name = ?').run(now, cn);
|
|
129
|
+
// 本地脏度(D-013 / task-04):阶段完成是本地推进
|
|
130
|
+
this.pm._touchLocalModified(cwd, cn, now);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// 写 history 文件(保持文件系统,不变)
|
|
134
|
+
const data = this.pm.read(cwd, cn);
|
|
135
|
+
if (data && data.stages && data.stages[stage]) {
|
|
136
|
+
const historyDir = this.pm._runtimePath(cwd, 'history');
|
|
137
|
+
mkdirSync(historyDir, { recursive: true });
|
|
138
|
+
const ts = now.replace(/[:.TZ-]/g, '');
|
|
139
|
+
const stageData = data.stages[stage];
|
|
140
|
+
writeAtomicSync(
|
|
141
|
+
join(historyDir, `${cn}-${stage}-${ts}.json`),
|
|
142
|
+
JSON.stringify({ change: cn, stage, data: stageData, completedAt: now }, null, 2) + '\n'
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(`✅ 阶段 ${stage} 已标记为完成(不自动推进,下一步由你决定)`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
show(cwd, changeName = null) {
|
|
150
|
+
// 如果指定了变更名,只显示该变更
|
|
151
|
+
if (changeName) {
|
|
152
|
+
return this._showChange(cwd, changeName);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 否则显示所有变更
|
|
156
|
+
const changes = this.pm.listChanges(cwd);
|
|
157
|
+
if (changes.length === 0) {
|
|
158
|
+
console.log('ℹ️ 没有活跃的变更');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (changes.length === 1) {
|
|
163
|
+
return this._showChange(cwd, changes[0]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 多个变更:汇总显示
|
|
167
|
+
const global = this.pm.readGlobal(cwd);
|
|
168
|
+
console.log('');
|
|
169
|
+
console.log(' ═══════════════════════════════════════');
|
|
170
|
+
console.log(` 项目: ${(global?.project) || basename(cwd) || '(未命名)'}`);
|
|
171
|
+
// 当前操作目标与活跃列表区分(坑 status-change-pointer-ambiguous):多活跃时不带
|
|
172
|
+
// --change 的 run/progress 不隐式选任何一个(index.js --change 解析多活跃报错),明确
|
|
173
|
+
// 说出来,防止把残留空壳(default/quick-xxx/目录缺失记录)误当操作目标跑错 change。
|
|
174
|
+
console.log(` 当前操作目标: (多活跃——不带 --change 时不隐式选定,run 须显式 --change <name>)`);
|
|
175
|
+
console.log(` 活跃变更记录: ${changes.length} 个(下列为 DB 中存在的活跃记录,非操作目标)`);
|
|
176
|
+
console.log(' ═══════════════════════════════════════');
|
|
177
|
+
console.log('');
|
|
178
|
+
|
|
179
|
+
const changesRoot = join(this.pm._getSpecDir(cwd), 'changes');
|
|
180
|
+
for (const cn of changes) {
|
|
181
|
+
const data = this.pm.read(cwd, cn);
|
|
182
|
+
const dirMissing = !existsSync(join(changesRoot, cn));
|
|
183
|
+
if (!data) {
|
|
184
|
+
console.log(` 📂 ${cn} — (无法读取)${dirMissing ? ' ⚠️ 目录缺失(残留记录,sillyspec doctor --cleanup-ghosts --confirm 可归档清理)' : ''}`);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const currentStage = data.currentStage || '(无)';
|
|
188
|
+
const stageLabel = STAGE_LABELS[data.currentStage] || currentStage;
|
|
189
|
+
const lastActive = data.lastActive ? this._timeAgo(data.lastActive) : '未知';
|
|
190
|
+
|
|
191
|
+
console.log(` 📂 ${cn}${dirMissing ? ' ⚠️ 目录缺失(残留记录,sillyspec doctor --cleanup-ghosts --confirm 可归档清理)' : ''}`);
|
|
192
|
+
console.log(` 当前阶段: ${stageLabel} 最近活跃: ${lastActive}`);
|
|
193
|
+
console.log('');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log(` 💡 查看详情:sillyspec progress show --change <name>`);
|
|
197
|
+
console.log('');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
_showChange(cwd, changeName) {
|
|
201
|
+
const data = this.pm.read(cwd, changeName);
|
|
202
|
+
if (!data) {
|
|
203
|
+
console.log(`❌ 未找到变更 ${changeName}`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
console.log('');
|
|
208
|
+
console.log(' ═══════════════════════════════════════');
|
|
209
|
+
console.log(` 变更: ${changeName}`);
|
|
210
|
+
console.log(` 项目: ${data.project || '(未命名)'}`);
|
|
211
|
+
console.log(` 当前阶段: ${STAGE_LABELS[data.currentStage] || data.currentStage || '(无)'}`);
|
|
212
|
+
console.log(` 最近活跃: ${data.lastActive ? this._timeAgo(data.lastActive) : '未知'}`);
|
|
213
|
+
console.log(' ═══════════════════════════════════════');
|
|
214
|
+
console.log('');
|
|
215
|
+
|
|
216
|
+
const statusIcons = { pending: '⬜', 'in-progress': '🔵', completed: '✅', failed: '❌', blocked: '🚫', waiting: '⏸️', revising: '🔧', stale: '⚠️' };
|
|
217
|
+
|
|
218
|
+
for (const stage of VALID_STAGES) {
|
|
219
|
+
const stageData = data.stages[stage] || emptyStage();
|
|
220
|
+
const label = STAGE_LABELS[stage] || stage;
|
|
221
|
+
const icon = statusIcons[stageData.status] || '⬜';
|
|
222
|
+
const isCurrent = data.currentStage === stage ? ' ◀' : '';
|
|
223
|
+
|
|
224
|
+
console.log(` ${icon} ${label}${isCurrent}`);
|
|
225
|
+
|
|
226
|
+
// Show revision info
|
|
227
|
+
if (stageData.revision && stageData.revision > 0) {
|
|
228
|
+
console.log(` 📋 revision: ${stageData.revision}${stageData.reopenedFromStep ? `, from step: ${stageData.reopenedFromStep}` : ''}`);
|
|
229
|
+
}
|
|
230
|
+
if (stageData.staleReason) {
|
|
231
|
+
console.log(` ⚠️ stale: ${stageData.staleReason}`);
|
|
232
|
+
if (stage === 'archive') {
|
|
233
|
+
console.log(` 📁 已有归档文件仍保留在磁盘上,但不再可信。`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (stageData.steps && stageData.steps.length > 0) {
|
|
238
|
+
for (const step of stageData.steps) {
|
|
239
|
+
const si = statusIcons[step.status] || '○';
|
|
240
|
+
const out = step.output ? ` — ${step.output.slice(0, 60)}` : '';
|
|
241
|
+
const waitingTag = step.status === 'waiting' ? ' [WAITING]' : ''
|
|
242
|
+
console.log(` ${si} ${step.name}${out}${waitingTag}`);
|
|
243
|
+
if (step.status === 'waiting') {
|
|
244
|
+
if (step.waitReason) console.log(` 原因:${step.waitReason}`);
|
|
245
|
+
if (step.waitOptions) console.log(` 选项:${(() => { try { const p = JSON.parse(step.waitOptions); return Array.isArray(p) ? p.join(', ') : step.waitOptions; } catch { return step.waitOptions; }})()}`);
|
|
246
|
+
if (step.waitedAt) console.log(` 等待时间:${step.waitedAt}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (stageData.startedAt) {
|
|
252
|
+
console.log(` 开始: ${new Date(stageData.startedAt).toLocaleString('zh-CN')}`);
|
|
253
|
+
}
|
|
254
|
+
if (stageData.completedAt) {
|
|
255
|
+
console.log(` 完成: ${new Date(stageData.completedAt).toLocaleString('zh-CN')}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 批量进度
|
|
260
|
+
if (data.batchProgress) {
|
|
261
|
+
const batchLine = this.pm._renderBatchProgress(data.batchProgress);
|
|
262
|
+
if (batchLine) {
|
|
263
|
+
console.log('');
|
|
264
|
+
console.log(` ${batchLine}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── Next 建议 ──
|
|
269
|
+
const suggestion = this._getNextSuggestion(data);
|
|
270
|
+
if (suggestion) {
|
|
271
|
+
console.log('');
|
|
272
|
+
console.log(` 💡 ${suggestion.text}`);
|
|
273
|
+
if (suggestion.command) console.log(` ${suggestion.command}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
console.log('');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 根据当前状态给出下一步建议
|
|
281
|
+
* @param {object} data - progress data
|
|
282
|
+
* @returns {{ text: string, command?: string }|null}
|
|
283
|
+
*/
|
|
284
|
+
_getNextSuggestion(data) {
|
|
285
|
+
// 建议命令携带 --change(坑 suggestion-command-missing-change,2026-08-21 实证):
|
|
286
|
+
// 多活跃变更仓 pm.read(cwd, null) 无法自动定位变更 → 照抄建议命令(不带 --change)执行
|
|
287
|
+
// 报「未找到进度数据」,阶段完结后的 --wait 确认门也登记不上。progress 带 currentChange
|
|
288
|
+
// 时附加;无(测试 fixture / 旧数据)保持裸命令(存量断言零回归)。
|
|
289
|
+
const cc = data.currentChange ? ` --change ${data.currentChange}` : '';
|
|
290
|
+
// 找到第一个 revising 阶段
|
|
291
|
+
const revisingStage = STAGE_ORDER.find(s => data.stages[s]?.status === 'revising');
|
|
292
|
+
if (revisingStage) {
|
|
293
|
+
const sd = data.stages[revisingStage];
|
|
294
|
+
return {
|
|
295
|
+
text: `${STAGE_LABELS[revisingStage] || revisingStage} 正在修订中(revision ${sd.revision || 1}),请继续完成修订。`,
|
|
296
|
+
command: `sillyspec run ${revisingStage}${cc}`,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 找到第一个 stale 阶段(上游已修,下游需要重建)
|
|
301
|
+
const staleStage = STAGE_ORDER.find(s => data.stages[s]?.status === 'stale');
|
|
302
|
+
if (staleStage) {
|
|
303
|
+
const sd = data.stages[staleStage];
|
|
304
|
+
return {
|
|
305
|
+
text: `${STAGE_LABELS[staleStage] || staleStage} 已失效(${sd.staleReason || '上游修订'}),需要从第一步重建。`,
|
|
306
|
+
command: `sillyspec run ${staleStage} --reopen --from-step 1${cc}`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 找到第一个有 pending/waiting/failed 步骤的 in-progress 阶段
|
|
311
|
+
// C14b:排除 scan(同下方主流程推荐循环)——scan 是 auxiliary,中途未完成时恒处
|
|
312
|
+
// STAGE_ORDER 首位且 in-progress,会把「下一步」劫持为 scan,掩盖主流程真实待办。
|
|
313
|
+
for (const s of STAGE_ORDER) {
|
|
314
|
+
if (s === 'scan') continue
|
|
315
|
+
const sd = data.stages[s];
|
|
316
|
+
if (!sd) continue;
|
|
317
|
+
if (sd.status === 'in-progress' && sd.steps) {
|
|
318
|
+
// waiting 优先于 pending/failed:暂停等用户决策的阶段,下一步是 --continue --answer
|
|
319
|
+
// 恢复(坑 archive-step3-wait-answer-hint-late:建议命令前置 --answer,而非泛泛的
|
|
320
|
+
// 「继续执行」让 agent 撞 --done 报错才知道要 --answer)
|
|
321
|
+
const waitIdx = sd.steps.findIndex(st => st.status === 'waiting');
|
|
322
|
+
if (waitIdx !== -1) {
|
|
323
|
+
const ws = sd.steps[waitIdx];
|
|
324
|
+
return {
|
|
325
|
+
text: `${STAGE_LABELS[s] || s} 进行中,Step ${waitIdx + 1}「${ws.name}」等待用户输入${ws.waitReason ? `(${ws.waitReason})` : ''},用 --answer 恢复。`,
|
|
326
|
+
command: `sillyspec run ${s} --continue --answer "用户回答"${cc}`,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const hasPending = sd.steps.some(st => ['pending', 'waiting', 'failed'].includes(st.status));
|
|
330
|
+
if (hasPending) {
|
|
331
|
+
return {
|
|
332
|
+
text: `${STAGE_LABELS[s] || s} 进行中,继续执行下一步。`,
|
|
333
|
+
command: `sillyspec run ${s}${cc}`,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// 找主流程里第一个「未完成且上游已就绪」的阶段,推荐它作为下一步。
|
|
340
|
+
// 修两个缺陷(曾导致 brainstorm 完成后误推 archive):
|
|
341
|
+
// ① 原 steps.length>0 要求:plan/execute/verify 首次 run 前惰性未初始化(steps 空)被跳过;
|
|
342
|
+
// ② 原 upstream 把 pending 当就绪:archive 的上游 plan/execute/verify 即便 pending 也算 ok。
|
|
343
|
+
// 叠加 → 循环跳过未初始化的中间阶段,漏到 archive(steps 非空)误推,违反流程顺序。
|
|
344
|
+
// 改为:不要求 steps(空=该开始了);upstream 必须全 completed(pending 不算就绪)——
|
|
345
|
+
// archive 只在 scan/brainstorm/plan/execute/verify 全完成后才可能被推荐。
|
|
346
|
+
for (const s of STAGE_ORDER) {
|
|
347
|
+
if (s === 'scan') continue // scan 是 auxiliary(按需显式跑),不作"下一步"推荐——否则未 completed 时恒处 STAGE_ORDER 首位且 upstream 空→恒就绪→误推 scan(回头路,与 complete.js brainstorm/quick 专属分支同类根因)。prompt-control-debt plan-c。
|
|
348
|
+
const sd = data.stages[s];
|
|
349
|
+
if (!sd) continue;
|
|
350
|
+
if (sd.status === 'completed' || sd.status === 'skipped') continue;
|
|
351
|
+
const idx = STAGE_ORDER.indexOf(s);
|
|
352
|
+
const upstream = STAGE_ORDER.slice(0, idx).filter(us => us !== 'scan'); // scan(auxiliary)不算上游——否则 scan 未 completed 会阻塞所有主流程阶段,与上面跳过 scan 的迭代矛盾
|
|
353
|
+
const upstreamOk = upstream.every(us => data.stages[us]?.status === 'completed');
|
|
354
|
+
if (upstreamOk) {
|
|
355
|
+
return {
|
|
356
|
+
text: `可以开始 ${STAGE_LABELS[s] || s}。`,
|
|
357
|
+
command: `sillyspec run ${s}${cc}`,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
status(cwd, changeName = null) {
|
|
366
|
+
this.show(cwd, changeName);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
validate(cwd, changeName = null) {
|
|
370
|
+
const data = this.pm.read(cwd, changeName);
|
|
371
|
+
if (!data) { console.log('❌ 无法读取进度数据'); return false; }
|
|
372
|
+
|
|
373
|
+
const errors = [];
|
|
374
|
+
if (!data._version || !Number.isInteger(data._version) || data._version < 1) {
|
|
375
|
+
errors.push(`_version 缺失或无效(期望正整数,实际为 ${JSON.stringify(data._version)})`);
|
|
376
|
+
}
|
|
377
|
+
if (!data.stages || typeof data.stages !== 'object') errors.push('缺少 stages');
|
|
378
|
+
if (!VALID_STAGES.every(s => data.stages[s])) errors.push('缺少阶段定义');
|
|
379
|
+
|
|
380
|
+
if (errors.length === 0) { console.log('✅ 进度数据格式正确'); return true; }
|
|
381
|
+
|
|
382
|
+
console.log(`⚠️ 发现问题,尝试修复...`);
|
|
383
|
+
let fixed = { ...data, stages: { ...data.stages } };
|
|
384
|
+
let changed = false;
|
|
385
|
+
if (!fixed.project) {
|
|
386
|
+
fixed.project = basename(cwd);
|
|
387
|
+
changed = true;
|
|
388
|
+
}
|
|
389
|
+
if (!fixed._version || !Number.isInteger(fixed._version) || fixed._version < 1) {
|
|
390
|
+
fixed._version = CURRENT_VERSION;
|
|
391
|
+
changed = true;
|
|
392
|
+
}
|
|
393
|
+
for (const s of VALID_STAGES) {
|
|
394
|
+
if (!fixed.stages[s]) { fixed.stages[s] = emptyStage(); changed = true; }
|
|
395
|
+
}
|
|
396
|
+
if (changed) {
|
|
397
|
+
this.pm._write(cwd, fixed);
|
|
398
|
+
console.log('✅ 已修复');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* 重新打开已完成的阶段进入修订模式
|
|
406
|
+
* - 不带 fromStep:只允许存在 pending/stale/waiting/failed 步骤时继续
|
|
407
|
+
* - 带 fromStep:从该步骤起,当前及后续步骤标记 stale/pending
|
|
408
|
+
* - 自动级联标记下游阶段为 stale
|
|
409
|
+
*
|
|
410
|
+
* @param {string} cwd
|
|
411
|
+
* @param {string} stage - 要重开的阶段
|
|
412
|
+
* @param {object} opts
|
|
413
|
+
* @param {string|number} [opts.fromStep] - 步骤名或序号(1-based)
|
|
414
|
+
* @param {string} [opts.changeName]
|
|
415
|
+
* @returns {{ ok: boolean, error?: string }}
|
|
416
|
+
*/
|
|
417
|
+
reopenStage(cwd, stage, opts = {}) {
|
|
418
|
+
const { fromStep, changeName = null } = opts;
|
|
419
|
+
|
|
420
|
+
const data = this.pm.read(cwd, changeName);
|
|
421
|
+
if (!data) return { ok: false, error: '无法读取进度数据' };
|
|
422
|
+
|
|
423
|
+
const stageData = data.stages[stage];
|
|
424
|
+
if (!stageData) return { ok: false, error: `未知阶段: ${stage}` };
|
|
425
|
+
|
|
426
|
+
const steps = stageData.steps || [];
|
|
427
|
+
|
|
428
|
+
// 确定 fromStep 对应的 index
|
|
429
|
+
let fromIdx = null;
|
|
430
|
+
if (fromStep != null) {
|
|
431
|
+
if (typeof fromStep === 'number' || /^\d+$/.test(String(fromStep))) {
|
|
432
|
+
fromIdx = parseInt(String(fromStep), 10) - 1; // 1-based → 0-based
|
|
433
|
+
if (fromIdx < 0 || fromIdx >= steps.length) {
|
|
434
|
+
return { ok: false, error: `步骤序号超出范围: ${fromStep}(共 ${steps.length} 步)` };
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
// 按名称匹配
|
|
438
|
+
fromIdx = steps.findIndex(s => s.name === fromStep);
|
|
439
|
+
if (fromIdx === -1) {
|
|
440
|
+
return { ok: false, error: `步骤不存在: ${fromStep}` };
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// 如果不带 fromStep,检查是否存在中断步骤
|
|
446
|
+
if (fromIdx === null) {
|
|
447
|
+
const hasInterrupted = steps.some(s =>
|
|
448
|
+
['pending', 'stale', 'waiting', 'failed'].includes(s.status)
|
|
449
|
+
);
|
|
450
|
+
if (!hasInterrupted) {
|
|
451
|
+
return { ok: false, error: `阶段 ${stage} 所有步骤均已完成,请使用 --from-step 指定从哪一步开始修订` };
|
|
452
|
+
}
|
|
453
|
+
// 找到第一个中断步骤
|
|
454
|
+
fromIdx = steps.findIndex(s =>
|
|
455
|
+
['pending', 'stale', 'waiting', 'failed'].includes(s.status)
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// 执行重开操作
|
|
460
|
+
const newRevision = (stageData.revision || 0) + 1;
|
|
461
|
+
const fromStepName = steps[fromIdx].name;
|
|
462
|
+
const now = new Date().toLocaleString('zh-CN', { hour12: false });
|
|
463
|
+
|
|
464
|
+
// 更新步骤状态:fromStep 之前的保持 completed,fromStep 变 pending,之后的变 stale
|
|
465
|
+
for (let i = 0; i < steps.length; i++) {
|
|
466
|
+
if (i === fromIdx) {
|
|
467
|
+
steps[i].status = 'pending';
|
|
468
|
+
steps[i].completedAt = null;
|
|
469
|
+
steps[i].output = null;
|
|
470
|
+
} else if (i > fromIdx) {
|
|
471
|
+
steps[i].status = 'stale';
|
|
472
|
+
steps[i].completedAt = null;
|
|
473
|
+
}
|
|
474
|
+
// i < fromIdx: 保持原状(completed)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
stageData.status = 'revising';
|
|
478
|
+
stageData.completedAt = null;
|
|
479
|
+
stageData.revision = newRevision;
|
|
480
|
+
stageData.reopenedFromStep = `${fromIdx + 1}: ${fromStepName}`; // 存 "index: name" 格式
|
|
481
|
+
stageData.reopenedAt = now;
|
|
482
|
+
stageData.steps = steps;
|
|
483
|
+
|
|
484
|
+
data.lastActive = now;
|
|
485
|
+
data.currentStage = stage;
|
|
486
|
+
|
|
487
|
+
this.pm._write(cwd, data, changeName);
|
|
488
|
+
|
|
489
|
+
// 级联标记下游阶段为 stale
|
|
490
|
+
const downstreamStages = this._getDownstreamStages(stage);
|
|
491
|
+
if (downstreamStages.length > 0) {
|
|
492
|
+
const data2 = this.pm.read(cwd, changeName); // 重新读取以获取最新状态
|
|
493
|
+
if (data2) {
|
|
494
|
+
for (const ds of downstreamStages) {
|
|
495
|
+
if (data2.stages[ds] && data2.stages[ds].status === 'completed') {
|
|
496
|
+
data2.stages[ds].status = 'stale';
|
|
497
|
+
data2.stages[ds].staleReason = `上游阶段 ${stage} 已修订 (revision ${newRevision})`;
|
|
498
|
+
data2.stages[ds].completedAt = null;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
this.pm._write(cwd, data2, changeName);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return { ok: true, revision: newRevision, fromStep: fromStepName };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* 获取指定阶段的下游主流程阶段列表
|
|
510
|
+
* @param {string} stage
|
|
511
|
+
* @returns {string[]}
|
|
512
|
+
*/
|
|
513
|
+
_getDownstreamStages(stage) {
|
|
514
|
+
const idx = MAIN_FLOW_ORDER.indexOf(stage);
|
|
515
|
+
if (idx === -1) return [];
|
|
516
|
+
return MAIN_FLOW_ORDER.slice(idx + 1);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
reset(cwd, stage, changeName = null) {
|
|
520
|
+
if (stage) {
|
|
521
|
+
const data = this.pm.read(cwd, changeName);
|
|
522
|
+
if (!data) { console.log('❌ 无法读取进度数据'); return; }
|
|
523
|
+
if (!data.stages[stage]) { console.log(`❌ 未知阶段: ${stage}`); return; }
|
|
524
|
+
// 破坏性预览:reset 不可逆地清空该阶段所有步骤(含已完成),先告诉 agent 丢了什么。
|
|
525
|
+
const sd = data.stages[stage];
|
|
526
|
+
const steps = sd.steps || [];
|
|
527
|
+
const doneCount = steps.filter(s => s.status === 'completed').length;
|
|
528
|
+
console.warn(`⚠️ 即将重置阶段「${stage}」:丢弃 ${steps.length} 个步骤(其中 ${doneCount} 个已完成,revision ${sd.revision || 0})。此操作不可逆。`);
|
|
529
|
+
data.stages[stage] = emptyStage();
|
|
530
|
+
data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
|
|
531
|
+
this.pm._write(cwd, data);
|
|
532
|
+
console.log(`✅ 已重置阶段: ${stage}`);
|
|
533
|
+
} else {
|
|
534
|
+
// 重置所有变更或指定变更
|
|
535
|
+
if (changeName) {
|
|
536
|
+
console.warn(`⚠️ 即将重置变更「${changeName}」的全部进度(所有阶段的 steps + stage 状态,产物文件不动)。此操作不可逆。`);
|
|
537
|
+
// SQL: 删除该变更的所有 stages 和 steps 数据
|
|
538
|
+
const db = this.pm._ensureDB(cwd);
|
|
539
|
+
db.transaction(() => {
|
|
540
|
+
const sqlDb = db.getDb();
|
|
541
|
+
const changeRow = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(changeName);
|
|
542
|
+
if (changeRow !== undefined) {
|
|
543
|
+
const changeId = changeRow.id;
|
|
544
|
+
sqlDb.prepare('DELETE FROM steps WHERE stage_id IN (SELECT id FROM stages WHERE change_id = ?)').run(changeId);
|
|
545
|
+
sqlDb.prepare('DELETE FROM stages WHERE change_id = ?').run(changeId);
|
|
546
|
+
// 重新插入所有阶段(注意:上方已 DELETE stages,无需再 UPDATE)
|
|
547
|
+
for (const s of VALID_STAGES) {
|
|
548
|
+
sqlDb.prepare(`INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, 'pending')`).run(changeId, s);
|
|
549
|
+
}
|
|
550
|
+
// 本地脏度(D-013 / task-04):重置该变更全部进度是本地推进
|
|
551
|
+
this.pm._touchLocalModified(cwd, changeName);
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
console.log(`✅ 已重置变更 ${changeName} 的进度`);
|
|
555
|
+
} else {
|
|
556
|
+
const changes = this.pm.listChanges(cwd);
|
|
557
|
+
console.warn(`⚠️ 即将重置【所有 ${changes.length} 个变更】的进度(仅 stage 状态,产物文件不动)。此操作不可逆且范围最大。`);
|
|
558
|
+
const db = this.pm._ensureDB(cwd);
|
|
559
|
+
db.transaction(() => {
|
|
560
|
+
const sqlDb = db.getDb();
|
|
561
|
+
for (const cn of changes) {
|
|
562
|
+
const changeRow = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(cn);
|
|
563
|
+
if (changeRow !== undefined) {
|
|
564
|
+
const changeId = changeRow.id;
|
|
565
|
+
sqlDb.prepare('DELETE FROM steps WHERE stage_id IN (SELECT id FROM stages WHERE change_id = ?)').run(changeId);
|
|
566
|
+
sqlDb.prepare(`UPDATE stages SET status = 'pending', started_at = NULL, completed_at = NULL WHERE change_id = ?`).run(changeId);
|
|
567
|
+
// 本地脏度(D-013 / task-04):重置进度是本地推进
|
|
568
|
+
this.pm._touchLocalModified(cwd, cn);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
console.log('✅ 已重置所有变更的进度');
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
_timeAgo(dateStr) {
|
|
578
|
+
if (!dateStr) return '未知';
|
|
579
|
+
let ts = Date.parse(dateStr);
|
|
580
|
+
if (isNaN(ts)) {
|
|
581
|
+
const m = dateStr.match(/(\d{4})[\/-](\d{1,2})[\/-](\d{1,2})[\s,]+(\d{1,2}):(\d{2})(?::(\d{2}))?/);
|
|
582
|
+
if (m) ts = new Date(+m[1], +m[2]-1, +m[3], +m[4], +m[5], +(m[6]||0)).getTime();
|
|
583
|
+
}
|
|
584
|
+
if (isNaN(ts)) return dateStr;
|
|
585
|
+
const diff = Date.now() - ts;
|
|
586
|
+
const minutes = Math.floor(diff / 60000);
|
|
587
|
+
if (minutes < 1) return '刚刚';
|
|
588
|
+
if (minutes < 60) return `${minutes} 分钟前`;
|
|
589
|
+
const hours = Math.floor(minutes / 60);
|
|
590
|
+
if (hours < 24) return `${hours} 小时前`;
|
|
591
|
+
return `${Math.floor(hours / 24)} 天前`;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|