Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format loosely follows Keep a Changelog.

## [Unreleased]

### Fixed

- **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.

## [1.0.0] - 2026-08-03

### Added
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ execution plan。它位于 `<change>/.superpowers/sdd/execution-plan.json`,不
候选项和推荐展示给用户。`plan` 或 `revise` 只接受匹配当前 artifact、contract 和 wave 的
凭据。用户用 `--confirm` 明确确认选择;若选择与推荐不同,必须额外
传入 `--acknowledge-recommendation` 记录已知风险。Batch Inline 始终串行,绝不冒充并行。
Quick、direct Hotfix 与 `tweak` 保持轻量例外:不要求 contract、execution plan、wave receipt 或 DP;在边界内验证后持久化 `test_result: pass`。
Quick、direct Hotfix 与 `tweak` 保持轻量例外:正常完成时不要求 contract、execution plan、wave receipt 或 DP;在边界内验证后持久化 `test_result: pass`。但一旦进入 DP-5 调试升级,任何路径都必须先有当前有效的 execution plan,才可记录失败尝试或持久化升级

```bash
ssf execution recommend changes/my-change \
Expand Down Expand Up @@ -334,6 +334,8 @@ spec-superflow 把这两类问题分开处理:先判断改动风险;小改

**如何选择:** Quick、direct Hotfix、Tweak 默认保持轻量,只记录范围和验证;Full 与 legacy Hotfix 才要求执行契约、执行计划和 review receipt。风险会说明原因并交给用户选择,不会擅自升级路径。

**DP-5 调试门禁:** 每次失败修复使用 `ssf debug attempt record` 保存唯一且可验证的证据;无论工作流路径,记录前都必须有 current、有效的 execution plan。Wave Review failure 不会计入调试次数。只有当前 execution context 下至少 3 次不同失败尝试,并由用户执行 `ssf debug escalate ... --confirm` 后,才会记录 DP-5。通用 `state set` 不能写入 `dp_5_*`,且不能通过多行值注入这些字段。

### 快速路径(Quick / Hotfix / Tweak)

- **Quick** — ≤3 文件/任务、单模块代码:低风险时同轮推荐/接受;触及 PRD、Spec/Design、API、数据/权限或跨模块时,展示风险后由用户选择 Quick 或 Full。选择 Quick 会记录 `tdd`、`new-test` 或 `bounded` 验证策略。
Expand Down
4 changes: 3 additions & 1 deletion docs/README_en.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ You: "add authorization to the API"

**Path selection:** Quick, direct Hotfix, and Tweak remain lightweight: record the boundary and verification only. Full and legacy Hotfix require an execution contract, execution plan, and review receipt. Risks are explained for the user to choose from; they do not silently upgrade a path.

**DP-5 debugging gate:** Record every failed fix with `ssf debug attempt record` and distinct, verifiable evidence. Every workflow path must have a current, valid execution plan before it records an attempt. Wave Review failures do not count as debugging attempts. DP-5 is persisted only after at least three failed attempts in that plan context and an explicit `ssf debug escalate ... --confirm`; generic `state set` cannot write or inject `dp_5_*`.

### Guarded execution plans

For Full/legacy Hotfix, DP-4 is a persisted, current execution plan at
Expand All @@ -278,7 +280,7 @@ user records a choice with `--confirm`; `plan` and `revise` require a receipt ma
artifacts, contract, and waves. A non-recommended choice also requires
`--acknowledge-recommendation`. Batch Inline remains serial and never claims
parallel work.
Quick, direct Hotfix, and Tweak are exempt from contract, execution-plan, and review-receipt gates; they persist `test_result: pass` after bounded verification.
Quick, direct Hotfix, and Tweak are exempt from contract, execution-plan, and review-receipt gates during their normal bounded path; they persist `test_result: pass` after verification. If they reach DP-5 debugging escalation, they must first establish a current execution plan before recording attempts or persisting DP-5.

```bash
ssf execution recommend changes/my-change \
Expand Down
3 changes: 2 additions & 1 deletion docs/decision-points.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
- **名称**:调试升级
- **触发条件**:bug-investigator 连续 3 次或更多修复尝试失败后,无法自动解决当前问题
- **所需输入**:失败日志、每次修复尝试的具体方案与结果、错误根因分析、剩余可行方案(如有)
- **预期输出**:用户决定继续调试(可附带方向指引)或放弃当前任务并标记为阻塞
- **证据门禁**:每次失败修复必须通过 `ssf debug attempt record` 写入结构化 ledger,包含唯一 attempt id、摘要和 change 目录内的物理证据文件。所有路径都必须先有 current、有效的 execution plan;缺失、陈旧或不匹配的 plan 会被拒绝。Wave Review 的 repair failure 不计入 debugging attempt;重复证据也会被拒绝。
- **预期输出**:至少 3 次不同且证据完整的失败尝试后,用户决定继续调试或放弃;仅 `ssf debug escalate ... --confirm` 可以持久化 DP-5,通用 `state set` 不可写入 `dp_5_*`。
- **关联 skill**:`spec-superflow:bug-investigator`

