@peterxiaoyang/superspec 0.1.15-alpha → 0.1.17-alpha
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/README.md +48 -31
- package/dist/cli.js +41 -34
- package/dist/format.d.ts +9 -0
- package/dist/format.js +35 -5
- package/dist/install.d.ts +19 -0
- package/dist/install.js +185 -0
- package/dist/next.js +42 -11
- package/dist/record.js +81 -6
- package/dist/transition.d.ts +2 -1
- package/dist/transition.js +55 -25
- package/dist/types.d.ts +5 -1
- package/package.json +1 -1
- package/templates/workflow/agents/architect.toml +13 -0
- package/templates/workflow/agents/code-reviewer.toml +13 -0
- package/templates/workflow/agents/critic.toml +13 -0
- package/templates/workflow/agents/executor.toml +13 -0
- package/templates/workflow/agents/explore.toml +13 -0
- package/templates/workflow/agents/test-engineer.toml +13 -0
- package/templates/workflow/agents/test-runner.toml +13 -0
- package/templates/workflow/agents/verifier.toml +13 -0
- package/templates/workflow/prompts/architect.md +44 -0
- package/templates/workflow/prompts/code-reviewer.md +34 -0
- package/templates/workflow/prompts/critic.md +47 -0
- package/templates/workflow/prompts/executor.md +32 -0
- package/templates/workflow/prompts/explore.md +27 -0
- package/templates/workflow/prompts/test-engineer.md +45 -0
- package/templates/workflow/prompts/test-runner.md +35 -0
- package/templates/workflow/prompts/verifier.md +53 -0
- package/templates/workflow/skills/superspec-apply/SKILL.md +8 -18
- package/templates/workflow/skills/superspec-archive/SKILL.md +2 -9
- package/templates/workflow/skills/superspec-explore/SKILL.md +5 -10
- package/templates/workflow/skills/superspec-propose/SKILL.md +28 -13
- package/templates/workflow/skills/superspec-review/SKILL.md +6 -28
package/dist/next.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
// SuperSpec 流程引擎 — next:返回可执行路径
|
|
2
2
|
import { rebuildSnapshot } from "./sync.js";
|
|
3
|
-
import {
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { validateDiscovery, countDiscoveryOpenQuestions } from "./format.js";
|
|
5
|
+
import { validateDiscovery, countDiscoveryOpenQuestions, collectProposeOpenQuestions } from "./format.js";
|
|
6
|
+
const ACTIVE_PROPOSAL_REVIEW_ROLES = new Set(["critic", "architect", "test-engineer"]);
|
|
7
|
+
function isActiveProposalReviewJob(job) {
|
|
8
|
+
return job.created_from_transition === "propose-ready" && ACTIVE_PROPOSAL_REVIEW_ROLES.has(job.role);
|
|
9
|
+
}
|
|
6
10
|
function packetCommand(change, jobId) {
|
|
7
11
|
return `superspec jobs packet --change "${change}" --job "${jobId}"`;
|
|
8
12
|
}
|
|
9
13
|
function transitionCommand(change, name, extra = "") {
|
|
10
14
|
return `superspec transition ${name} --change "${change}"${extra ? " " + extra : ""}`;
|
|
11
15
|
}
|
|
16
|
+
function riskFlag(risk) {
|
|
17
|
+
return risk === "strict" ? "" : `--risk ${risk}`;
|
|
18
|
+
}
|
|
12
19
|
/** next 命令:读 snapshot,返回唯一可执行路径 */
|
|
13
|
-
export function next(projectRoot, change, changeRoot, defaultRisk = "
|
|
20
|
+
export function next(projectRoot, change, changeRoot, defaultRisk = "strict") {
|
|
14
21
|
const snapshot = rebuildSnapshot(projectRoot, change, changeRoot);
|
|
15
22
|
switch (snapshot.state) {
|
|
16
23
|
case "init":
|
|
@@ -46,14 +53,25 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "normal") {
|
|
|
46
53
|
return {
|
|
47
54
|
state: "explore",
|
|
48
55
|
path: "next_command",
|
|
49
|
-
next_command: transitionCommand(change, "explore"),
|
|
56
|
+
next_command: transitionCommand(change, "explore", riskFlag(defaultRisk)),
|
|
50
57
|
reason: "探索完成,推进到计划阶段",
|
|
51
58
|
missing_inputs: [],
|
|
52
59
|
};
|
|
53
60
|
}
|
|
54
61
|
case "propose": {
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
const openQuestions = collectProposeOpenQuestions(changeRoot);
|
|
63
|
+
if (openQuestions.openCount > 0) {
|
|
64
|
+
const files = openQuestions.files.map(f => `${f.path}(${f.openCount})`).join(", ");
|
|
65
|
+
const ask = {
|
|
66
|
+
question: `计划文档有 ${openQuestions.openCount} 个待用户确认问题:${files}。请确认并更新计划文档后继续`,
|
|
67
|
+
allowed_answers: ["所有问题已确认"],
|
|
68
|
+
scope: "propose_open_questions",
|
|
69
|
+
};
|
|
70
|
+
return { state: "propose", path: "ask_user", ask_user: ask, reason: `有 ${openQuestions.openCount} 个 propose 未确认问题` };
|
|
71
|
+
}
|
|
72
|
+
const proposalReviewJobs = snapshot.open_jobs.filter(isActiveProposalReviewJob);
|
|
73
|
+
if (proposalReviewJobs.length > 0) {
|
|
74
|
+
const jobs = proposalReviewJobs.map(j => ({
|
|
57
75
|
job_id: j.job_id,
|
|
58
76
|
role: j.role,
|
|
59
77
|
packet_command: packetCommand(change, j.job_id),
|
|
@@ -65,16 +83,28 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "normal") {
|
|
|
65
83
|
reason: `有 ${jobs.length} 个待完成工作项`,
|
|
66
84
|
};
|
|
67
85
|
}
|
|
68
|
-
const riskFlag = `--risk ${defaultRisk}`;
|
|
69
86
|
return {
|
|
70
87
|
state: "propose",
|
|
71
88
|
path: "next_command",
|
|
72
|
-
next_command: transitionCommand(change, "propose-ready", riskFlag),
|
|
89
|
+
next_command: transitionCommand(change, "propose-ready", riskFlag(defaultRisk)),
|
|
73
90
|
reason: "计划文档就绪,提交 propose-ready",
|
|
74
91
|
missing_inputs: [],
|
|
75
92
|
};
|
|
76
93
|
}
|
|
77
|
-
case "propose_ready":
|
|
94
|
+
case "propose_ready": {
|
|
95
|
+
const proposalReviewJobs = snapshot.open_jobs.filter(isActiveProposalReviewJob);
|
|
96
|
+
if (proposalReviewJobs.length > 0) {
|
|
97
|
+
return {
|
|
98
|
+
state: "propose_ready",
|
|
99
|
+
path: "required_job",
|
|
100
|
+
required_jobs: proposalReviewJobs.map(j => ({
|
|
101
|
+
job_id: j.job_id,
|
|
102
|
+
role: j.role,
|
|
103
|
+
packet_command: packetCommand(change, j.job_id),
|
|
104
|
+
})),
|
|
105
|
+
reason: `有 ${proposalReviewJobs.length} 个待完成 proposal 审查工作项`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
78
108
|
return {
|
|
79
109
|
state: "propose_ready",
|
|
80
110
|
path: "next_command",
|
|
@@ -82,6 +112,7 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "normal") {
|
|
|
82
112
|
reason: "计划就绪,开始执行",
|
|
83
113
|
missing_inputs: [],
|
|
84
114
|
};
|
|
115
|
+
}
|
|
85
116
|
case "apply": {
|
|
86
117
|
// 有 open job → 做
|
|
87
118
|
if (snapshot.open_jobs.length > 0) {
|
|
@@ -99,7 +130,7 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "normal") {
|
|
|
99
130
|
return {
|
|
100
131
|
state: "apply",
|
|
101
132
|
path: "next_command",
|
|
102
|
-
next_command: transitionCommand(change, "review-ready"),
|
|
133
|
+
next_command: transitionCommand(change, "review-ready", riskFlag(defaultRisk)),
|
|
103
134
|
reason: "所有任务完成,进入审查",
|
|
104
135
|
missing_inputs: [],
|
|
105
136
|
};
|
|
@@ -124,7 +155,7 @@ export function next(projectRoot, change, changeRoot, defaultRisk = "normal") {
|
|
|
124
155
|
return {
|
|
125
156
|
state: "apply_done",
|
|
126
157
|
path: "next_command",
|
|
127
|
-
next_command: transitionCommand(change, "review-ready"),
|
|
158
|
+
next_command: transitionCommand(change, "review-ready", riskFlag(defaultRisk)),
|
|
128
159
|
reason: "所有任务完成,进入审查",
|
|
129
160
|
missing_inputs: [],
|
|
130
161
|
};
|
package/dist/record.js
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
import { readFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { ensureChangeLayout, readEvents, appendEvent, makeEvent, sha256File, withLock, } from "./store.js";
|
|
5
|
+
const REVIEW_REPORT_REQUIRED_FIELDS = ["role", "verdict", "findings"];
|
|
6
|
+
const REVIEW_REPORT_OPTIONAL_FIELDS = ["summary", "evidence_refs", "risks", "open_questions"];
|
|
7
|
+
const REVIEWER_KINDS = new Set(["codex-subagent", "human", "external-agent"]);
|
|
8
|
+
function requiresReviewer(role) {
|
|
9
|
+
return role === "critic" || role === "architect" || role === "test-engineer";
|
|
10
|
+
}
|
|
11
|
+
function recommendedAgentForRole(role) {
|
|
12
|
+
switch (role) {
|
|
13
|
+
case "critic": return "critic";
|
|
14
|
+
case "architect": return "architect";
|
|
15
|
+
case "test-engineer": return "test-engineer";
|
|
16
|
+
case "verifier": return "verifier";
|
|
17
|
+
case "executor": return "executor";
|
|
18
|
+
case "test-run": return "test-runner";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function roleDescription(role) {
|
|
22
|
+
switch (role) {
|
|
23
|
+
case "critic":
|
|
24
|
+
return "从反方角度审查需求澄清或计划材料中的隐藏假设、范围漂移、验收漏洞和证据缺口";
|
|
25
|
+
case "architect":
|
|
26
|
+
return "审查架构边界、接口契约、长期维护风险和设计取舍";
|
|
27
|
+
case "test-engineer":
|
|
28
|
+
return "审查测试契约、覆盖策略、RED/GREEN 可信度和验收场景映射";
|
|
29
|
+
case "verifier":
|
|
30
|
+
return "验证 proposal、实现状态、任务完成、测试契约和 SuperSpec 证据是否足以支撑完成结论";
|
|
31
|
+
case "executor":
|
|
32
|
+
return "执行受限实现工作项";
|
|
33
|
+
case "test-run":
|
|
34
|
+
return "执行受限测试工作项";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
5
37
|
/** 从 events 中查找 job(H4 修复:job 只在 transition_commit 的 new_jobs payload 里) */
|
|
6
38
|
function findJob(events, jobId) {
|
|
7
39
|
for (const ev of events) {
|
|
@@ -65,15 +97,50 @@ export function recordJobSubmit(projectRoot, change, changeRoot, jobId, reportFi
|
|
|
65
97
|
const reportDigest = sha256File(reportFile) ?? "sha256:unknown";
|
|
66
98
|
// acceptance checks
|
|
67
99
|
const checks = [];
|
|
68
|
-
// 0.
|
|
100
|
+
// 0. 报告格式和角色匹配(最小 JSON contract)
|
|
69
101
|
try {
|
|
70
102
|
const report = JSON.parse(reportContent);
|
|
71
|
-
if (report
|
|
72
|
-
checks.push(
|
|
103
|
+
if (!report || typeof report !== "object" || Array.isArray(report)) {
|
|
104
|
+
checks.push("报告必须是 JSON object");
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const obj = report;
|
|
108
|
+
for (const field of REVIEW_REPORT_REQUIRED_FIELDS) {
|
|
109
|
+
if (!(field in obj))
|
|
110
|
+
checks.push(`报告缺少必填字段 ${field}`);
|
|
111
|
+
}
|
|
112
|
+
if (obj.role !== job.role) {
|
|
113
|
+
checks.push(`报告角色 ${String(obj.role)} 与工作项角色 ${job.role} 不匹配`);
|
|
114
|
+
}
|
|
115
|
+
if (obj.verdict !== "pass" && obj.verdict !== "fail") {
|
|
116
|
+
checks.push("报告 verdict 必须是 pass 或 fail");
|
|
117
|
+
}
|
|
118
|
+
if (!Array.isArray(obj.findings)) {
|
|
119
|
+
checks.push("报告 findings 必须是数组");
|
|
120
|
+
}
|
|
121
|
+
if (obj.verdict === "fail") {
|
|
122
|
+
checks.push("报告 verdict=fail,工作项未通过");
|
|
123
|
+
}
|
|
124
|
+
if (requiresReviewer(job.role)) {
|
|
125
|
+
if (!("reviewer" in obj))
|
|
126
|
+
checks.push("报告缺少必填字段 reviewer");
|
|
127
|
+
const reviewer = obj.reviewer;
|
|
128
|
+
if (!reviewer || typeof reviewer !== "object" || Array.isArray(reviewer)) {
|
|
129
|
+
checks.push("报告 reviewer 必须是包含 kind/id 的对象");
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
if (typeof reviewer.kind !== "string" || !REVIEWER_KINDS.has(reviewer.kind)) {
|
|
133
|
+
checks.push(`报告 reviewer.kind 必须是 ${[...REVIEWER_KINDS].join("|")} 之一`);
|
|
134
|
+
}
|
|
135
|
+
if (typeof reviewer.id !== "string" || reviewer.id.trim() === "") {
|
|
136
|
+
checks.push("报告 reviewer.id 必须是非空字符串");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
73
140
|
}
|
|
74
141
|
}
|
|
75
142
|
catch {
|
|
76
|
-
|
|
143
|
+
checks.push("报告必须是有效 JSON");
|
|
77
144
|
}
|
|
78
145
|
// 1. boundFiles 仍匹配当前文档(missing 也算不匹配)
|
|
79
146
|
for (const bf of job.boundFiles) {
|
|
@@ -199,10 +266,18 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
199
266
|
packet: {
|
|
200
267
|
job_id: job.job_id,
|
|
201
268
|
role: job.role,
|
|
269
|
+
recommended_agent: recommendedAgentForRole(job.role),
|
|
202
270
|
boundFiles: job.boundFiles,
|
|
203
271
|
packet_digest: job.packet_digest,
|
|
204
|
-
required_output_kind: "
|
|
205
|
-
|
|
272
|
+
required_output_kind: "job_report_json",
|
|
273
|
+
output_contract_fields: requiresReviewer(job.role) ? [...REVIEW_REPORT_REQUIRED_FIELDS, "reviewer"] : [...REVIEW_REPORT_REQUIRED_FIELDS],
|
|
274
|
+
output_contract_optional_fields: [...REVIEW_REPORT_OPTIONAL_FIELDS],
|
|
275
|
+
output_instructions: `${roleDescription(job.role)}。请审查 ${job.boundFiles.map(f => f.path).join(", ")},` +
|
|
276
|
+
(requiresReviewer(job.role) ? `必须由独立 ${recommendedAgentForRole(job.role)} reviewer 执行并在 reviewer.kind/id 中记录来源,` : "") +
|
|
277
|
+
`产出 JSON 报告文件并通过 superspec record job-submit 登记。` +
|
|
278
|
+
(requiresReviewer(job.role)
|
|
279
|
+
? `最小格式:{"role":"${job.role}","verdict":"pass|fail","findings":[],"reviewer":{"kind":"codex-subagent","id":"<thread-or-agent-id>"}}`
|
|
280
|
+
: `最小格式:{"role":"${job.role}","verdict":"pass|fail","findings":[]}`),
|
|
206
281
|
stop_conditions: ["审查完成后提交报告,不要修改文档"],
|
|
207
282
|
created_from_transition: job.created_from_transition,
|
|
208
283
|
},
|
package/dist/transition.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ interface Decision {
|
|
|
9
9
|
type: string;
|
|
10
10
|
payload: Record<string, unknown>;
|
|
11
11
|
}[];
|
|
12
|
+
details?: Record<string, unknown>;
|
|
12
13
|
postCommit?: (projectRoot: string, change: string, changeRoot: string) => void;
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
@@ -24,7 +25,7 @@ export declare function commitTransition(projectRoot: string, change: string, ch
|
|
|
24
25
|
}): TransitionResult;
|
|
25
26
|
export declare function proposeReady(projectRoot: string, change: string, changeRoot: string, risk?: "minimal" | "normal" | "strict"): TransitionResult;
|
|
26
27
|
export declare function transitionInit(projectRoot: string, change: string, changeRoot: string): TransitionResult;
|
|
27
|
-
export declare function transitionExplore(projectRoot: string, change: string, changeRoot: string): TransitionResult;
|
|
28
|
+
export declare function transitionExplore(projectRoot: string, change: string, changeRoot: string, risk?: "minimal" | "normal" | "strict"): TransitionResult;
|
|
28
29
|
export declare function startApply(projectRoot: string, change: string, changeRoot: string): TransitionResult;
|
|
29
30
|
export declare function taskStart(projectRoot: string, change: string, changeRoot: string, taskId: string): TransitionResult;
|
|
30
31
|
export declare function reviewReady(projectRoot: string, change: string, changeRoot: string, risk?: "minimal" | "normal" | "strict"): TransitionResult;
|
package/dist/transition.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs";
|
|
4
4
|
import { ensureChangeLayout, readEvents, appendEvent, makeEvent, writeSnapshot, snapshotDigest, withLock, idempotencyKey, stagingDir, sha256File, sha256Text, } from "./store.js";
|
|
5
5
|
import { rebuildSnapshot } from "./sync.js";
|
|
6
|
-
import { validateDiscovery, findTaskInLines, parseTasksMd, tasksStructureDigest } from "./format.js";
|
|
6
|
+
import { validateDiscovery, collectProposeOpenQuestions, findTaskInLines, parseTasksMd, tasksStructureDigest } from "./format.js";
|
|
7
7
|
let transitionSeq = 0;
|
|
8
8
|
function newTransitionId() { return `T-${Date.now()}-${++transitionSeq}`; }
|
|
9
9
|
let jobSeq = 0;
|
|
@@ -11,13 +11,13 @@ function newJobId(change, role) { return `JOB-${change.slice(0, 8)}-${role.slice
|
|
|
11
11
|
const TRANSITION_REQUIREMENTS = {
|
|
12
12
|
"propose-ready": {
|
|
13
13
|
minimal: [],
|
|
14
|
-
normal: ["
|
|
15
|
-
strict: ["critic
|
|
14
|
+
normal: ["critic"],
|
|
15
|
+
strict: ["critic", "architect", "test-engineer"],
|
|
16
16
|
},
|
|
17
17
|
"explore": {
|
|
18
18
|
minimal: [],
|
|
19
19
|
normal: [],
|
|
20
|
-
strict: ["
|
|
20
|
+
strict: ["critic"],
|
|
21
21
|
},
|
|
22
22
|
};
|
|
23
23
|
/**
|
|
@@ -28,10 +28,10 @@ const TRANSITION_REQUIREMENTS = {
|
|
|
28
28
|
function checkOrCreateReviewJobs(snapshot, requiredRoles, changeRoot, change, transitionName, bindDocPaths) {
|
|
29
29
|
const staleRoles = [];
|
|
30
30
|
for (const role of requiredRoles) {
|
|
31
|
-
const openForRole = snapshot.open_jobs.find(j => j.role === role);
|
|
31
|
+
const openForRole = snapshot.open_jobs.find(j => j.role === role && j.created_from_transition === transitionName);
|
|
32
32
|
if (openForRole)
|
|
33
33
|
return { fromState: snapshot.state, toState: snapshot.state, outcome: "advanced", reason: `工作项 ${role} 已存在(${openForRole.job_id}),请先完成它` };
|
|
34
|
-
const fresh = snapshot.accepted_jobs.find(j => j.role === role);
|
|
34
|
+
const fresh = snapshot.accepted_jobs.find(j => j.role === role && j.created_from_transition === transitionName);
|
|
35
35
|
if (!fresh) {
|
|
36
36
|
staleRoles.push({ role, reason: `需求 ${role} 无已接受的工作项` });
|
|
37
37
|
}
|
|
@@ -57,6 +57,20 @@ function checkOrCreateReviewJobs(snapshot, requiredRoles, changeRoot, change, tr
|
|
|
57
57
|
}
|
|
58
58
|
return null; // 全部满足
|
|
59
59
|
}
|
|
60
|
+
function historicalProposeReadyRoles(projectRoot, change) {
|
|
61
|
+
const roles = new Set();
|
|
62
|
+
for (const ev of readEvents(projectRoot, change)) {
|
|
63
|
+
if (ev.event_type !== "transition_commit")
|
|
64
|
+
continue;
|
|
65
|
+
const newJobs = ev.payload.new_jobs ?? [];
|
|
66
|
+
for (const job of newJobs) {
|
|
67
|
+
if (job.created_from_transition === "propose-ready" &&
|
|
68
|
+
(job.role === "critic" || job.role === "architect" || job.role === "test-engineer"))
|
|
69
|
+
roles.add(job.role);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return [...roles];
|
|
73
|
+
}
|
|
60
74
|
/**
|
|
61
75
|
* 已迁移到 format.ts:findTaskInLines / parseTasksMd / tasksStructureDigest
|
|
62
76
|
* 以下保留 findTaskLine 作为兼容 wrapper(内部调用 format.ts)
|
|
@@ -92,7 +106,7 @@ export function commitTransition(projectRoot, change, changeRoot, opts) {
|
|
|
92
106
|
created_jobs: [], message: decision.message, events_written: 0,
|
|
93
107
|
};
|
|
94
108
|
}
|
|
95
|
-
const { fromState, toState, outcome, newJobs = [], reason, extraEvents = [] } = decision;
|
|
109
|
+
const { fromState, toState, outcome, newJobs = [], reason, extraEvents = [], details } = decision;
|
|
96
110
|
if (fromState !== snapshot.state) {
|
|
97
111
|
return {
|
|
98
112
|
transition: name, outcome: "advanced",
|
|
@@ -127,11 +141,12 @@ export function commitTransition(projectRoot, change, changeRoot, opts) {
|
|
|
127
141
|
created_jobs: newJobs.map(j => j.job_id),
|
|
128
142
|
message: outcome === "advanced" ? `状态推进:${fromState} → ${toState}` : `状态不变(${fromState}),创建了 ${newJobs.length} 个工作项`,
|
|
129
143
|
events_written: 1 + extraEvents.length,
|
|
144
|
+
...(details ? { details } : {}),
|
|
130
145
|
};
|
|
131
146
|
});
|
|
132
147
|
}
|
|
133
148
|
// ===== propose-ready =====
|
|
134
|
-
export function proposeReady(projectRoot, change, changeRoot, risk = "
|
|
149
|
+
export function proposeReady(projectRoot, change, changeRoot, risk = "strict") {
|
|
135
150
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
136
151
|
name: "propose-ready", idempotencyInputs: { risk },
|
|
137
152
|
decide: (snapshot) => {
|
|
@@ -143,6 +158,11 @@ export function proposeReady(projectRoot, change, changeRoot, risk = "normal") {
|
|
|
143
158
|
const tasksContent = readFileSync(tasksPath, "utf8");
|
|
144
159
|
if (!tasksContent.includes("# Tasks") && !tasksContent.includes("- [ ]"))
|
|
145
160
|
return { skip: true, message: "tasks.md 内容不像任务计划文档" };
|
|
161
|
+
const openQuestions = collectProposeOpenQuestions(changeRoot);
|
|
162
|
+
if (openQuestions.openCount > 0) {
|
|
163
|
+
const files = openQuestions.files.map(f => `${f.path}(${f.openCount})`).join(", ");
|
|
164
|
+
return { skip: true, message: `计划文档有 ${openQuestions.openCount} 个待用户确认问题:${files}` };
|
|
165
|
+
}
|
|
146
166
|
if (risk !== "minimal") {
|
|
147
167
|
const artifactsDir = join(changeRoot, ".superspec", "artifacts");
|
|
148
168
|
for (const doc of ["discovery.md", "business-invariants.md", "test-contract.md"]) {
|
|
@@ -172,9 +192,9 @@ export function transitionInit(projectRoot, change, changeRoot) {
|
|
|
172
192
|
});
|
|
173
193
|
}
|
|
174
194
|
// ===== explore =====
|
|
175
|
-
export function transitionExplore(projectRoot, change, changeRoot) {
|
|
195
|
+
export function transitionExplore(projectRoot, change, changeRoot, risk = "strict") {
|
|
176
196
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
177
|
-
name: "explore", idempotencyInputs: { phase: "explore" },
|
|
197
|
+
name: "explore", idempotencyInputs: { phase: "explore", risk },
|
|
178
198
|
decide: (snapshot) => {
|
|
179
199
|
if (snapshot.state === "init")
|
|
180
200
|
return { fromState: "init", toState: "explore", outcome: "advanced", reason: "进入探索阶段" };
|
|
@@ -182,12 +202,12 @@ export function transitionExplore(projectRoot, change, changeRoot) {
|
|
|
182
202
|
const discoveryPath = join(changeRoot, ".superspec", "artifacts", "discovery.md");
|
|
183
203
|
if (!existsSync(discoveryPath))
|
|
184
204
|
return { skip: true, message: "discovery.md 不存在" };
|
|
185
|
-
// explore→propose:校验 discovery +
|
|
205
|
+
// explore→propose:校验 discovery + strict 模式下的 critic 审查
|
|
186
206
|
const discoveryCheck = validateDiscovery(changeRoot);
|
|
187
207
|
if (!discoveryCheck.ok)
|
|
188
208
|
return { skip: true, message: discoveryCheck.message };
|
|
189
209
|
// 通用 job 审查(和 propose-ready 同一个 helper)
|
|
190
|
-
const requiredRoles = TRANSITION_REQUIREMENTS["explore"]?.[
|
|
210
|
+
const requiredRoles = TRANSITION_REQUIREMENTS["explore"]?.[risk] ?? [];
|
|
191
211
|
const reviewResult = checkOrCreateReviewJobs(snapshot, requiredRoles, changeRoot, change, "explore", [".superspec/artifacts/discovery.md"]);
|
|
192
212
|
if (reviewResult)
|
|
193
213
|
return reviewResult;
|
|
@@ -204,6 +224,16 @@ export function startApply(projectRoot, change, changeRoot) {
|
|
|
204
224
|
decide: (snapshot) => {
|
|
205
225
|
if (snapshot.state !== "propose_ready")
|
|
206
226
|
return { skip: true, message: `当前状态 ${snapshot.state},需要 propose_ready` };
|
|
227
|
+
const reviewedRoles = historicalProposeReadyRoles(projectRoot, change);
|
|
228
|
+
if (reviewedRoles.length > 0) {
|
|
229
|
+
const reviewResult = checkOrCreateReviewJobs(snapshot, reviewedRoles, changeRoot, change, "propose-ready", ["proposal.md", "tasks.md", "design.md", ".superspec/artifacts/discovery.md", ".superspec/artifacts/business-invariants.md", ".superspec/artifacts/test-contract.md"]);
|
|
230
|
+
if (reviewResult) {
|
|
231
|
+
return {
|
|
232
|
+
...reviewResult,
|
|
233
|
+
reason: `进入 apply 前需要 fresh proposal 审查:${reviewResult.reason}`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
207
237
|
return { fromState: "propose_ready", toState: "apply", outcome: "advanced", reason: "进入执行阶段" };
|
|
208
238
|
},
|
|
209
239
|
});
|
|
@@ -240,12 +270,13 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
|
|
|
240
270
|
fromState: "apply", toState: "apply", outcome: "advanced",
|
|
241
271
|
reason: `创建任务 ${taskId} 执行尝试`,
|
|
242
272
|
extraEvents: [{ type: "task_started", payload: attempt }],
|
|
273
|
+
details: { attempt_id: attempt.attempt_id },
|
|
243
274
|
};
|
|
244
275
|
},
|
|
245
276
|
});
|
|
246
277
|
}
|
|
247
278
|
// ===== review-ready =====
|
|
248
|
-
export function reviewReady(projectRoot, change, changeRoot, risk = "
|
|
279
|
+
export function reviewReady(projectRoot, change, changeRoot, risk = "strict") {
|
|
249
280
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
250
281
|
name: "review-ready", idempotencyInputs: { phase: "review-ready", risk },
|
|
251
282
|
decide: (snapshot) => {
|
|
@@ -259,27 +290,26 @@ export function reviewReady(projectRoot, change, changeRoot, risk = "normal") {
|
|
|
259
290
|
return { fromState: "apply", toState: "apply_done", outcome: "advanced", reason: "所有任务完成" };
|
|
260
291
|
}
|
|
261
292
|
if (snapshot.state === "apply_done") {
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
if (
|
|
265
|
-
return { skip: true, message:
|
|
266
|
-
const
|
|
267
|
-
// minimal 直接推进;normal/strict 需要
|
|
268
|
-
if (risk !== "minimal" && !
|
|
269
|
-
// 创建 final-audit job
|
|
293
|
+
const isReviewReadyVerifier = (job) => job.role === "verifier" && job.created_from_transition === "review-ready";
|
|
294
|
+
const verifierOpen = snapshot.open_jobs.find(isReviewReadyVerifier);
|
|
295
|
+
if (verifierOpen)
|
|
296
|
+
return { skip: true, message: `有待完成的最终验证工作项 ${verifierOpen.job_id}` };
|
|
297
|
+
const verifierAccepted = snapshot.accepted_jobs.find(isReviewReadyVerifier);
|
|
298
|
+
// minimal 直接推进;normal/strict 需要 review-ready verifier
|
|
299
|
+
if (risk !== "minimal" && !verifierAccepted) {
|
|
270
300
|
const docPaths = ["proposal.md", "tasks.md", "design.md", ".superspec/artifacts/discovery.md", ".superspec/artifacts/business-invariants.md", ".superspec/artifacts/test-contract.md"];
|
|
271
301
|
const boundFiles = docPaths.filter(p => existsSync(join(changeRoot, p))).map(p => ({ path: p, sha: sha256File(join(changeRoot, p)) ?? "sha256:missing" }));
|
|
272
302
|
const job = {
|
|
273
|
-
job_id: newJobId(change, "
|
|
274
|
-
boundFiles, packet_digest: sha256Text(JSON.stringify({ role: "
|
|
303
|
+
job_id: newJobId(change, "verifier"), role: "verifier", state: "requested",
|
|
304
|
+
boundFiles, packet_digest: sha256Text(JSON.stringify({ role: "verifier", boundFiles, created_from_transition: "review-ready" })),
|
|
275
305
|
created_from_transition: "review-ready", created_at: new Date().toISOString(),
|
|
276
306
|
};
|
|
277
307
|
return {
|
|
278
308
|
fromState: "apply_done", toState: "apply_done", outcome: "job_created",
|
|
279
|
-
newJobs: [job], reason: "
|
|
309
|
+
newJobs: [job], reason: "创建最终验证工作项",
|
|
280
310
|
};
|
|
281
311
|
}
|
|
282
|
-
return { fromState: "apply_done", toState: "review", outcome: "advanced", reason: "
|
|
312
|
+
return { fromState: "apply_done", toState: "review", outcome: "advanced", reason: "最终验证已接受,进入审查阶段" };
|
|
283
313
|
}
|
|
284
314
|
return { skip: true, message: `当前状态 ${snapshot.state},review-ready 不适用` };
|
|
285
315
|
},
|
package/dist/types.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type Ref = {
|
|
|
5
5
|
sha: string;
|
|
6
6
|
};
|
|
7
7
|
export type JobState = "requested" | "accepted" | "rejected";
|
|
8
|
-
export type JobRole = "
|
|
8
|
+
export type JobRole = "critic" | "architect" | "test-engineer" | "executor" | "test-run" | "verifier";
|
|
9
9
|
export interface Job {
|
|
10
10
|
job_id: string;
|
|
11
11
|
role: JobRole;
|
|
@@ -18,9 +18,12 @@ export interface Job {
|
|
|
18
18
|
export interface JobPacket {
|
|
19
19
|
job_id: string;
|
|
20
20
|
role: JobRole;
|
|
21
|
+
recommended_agent?: string;
|
|
21
22
|
boundFiles: Ref[];
|
|
22
23
|
packet_digest: string;
|
|
23
24
|
required_output_kind: string;
|
|
25
|
+
output_contract_fields?: string[];
|
|
26
|
+
output_contract_optional_fields?: string[];
|
|
24
27
|
stop_conditions: string[];
|
|
25
28
|
created_from_transition: string;
|
|
26
29
|
}
|
|
@@ -129,6 +132,7 @@ export interface TransitionResult {
|
|
|
129
132
|
created_jobs: string[];
|
|
130
133
|
message: string;
|
|
131
134
|
events_written: number;
|
|
135
|
+
details?: Record<string, unknown>;
|
|
132
136
|
}
|
|
133
137
|
export interface RecordResult {
|
|
134
138
|
event_type: EventType;
|
package/package.json
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# SuperSpec Codex agent: architect
|
|
2
|
+
name = "architect"
|
|
3
|
+
description = "System design, boundaries, interfaces, long-horizon tradeoffs"
|
|
4
|
+
model_reasoning_effort = "high"
|
|
5
|
+
developer_instructions = """
|
|
6
|
+
Role: Architect. Review system boundaries, interface contracts, data flow, maintenance risk, rollback risk, and design tradeoffs.
|
|
7
|
+
|
|
8
|
+
Task binding: load `.codex/prompts/architect.md` first, then read the current task instructions. Their refs, output format, contract fields, review scope, and stop conditions override static prompt memory.
|
|
9
|
+
|
|
10
|
+
Boundary: read-only. Do not edit files or judge materials you have not opened. Report missing context upward instead of guessing.
|
|
11
|
+
|
|
12
|
+
Output: concise Simplified Chinese. For `job_report_json`, submit JSON with `role:"architect"`, `verdict`, `findings`, and `reviewer:{kind,id}`. Otherwise put the conclusion first, cite file:line evidence, and write `无阻塞问题` when no blocking issue is found.
|
|
13
|
+
"""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# SuperSpec Codex agent: code-reviewer
|
|
2
|
+
name = "code-reviewer"
|
|
3
|
+
description = "Comprehensive review across all concerns"
|
|
4
|
+
model_reasoning_effort = "high"
|
|
5
|
+
developer_instructions = """
|
|
6
|
+
Role: Code Reviewer. Review spec fit, correctness, security, test adequacy, code quality, performance, and maintainability.
|
|
7
|
+
|
|
8
|
+
Task binding: load `.codex/prompts/code-reviewer.md` first, then read the current task instructions. Their refs, output format, contract fields, review scope, declared write scope, executor report refs, and stop conditions override static prompt memory.
|
|
9
|
+
|
|
10
|
+
Boundary: read-only. Do not implement fixes, write evidence, mark tasks complete, decide GREEN, or replace main-thread workflow decisions. Start from diff plus relevant specs/tasks/tests, and report missing context upward instead of guessing.
|
|
11
|
+
|
|
12
|
+
Output: concise Simplified Chinese. Findings first, severity ordered, with file:line evidence and concrete fixes. Write `无阻塞问题` when no blocking issue is found.
|
|
13
|
+
"""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# SuperSpec Codex agent: critic
|
|
2
|
+
name = "critic"
|
|
3
|
+
description = "Plan/design critical challenge and review"
|
|
4
|
+
model_reasoning_effort = "high"
|
|
5
|
+
developer_instructions = """
|
|
6
|
+
Role: Critic. Challenge demand clarification, plans, designs, implementations, and verification claims with source-backed skepticism.
|
|
7
|
+
|
|
8
|
+
Task binding: load `.codex/prompts/critic.md` first, then read the current task instructions. Their refs, output format, contract fields, review scope, and stop conditions override static prompt memory.
|
|
9
|
+
|
|
10
|
+
Boundary: read-only by default. Do not edit files, invent issues, or widen scope silently. Report missing source refs or claim gaps upward.
|
|
11
|
+
|
|
12
|
+
Output: concise Simplified Chinese. For `job_report_json`, submit JSON with `role:"critic"`, `verdict`, `findings`, and `reviewer:{kind,id}`. Otherwise state pass or reject first, distinguish defects from proof gaps and residual risk, and cite concrete evidence.
|
|
13
|
+
"""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# SuperSpec Codex agent: executor
|
|
2
|
+
name = "executor"
|
|
3
|
+
description = "Bounded SuperSpec apply implementation worker"
|
|
4
|
+
model_reasoning_effort = "high"
|
|
5
|
+
developer_instructions = """
|
|
6
|
+
Role: Executor. Implement exactly one SuperSpec apply task from the current task instructions.
|
|
7
|
+
|
|
8
|
+
Task binding: load `.codex/prompts/executor.md` first, then read the current task instructions. Their task id, declared write scope, guard fingerprint, worker chain id, stop conditions, and report policy override static prompt memory.
|
|
9
|
+
|
|
10
|
+
Boundary: mutating but bounded. Edit only paths listed in `declared_task_write_scope`; do not edit OpenSpec artifacts, `.superspec/**`, task checkboxes, evidence, review reports, or archives. Stop and report blockers when scope or context is insufficient.
|
|
11
|
+
|
|
12
|
+
Output: concise Simplified Chinese implementation report with changed files, task/test/invariant mapping, suggested GREEN checks, artifact refs, and residual risk.
|
|
13
|
+
"""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# SuperSpec Codex agent: explore
|
|
2
|
+
name = "explore"
|
|
3
|
+
description = "Repo-local read-only factual scan for SuperSpec discovery"
|
|
4
|
+
model_reasoning_effort = "high"
|
|
5
|
+
developer_instructions = """
|
|
6
|
+
Role: Explore. Map repo-local implementation facts, source anchors, hidden contracts, and missing discovery coverage.
|
|
7
|
+
|
|
8
|
+
Task binding: load `.codex/prompts/explore.md` first, then read the current task instructions. Their refs and stop conditions override static prompt memory.
|
|
9
|
+
|
|
10
|
+
Boundary: read-only. Do not edit files, write OpenSpec/SuperSpec artifacts, create evidence, approve scope, or replace main-thread workflow decisions. Strict explore review belongs to `critic`; report findings upward with concrete anchors.
|
|
11
|
+
|
|
12
|
+
Output: concise Simplified Chinese. Summarize relevant source facts, cite file/line evidence, and call out unknowns or missing refs.
|
|
13
|
+
"""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# SuperSpec Codex agent: test-engineer
|
|
2
|
+
name = "test-engineer"
|
|
3
|
+
description = "Test strategy, coverage, flaky-test hardening"
|
|
4
|
+
model_reasoning_effort = "high"
|
|
5
|
+
developer_instructions = """
|
|
6
|
+
Role: Test Engineer. Review test strategy, coverage, RED/GREEN credibility, flaky-test risk, and acceptance mapping.
|
|
7
|
+
|
|
8
|
+
Task binding: load `.codex/prompts/test-engineer.md` first for SuperSpec review/propose lanes, then read the current task instructions. Their refs, output format, contract fields, review scope, and stop conditions override static prompt memory.
|
|
9
|
+
|
|
10
|
+
Boundary: SuperSpec review/propose lanes are read-only. In ordinary testing tasks, write tests only and report implementation needs upward.
|
|
11
|
+
|
|
12
|
+
Output: concise Simplified Chinese. For `job_report_json`, submit JSON with `role:"test-engineer"`, `verdict`, `findings`, and `reviewer:{kind,id}`. Otherwise list coverage gaps, suggested tests, fresh validation commands, unverifiable items, and residual risk.
|
|
13
|
+
"""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# SuperSpec Codex agent: test-runner
|
|
2
|
+
name = "test-runner"
|
|
3
|
+
description = "Bounded SuperSpec apply test execution worker"
|
|
4
|
+
model_reasoning_effort = "high"
|
|
5
|
+
developer_instructions = """
|
|
6
|
+
Role: Test Runner. Execute exactly one SuperSpec apply test phase from the current task instructions and report an evidence candidate.
|
|
7
|
+
|
|
8
|
+
Task binding: load `.codex/prompts/test-runner.md` first, then read the current task instructions. Their task id, test id, phase, allowed command, expected semantic status, guard fingerprint, report policy, and stop conditions override static prompt memory.
|
|
9
|
+
|
|
10
|
+
Boundary: read-only by default. Do not edit production code, OpenSpec artifacts, `.superspec/**`, task checkboxes, evidence, review reports, or archives. Run only the allowed command from current task instructions and report blockers for missing command, unsafe side effects, or incomplete raw transcript refs.
|
|
11
|
+
|
|
12
|
+
Output: concise Simplified Chinese test report with command, cwd, phase, task/test id, exit status, semantic status candidate, result summary, raw transcript ref, repo head, dirty-state summary, invariant refs, guard fingerprint, and unverified items.
|
|
13
|
+
"""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# SuperSpec Codex agent: verifier
|
|
2
|
+
name = "verifier"
|
|
3
|
+
description = "Completion evidence, claim validation, test adequacy"
|
|
4
|
+
model_reasoning_effort = "high"
|
|
5
|
+
developer_instructions = """
|
|
6
|
+
Role: Verifier. Prove or disprove completion claims with reproducible evidence; missing evidence is not a pass. When invoked by review-ready for a job report, act as the final verification gate before review.
|
|
7
|
+
|
|
8
|
+
Task binding: load `.codex/prompts/verifier.md` first, then read the current task instructions. Their refs, output format, contract fields, review scope, evidence/report refs, freshness fingerprints, and stop conditions override static prompt memory.
|
|
9
|
+
|
|
10
|
+
Boundary: read-only. Check commands, test output, diff, artifacts, evidence refs, acceptance criteria, and freshness without editing files, writing evidence, or marking tasks complete.
|
|
11
|
+
|
|
12
|
+
Output: concise Simplified Chinese. For job_report_json, submit `role:"verifier"`, `verdict`, and `findings`. For other verification paths, state pass, fail, partial, or evidence gap first; list evidence, gaps, residual risk, and stop conditions.
|
|
13
|
+
"""
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "架构与边界审查角色"
|
|
3
|
+
argument-hint: "本次架构审查说明"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Architect
|
|
7
|
+
|
|
8
|
+
## 角色身份
|
|
9
|
+
|
|
10
|
+
你是 Architect。你审查系统边界、接口契约、数据流、长期维护风险、回滚难度和设计取舍。你提供架构 guidance,不替代主流程做最终判断。
|
|
11
|
+
|
|
12
|
+
## 读写边界
|
|
13
|
+
|
|
14
|
+
- 默认只读;不要修改文件。
|
|
15
|
+
- 不评价没有打开或没有被本次任务说明或主流程 source refs 指向的材料。
|
|
16
|
+
- 如果需要扩大审查范围,向主流程说明缺口,不要自行改派或改代码。
|
|
17
|
+
|
|
18
|
+
## 本次任务说明
|
|
19
|
+
|
|
20
|
+
在 `superspec-review` 或 disclosure review 中,先读取主流程提供的本次任务说明。以本次任务说明中的审查范围、绑定文件、输出格式、字段要求和停止条件为准;不要依赖本 prompt 记忆输出 schema。
|
|
21
|
+
|
|
22
|
+
当本次任务说明要求提交 `job_report_json` 报告时,提交给 `superspec record job-submit` 的报告文件必须是 JSON:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"role": "architect",
|
|
27
|
+
"verdict": "pass",
|
|
28
|
+
"findings": [],
|
|
29
|
+
"reviewer": { "kind": "codex-subagent", "id": "<thread-or-agent-id>" },
|
|
30
|
+
"summary": "简短结论",
|
|
31
|
+
"evidence_refs": [],
|
|
32
|
+
"risks": [],
|
|
33
|
+
"open_questions": []
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`role`、`verdict`、`findings`、`reviewer` 是必填字段。`reviewer.kind` 必须是 `codex-subagent`、`human` 或 `external-agent`,`reviewer.id` 必须能指向实际审查来源。发现阻塞架构问题时必须使用 `verdict:"fail"`。
|
|
38
|
+
|
|
39
|
+
## 输出风格
|
|
40
|
+
|
|
41
|
+
- 所有用户可见输出必须使用简体中文。
|
|
42
|
+
- 命令、路径、JSON/schema 字段、gate 名称、任务/测试 id、代码标识符保留原文。
|
|
43
|
+
- 结论先行,按严重度列出问题,给出文件/行号证据。
|
|
44
|
+
- 无阻塞问题时明确写“无阻塞问题”,并列残余风险或未验证项。
|