Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The format loosely follows Keep a Changelog.

### Fixed

- **Active review projection repair (#106)**: add an opt-in, fail-closed `execution review --repair-active-projection` path that rebuilds only the current root PASS projection from matching scoped evidence, preserves immutable revision state, and makes `execution show` prefer valid root evidence with a safe scoped fallback.
- **Evidence-backed DP-5 escalation (#102)**: replace raw `state set dp_5_*` writes with a guarded debugging-attempt ledger. `ssf debug attempt record/show` keeps failed fixes distinct from Wave Review repair failures, rejects duplicate or stale evidence, and `ssf debug escalate --confirm` requires at least three current attempts. Audit reports unsupported legacy DP-5 records instead of treating them as valid approvals.
- **Bounded automatic repair retries (#105)**: stop formal review repair after three unresolved failures instead of five. Every implementer retry must carry new failure evidence, an updated focused brief, and a changed strategy; the third unresolved failure returns control for user adjudication rather than dispatching another subagent.

Expand Down
14 changes: 14 additions & 0 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,10 @@ ssf execution revise changes/my-change --mode sdd --confirm --reason "need paral
--wave integration:serial:2.1:foundation
ssf execution review changes/my-change --wave foundation --base <sha> --head <sha> \
--report .superpowers/sdd/reviews/foundation.md --verdict pass
# 只修復 current root active projection;必須已有相符的 current scoped PASS snapshot。
ssf execution review changes/my-change --wave foundation --base <sha> --head <sha> \
--report .superpowers/sdd/reviews/foundation.md --verdict pass \
--repair-active-projection --json
```

`--report` 相对于 `<change>` 解析,且必须位于
Expand All @@ -860,6 +864,16 @@ report 本身必须为普通、非空、非符号链接文件。
每一个 wave 均须有当前 `pass` review receipt,才可启动依赖 wave 或进入 closing;
修订计划会废止旧 receipt。恢复、切换和手动保存属于 control-plane overlay,不增加第九个状态。

Repair 旗標僅接受 `pass`。root
`<change>/.superpowers/sdd/reviews/<safe-wave>.json` 是 current active projection;
`plans/<identity>/reviews/...` 與 `repair-state` 是 immutable revision evidence。只有 current
plan、known wave、相符的 current scoped PASS snapshot、Git range 與 current report evidence
全部通過驗證後,repair 才會建立或更新 root receipt。相同 evidence 會回傳 no-op,保留檔案
bytes、mtime 與 `recorded_at`。無效 FAIL、snapshot、report 或 range 會在不寫入的情況下拒絕,
且不會修改 scoped review、repair-state 或 workspace。`ssf execution show` 優先使用有效 root;
root 缺失或為無效 PASS 時才回退有效 scoped receipt,無效 FAIL 仍是 blocker。未帶 repair
旗標的 `execution review` 行為不變。

Delta spec 的规范路径是 `specs/<capability>/spec.md`。扁平的 `specs/<capability>.md` 和根级 `specs/spec.md` 都不会被当作合法规范静默通过。

### `ssf inject` 用法
Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ npx spec-superflow list # 或通过 npx 使用
| `ssf execution plan <dir> ...` | 在用户确认选择后,为 Full/legacy Hotfix 保存受 guard 保护的执行计划 |
| `ssf execution show <dir> [--json]` | 查看并校验当前执行计划、wave 与 receipt |
| `ssf execution revise <dir> ...` | 将已有计划保留/升级为 SDD,并生成新 revision;不允许降级 |
| `ssf execution review <dir> ...` | 为一个计划 wave 记录 review receipt |
| `ssf execution review <dir> --wave <id> --base <sha> --head <sha> --report <path> --verdict pass\|fail [--repair-active-projection] [--json]` | 為一個計畫 wave 記錄 review receipt;repair 旗標只修復 current active projection |
| `ssf install-cursor` | 部署到 Cursor `.cursor/` 目录 |
| `ssf install-workbuddy` | 部署到 WorkBuddy marketplace 插件(含 skills/rules/runtime) |
| `ssf install-codebuddy` | 部署到 `~/.codebuddy/`(CodeBuddy Code CLI) |
Expand Down Expand Up @@ -245,6 +245,10 @@ ssf execution revise changes/my-change --mode sdd --confirm --reason "need paral
# 每个 wave 都先写入非空 review report,再记录 receipt。
ssf execution review changes/my-change --wave foundation --base <sha> --head <sha> \
--report .superpowers/sdd/reviews/foundation.md --verdict pass
# 只修復 current root active projection;必須已有相符的 current scoped PASS snapshot。
ssf execution review changes/my-change --wave foundation --base <sha> --head <sha> \
--report .superpowers/sdd/reviews/foundation.md --verdict pass \
--repair-active-projection --json
```

`--report` 相对于 `<change>` 解析,且必须位于
Expand All @@ -257,6 +261,16 @@ report 本身必须为普通、非空、非符号链接文件。
才会放行;修订计划会使旧 receipt 失效。恢复、切换和手动保存是 control-plane
overlay,不会增加第九个状态;其 CLI 与 CodeBuddy/WorkBuddy Markdown adapter 保持相同 guard。

Repair 旗標僅接受 `pass`:root
`<change>/.superpowers/sdd/reviews/<safe-wave>.json` 是 current active projection;
`plans/<identity>/reviews/...` 與 `repair-state` 是 immutable revision evidence。它只會在
current valid plan、known wave、既存且相符的 current scoped PASS snapshot、有效 Git range 與
current report evidence 都通過時建立或更新 root,且只更新 current report hash 與
`recorded_at`。同一 evidence 的 repair 為 no-op,不會重寫任何檔案;FAIL、snapshot 不符、
report/range 無效皆拒絕且不寫入,也不會碰 scoped review、repair-state 或 workspace。
`ssf execution show` 優先使用有效 root;root 缺失或無效 PASS 時才回退有效 scoped receipt,
無效 FAIL 仍是 blocker。未帶 repair 旗標的 `execution review` 行為不變。

---

## 为什么需要它
Expand Down
18 changes: 18 additions & 0 deletions docs/README_en.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,11 @@ ssf execution revise changes/my-change --mode sdd --confirm --reason "need paral
--wave integration:serial:2.1:foundation
ssf execution review changes/my-change --wave foundation --base <sha> --head <sha> \
--report .superpowers/sdd/reviews/foundation.md --verdict pass
# Repair only the current root active projection; a matching current scoped
# PASS snapshot must already exist.
ssf execution review changes/my-change --wave foundation --base <sha> --head <sha> \
--report .superpowers/sdd/reviews/foundation.md --verdict pass \
--repair-active-projection --json
```

The `--report` path is resolved relative to `<change>` and must remain under
Expand All @@ -315,6 +320,19 @@ Recovery, switching, and manual save form a control-plane overlay, not a ninth
workflow state; their CLI and CodeBuddy/WorkBuddy Markdown adapters keep the
same guards.

The repair flag accepts only `pass`. The root
`<change>/.superpowers/sdd/reviews/<safe-wave>.json` is the current active
projection, while `plans/<identity>/reviews/...` and `repair-state` remain
immutable revision evidence. Repair creates or updates only the root receipt,
and only after the current plan, known wave, matching current scoped PASS
snapshot, Git range, and current report evidence all validate. Matching root
evidence is a no-op that preserves the file bytes, mtime, and `recorded_at`.
Invalid FAIL, snapshot, report, or range evidence is rejected without writing;
scoped reviews, repair-state, and workspace are never changed. `ssf execution
show` prefers a valid root receipt, falls back to a valid scoped receipt only
when the root is missing or an invalid PASS, and keeps an invalid FAIL as a
blocker. Without the repair flag, `execution review` behaves as before.

### Fast Paths (Quick / Hotfix / Tweak)

- **Quick** — ≤3 single-module code files/tasks → direct acceptance when low risk; for PRD, Spec/Design, API, data/permission, or cross-module impact, show the risk and let the user choose Quick or Full. A chosen Quick records `tdd`, `new-test`, or `bounded` verification.
Expand Down
21 changes: 21 additions & 0 deletions docs/artifact-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,27 @@ retains or upgrades an existing plan as `sdd`, requires fresh confirmation,
creates a new revision, and
clears prior review receipts; it never permits a downgrade.

#### Active review projection 與 immutable revision evidence

對 current plan 的每個 wave,root
`<change>/.superpowers/sdd/reviews/<safe-wave>.json` 是 `ssf execution show` 使用的 current
active projection;`<change>/.superpowers/sdd/plans/<identity>/reviews/...` 與同 scope 的
`repair-state` 則是 immutable revision evidence。`ssf execution show` 先採用有效 root;root
缺失或為無效 PASS 時才回退有效 scoped receipt,而無效 FAIL 仍維持為 blocker。

只可透過下列明確 opt-in 修復 root projection:

```bash
ssf execution review <dir> --wave <id> --base <sha> --head <sha> --report <path> \
--verdict pass --repair-active-projection [--json]
```

repair 必須通過 current valid plan、known wave、既存且完全相符的 current plan-scoped PASS
snapshot、有效 Git range 與 current report evidence 驗證。成功時只建立或更新 root,並只更新
current report hash 與 `recorded_at`;既有相同 evidence 時為 no-op,不重寫。FAIL verdict、
snapshot 不符或 report/range 無效都會拒絕且不寫入;repair 不會改寫 scoped review、
repair-state 或 workspace。未帶此旗標的一般 `execution review` 保持原本行為。

### Recovery control-plane overlay

Recovery commands operate beside the eight-state workflow, without creating a
Expand Down
31 changes: 29 additions & 2 deletions scripts/lib/cmd-execution.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { parseArgs } from 'node:util';
import { createPlan, describeWaves, EXECUTION_MODES, readPlan, recordReview, validatePlan, writePlan } from './execution-plan.mjs';
import {
createPlan,
describeWaves,
EXECUTION_MODES,
readPlan,
recordReview,
repairActiveReviewProjection,
validatePlan,
writePlan,
} from './execution-plan.mjs';
import {
createRecommendationReceipt,
readCurrentRecommendationReceipt,
Expand All @@ -22,6 +31,7 @@ export function run(args, io = { stdout: process.stdout, stderr: process.stderr
head: { type: 'string' },
report: { type: 'string' },
verdict: { type: 'string' },
'repair-active-projection': { type: 'boolean', default: false },
json: { type: 'boolean', default: false },
help: { type: 'boolean', default: false },
},
Expand Down Expand Up @@ -136,6 +146,23 @@ function recordAndPrintReview(changeDir, values, io) {
requireOption(values.head, '--head');
requireOption(values.report, '--report');
if (!['pass', 'fail'].includes(values.verdict)) throw new Error("--verdict must be 'pass' or 'fail'");
if (values['repair-active-projection']) {
if (values.verdict !== 'pass') throw new Error('Active projection repair request only accepts PASS verdict');
const result = repairActiveReviewProjection(changeDir, values.wave[0], {
status: values.verdict,
base: values.base,
head: values.head,
report: values.report,
});
print(values.json, {
ok: true,
wave: values.wave[0],
mode: 'active-projection-repair',
changed: result.changed,
receipt: result.receipt,
}, `Active review projection for ${values.wave[0]} ${result.changed ? 'repaired' : 'is already current'}.`, io);
return;
}
const receipt = recordReview(changeDir, values.wave[0], {
status: values.verdict,
base: values.base,
Expand Down Expand Up @@ -206,5 +233,5 @@ function printHelp(io) {
ssf execution plan <dir> --mode <mode> --confirm --reason <text> --wave <id>:<strategy>:<task,...>[:<depends-on,...>] [--acknowledge-recommendation]
ssf execution show <dir> [--json]
ssf execution revise <dir> --mode sdd --confirm --reason <text> --wave <id>:<strategy>:<task,...>[:<depends-on,...>] [--acknowledge-recommendation]
ssf execution review <dir> --wave <id> --base <sha> --head <sha> --report <path> --verdict pass|fail\n`);
ssf execution review <dir> --wave <id> --base <sha> --head <sha> --report <path> --verdict pass|fail [--repair-active-projection] [--json]\n`);
}
99 changes: 94 additions & 5 deletions scripts/lib/execution-plan.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,57 @@ export function recordReview(changeDir, waveId, receipt) {
return savedReceipt;
}

export function repairActiveReviewProjection(changeDir, waveId, receipt) {
const plan = readPlan(changeDir);
const validation = validatePlan(changeDir, plan);
if (!validation.valid) {
throw new Error(`Cannot repair the active review projection for an invalid execution plan: ${validation.failures.join('; ')}`);
}
const wave = Array.isArray(plan?.waves) && plan.waves.find(candidate => candidate?.id === waveId);
if (!wave) throw new Error(`Active projection repair references unknown wave '${waveId}'`);
if (receipt?.status !== 'pass') throw new Error('Active projection repair request only accepts PASS receipts');
for (const field of ['base', 'head']) requireText(receipt?.[field], `receipt.${field}`);

const reportEvidence = validateReviewReportEvidence(changeDir, receipt?.report);
const { base, head } = validateReviewRange(changeDir, receipt.base, receipt.head);
const receiptName = `${safeFileName(waveId)}.json`;
const scopedPath = join(getPlanScopedPaths(changeDir, plan).reviews, receiptName);
const scopedReceipt = readScopedRepairReceipt(scopedPath, plan, waveId);
if (scopedReceipt.status !== receipt.status
|| scopedReceipt.base !== base
|| scopedReceipt.head !== head
|| scopedReceipt.report !== reportEvidence.path) {
throw new Error(`Current plan-scoped review snapshot for wave '${waveId}' does not match the active projection repair request`);
}

const rootPath = join(getOverlayPaths(changeDir).reviews, receiptName);
const rootReceipt = readJsonIfPresent(rootPath);
if (rootReceipt?.plan_hash === plan.hash
&& rootReceipt?.plan_revision === plan.revision
&& rootReceipt?.status === 'fail') {
throw new Error(`Wave '${waveId}' has a current FAIL active receipt and cannot use PASS active projection repair`);
}
if (sameReviewEvidence(rootReceipt, {
...scopedReceipt,
report_sha256: reportEvidence.sha256,
})) {
return { changed: false, receipt: rootReceipt };
}

const savedReceipt = {
status: scopedReceipt.status,
base,
head,
report: reportEvidence.path,
report_sha256: reportEvidence.sha256,
plan_hash: plan.hash,
plan_revision: plan.revision,
recorded_at: new Date().toISOString(),
};
atomicWrite(rootPath, `${JSON.stringify(savedReceipt, null, 2)}\n`);
return { changed: true, receipt: savedReceipt };
}

/**
* Returns the current plan's receipt for one wave. Receipts from a previous
* revision/hash are never evidence for the current plan.
Expand All @@ -186,15 +237,18 @@ function readCurrentReviewEvidence(changeDir, waveId, plan = readPlan(changeDir)
if (!plan) return { receipt: null, blocker: null };
const currentScope = getPlanScopedPaths(changeDir, plan);
const currentPath = join(currentScope.reviews, `${safeFileName(waveId)}.json`);
const legacyPath = join(getOverlayPaths(changeDir).reviews, `${safeFileName(waveId)}.json`);
// Newer callers may have written a scoped receipt, but the established
// root receipt remains the compatibility source until a future format
// migration. In either case the plan identity below is mandatory.
const filePath = existsSync(currentPath) ? currentPath : legacyPath;
const rootPath = join(getOverlayPaths(changeDir).reviews, `${safeFileName(waveId)}.json`);
const rootEvidence = readReviewEvidenceFile(changeDir, rootPath, plan);
if (rootEvidence.receipt || rootEvidence.blocker) return rootEvidence;
return readReviewEvidenceFile(changeDir, currentPath, plan);
}

function readReviewEvidenceFile(changeDir, filePath, plan) {
if (!existsSync(filePath)) return { receipt: null, blocker: null };
try {
const receipt = JSON.parse(readFileSync(filePath, 'utf8'));
if (receipt?.plan_hash !== plan.hash || receipt?.plan_revision !== plan.revision) return { receipt: null, blocker: null };
if (!REVIEW_STATUSES.has(receipt?.status)) return { receipt: null, blocker: null };
const range = validateReviewRange(changeDir, receipt?.base, receipt?.head);
if (receipt.base !== range.base || receipt.head !== range.head) return { receipt: null, blocker: null };
// Reports remain evidence only while their safety and content identity can
Expand All @@ -221,6 +275,41 @@ function readCurrentReviewEvidence(changeDir, waveId, plan = readPlan(changeDir)
}
}

function readScopedRepairReceipt(filePath, plan, waveId) {
if (!existsSync(filePath)) {
throw new Error(`Current plan-scoped review receipt is missing for wave '${waveId}'`);
}
let receipt;
try {
receipt = JSON.parse(readFileSync(filePath, 'utf8'));
} catch (error) {
throw new Error(`Current plan-scoped review receipt is invalid or cannot be parsed: ${error.message}`);
}
if (receipt?.plan_hash !== plan.hash || receipt?.plan_revision !== plan.revision) {
throw new Error(`Current plan-scoped review receipt plan identity does not match the current execution plan for wave '${waveId}'`);
}
if (receipt?.status !== 'pass') {
throw new Error(`Current plan-scoped review receipt for wave '${waveId}' must record PASS`);
}
return receipt;
}

function readJsonIfPresent(filePath) {
if (!existsSync(filePath)) return null;
try {
return JSON.parse(readFileSync(filePath, 'utf8'));
} catch {
return null;
}
}

function sameReviewEvidence(actual, expected) {
if (!actual || typeof actual !== 'object') return false;
if (typeof actual.recorded_at !== 'string' || actual.recorded_at.trim() === '') return false;
return ['status', 'base', 'head', 'report', 'report_sha256', 'plan_hash', 'plan_revision']
.every(field => actual[field] === expected[field]);
}

/**
* Machine-readable execution status used by `ssf execution show`. A wave is
* eligible when it has no current receipt, or its current receipt failed and
Expand Down
2 changes: 1 addition & 1 deletion scripts/spec-superflow.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Commands:
Show and validate the current execution plan
execution revise <change-dir> --mode sdd --confirm --reason <text> --wave <id>:<strategy>:<task,...> [--acknowledge-recommendation]
Upgrade inline/batch to SDD, or replan existing SDD waves, as a new revision
execution review <change-dir> --wave <id> --base <sha> --head <sha> --report <path> --verdict pass|fail
execution review <change-dir> --wave <id> --base <sha> --head <sha> --report <path> --verdict pass|fail [--repair-active-projection] [--json]
Record one review receipt for a planned wave
resume [change-dir] [--json]
Recover the only active change or an explicit change context
Expand Down
Loading