## DP-6: 验证失败
Expand Down
6 changes: 4 additions & 2 deletions docs/state-machine.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ accept only a receipt whose artifacts, contract, and waves still match. The user
selected mode with `--confirm`; a non-recommended selection also requires
`--acknowledge-recommendation`. Batch Inline remains serial and is never a
substitute for parallel execution.
Quick, direct Hotfix, and Tweak are exempt from execution-plan and review-receipt requirements; each closes only with `test_result: pass`.
Quick, direct Hotfix, and Tweak are exempt from execution-plan and review-receipt requirements on their normal bounded path; each closes with `test_result: pass`. If any enters DP-5 debugging escalation, it must first establish a current execution plan before recording an attempt or persisting DP-5.

For Full/legacy Hotfix, the plan names ordered execution waves, dependencies,
and parallel/serial strategy. `ssf execution show <change-dir> --json` reports
Expand Down Expand Up @@ -195,7 +195,9 @@ During `executing`, if a bug, test failure, or unexpected behavior blocks progre
1. Pause `executing` and enter `debugging`
2. `bug-investigator` performs 4-phase root cause analysis
3. If root cause found → fix (with TDD) → return to `executing`
4. If 3+ fix attempts fail → question architecture → escalate to user
4. Before recording any attempt, require a current valid execution plan, including for Quick/direct Hotfix/Tweak; then record one distinct evidence-backed attempt with `ssf debug attempt record`; Wave Review repair failures remain separate
5. If 3+ recorded fix attempts fail → question architecture → present the ledger to the user
6. Persist DP-5 only through `ssf debug escalate ... --confirm`; raw `state set dp_5_*` is blocked

## Anti-Pattern

Expand Down
23 changes: 19 additions & 4 deletions scripts/lib/cmd-audit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { parseArgs } from 'node:util';
import { readState } from './state-loader.mjs';
import { inspectDebugEscalation } from './debug-attempts.mjs';

const DP_NAMES = {
0: '用户确认门禁',
Expand Down Expand Up @@ -39,14 +40,26 @@ function formatDpResult(state, dp) {
}

function generateReport(changeDir, state) {
const debugEscalation = inspectDebugEscalation(changeDir, state);
const rows = [];
for (let i = 0; i <= 7; i++) {
const result = formatDpResult(state, i);
const recordedResult = formatDpResult(state, i);
const unsupportedDebugRecord = i === 5
&& recordedResult !== 'not recorded'
&& !debugEscalation.supported;
const result = unsupportedDebugRecord ? `unsupported: ${recordedResult}` : recordedResult;
const timestamp = formatTimestamp(state[`dp_${i}_timestamp`]);
rows.push({ dp: i, name: DP_NAMES[i], result, timestamp });
rows.push({
dp: i,
name: DP_NAMES[i],
result,
timestamp,
recorded: recordedResult !== 'not recorded' && !unsupportedDebugRecord,
unsupportedReason: unsupportedDebugRecord ? debugEscalation.reason : null,
});
}

const recordedCount = rows.filter(r => r.result !== 'not recorded').length;
const recordedCount = rows.filter(r => r.recorded).length;
const missingCount = rows.length - recordedCount;

let md = `# Decision-Point Audit Report\n\n`;
Expand All @@ -68,7 +81,9 @@ function generateReport(changeDir, state) {
md += `### DP-${r.dp}: ${r.name}\n\n`;
md += `- **结果**: ${r.result}\n`;
md += `- **时间戳**: ${r.timestamp}\n`;
if (r.result === 'not recorded') {
if (r.unsupportedReason) {
md += `- **解读**: DP-5 unsupported — ${r.unsupportedReason}.\n`;
} else if (r.result === 'not recorded') {
md += '- **解读**: 该决策点尚未记录结果。如果工作流已经经过该阶段,请检查是否漏记。\n';
} else {
md += `- **解读**: 决策点 DP-${r.dp} 已记录为 "${r.result}"。\n`;
Expand Down
84 changes: 84 additions & 0 deletions scripts/lib/cmd-debug.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { parseArgs } from 'node:util';
import { recordDebugAttempt, recordDebugEscalation, showDebugAttempts } from './debug-attempts.mjs';

const OPTIONS = {
id: { type: 'string' },
summary: { type: 'string' },
evidence: { type: 'string' },
decision: { type: 'string' },
reason: { type: 'string' },
confirm: { type: 'boolean', default: false },
json: { type: 'boolean', default: false },
help: { type: 'boolean', default: false },
};

class UsageError extends Error {}

export function run(args, io = { stdout: process.stdout, stderr: process.stderr }) {
let parsed;
try {
parsed = parseArgs({ args, options: OPTIONS, allowPositionals: true });
} catch (error) {
return fail(error.message, 2, io);
}

const { positionals, values } = parsed;
if (values.help || positionals.length === 0) {
printHelp(io);
return { exitCode: 0 };
}

try {
if (positionals[0] === 'attempt') {
if (!['record', 'show'].includes(positionals[1]) || positionals.length !== 3) {
throw new UsageError('Usage: ssf debug attempt <record|show> <change-dir> [options]');
}
const changeDir = positionals[2];
if (positionals[1] === 'record') {
const result = recordDebugAttempt(changeDir, {
id: values.id,
summary: values.summary,
evidence: values.evidence,
});
return print({ ok: true, ...result }, values.json, io);
}
return print({ ok: true, ...showDebugAttempts(changeDir) }, values.json, io);
}

if (positionals[0] === 'escalate') {
if (positionals.length !== 2) {
throw new UsageError('Usage: ssf debug escalate <change-dir> --decision <continue|abandon> --reason <text> --confirm');
}
const result = recordDebugEscalation(positionals[1], {
decision: values.decision,
reason: values.reason,
confirm: values.confirm,
});
return print({ ok: true, ...result }, values.json, io);
}

throw new UsageError(`Unknown debug subcommand: ${positionals[0]}`);
} catch (error) {
return fail(error.message, error instanceof UsageError ? 2 : 1, io);
}
}

function print(value, json, io) {
if (json) io.stdout.write(`${JSON.stringify(value)}\n`);
else if (value.attempt) io.stdout.write(`Debug attempt ${value.attempt.id} recorded (${value.attempt_count}/3).\n`);
else if (value.escalation) io.stdout.write(`DP-5 recorded: ${value.escalation.result}\n`);
else io.stdout.write(`Debug attempts recorded: ${value.attempt_count}\n`);
return { exitCode: 0 };
}

function fail(message, exitCode, io) {
io.stderr.write(`${message}\n`);
return { exitCode };
}

function printHelp(io) {
io.stdout.write(`Usage:
ssf debug attempt record <dir> --id <id> --summary <text> --evidence <path> [--json]
ssf debug attempt show <dir> [--json]
ssf debug escalate <dir> --decision <continue|abandon> --reason <text> --confirm [--json]\n`);
}
5 changes: 4 additions & 1 deletion scripts/lib/cmd-state.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ const SETTABLE_FIELDS = [
'dp_1_result', 'dp_1_timestamp', 'dp_1_decisions', 'dp_1_confirmed',
'dp_2_result', 'dp_2_timestamp', 'dp_2_decisions', 'dp_2_confirmed',
'dp_3_result', 'dp_3_timestamp', 'dp_3_decisions', 'dp_3_confirmed',
'dp_5_result', 'dp_5_timestamp', 'dp_5_decisions', 'dp_5_confirmed',
'dp_6_result', 'dp_6_timestamp', 'dp_6_decisions', 'dp_6_confirmed',
'dp_7_result', 'dp_7_timestamp', 'dp_7_decisions', 'dp_7_confirmed',
];
Expand Down Expand Up @@ -224,6 +223,10 @@ export async function run(args) {
console.error(`⛔ Field '${field}' is not settable (use 'transition' for state, or check SETTABLE_FIELDS)`);
process.exit(1);
}
if (/[\p{Cc}\p{Zl}\p{Zp}]/u.test(value)) {
console.error('State field values must not contain control characters or line separators');
process.exit(1);
}
updateField(changeDir, field, value);
if (values.json) {
console.log(JSON.stringify({ ok: true, field, value }));
Expand Down
Loading
Loading