@peterxiaoyang/superspec 0.1.24 → 0.1.26
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 +10 -2
- package/dist/cli.js +75 -17
- package/dist/install.d.ts +1 -0
- package/dist/install.js +37 -1
- package/dist/job_action.d.ts +6 -0
- package/dist/job_action.js +21 -0
- package/dist/next.js +37 -48
- package/dist/record.d.ts +4 -0
- package/dist/record.js +180 -152
- package/dist/task.d.ts +5 -0
- package/dist/task.js +38 -28
- package/dist/transition.d.ts +11 -4
- package/dist/transition.js +19 -2
- package/dist/types.d.ts +13 -6
- package/package.json +1 -1
- package/templates/workflow/AGENTS.md +11 -0
- package/templates/workflow/prompts/architect.md +1 -1
- package/templates/workflow/prompts/critic.md +1 -1
- package/templates/workflow/prompts/test-engineer.md +1 -1
- package/templates/workflow/skills/superspec-apply/SKILL.md +4 -2
- package/templates/workflow/skills/superspec-archive/SKILL.md +2 -0
- package/templates/workflow/skills/superspec-explore/SKILL.md +12 -10
- package/templates/workflow/skills/superspec-propose/SKILL.md +7 -3
- package/templates/workflow/skills/superspec-review/SKILL.md +3 -1
package/dist/record.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// SuperSpec 流程引擎 — record:工作项结果登记
|
|
2
2
|
import { readFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { ensureChangeLayout, readEvents, appendEvent, makeEvent, sha256File, withLock, appendRawRecord, } from "./store.js";
|
|
4
|
+
import { ensureChangeLayout, readEvents, appendEvent, makeEvent, sha256File, sha256Text, withLock, appendRawRecord, } from "./store.js";
|
|
5
5
|
import { reviewEvidenceDigest, reviewVerifierStaleReason } from "./review.js";
|
|
6
|
+
import { jobSubmitArgv } from "./job_action.js";
|
|
6
7
|
const REVIEW_REPORT_REQUIRED_FIELDS = ["role", "verdict", "findings"];
|
|
7
8
|
const REVIEW_REPORT_OPTIONAL_FIELDS = ["summary", "evidence_refs", "risks", "open_questions"];
|
|
8
9
|
const REVIEWER_KINDS = new Set(["codex-subagent", "human", "external-agent"]);
|
|
@@ -57,142 +58,194 @@ function jobTerminalState(events, jobId) {
|
|
|
57
58
|
}
|
|
58
59
|
return null;
|
|
59
60
|
}
|
|
61
|
+
function terminalJobSubmitResult(events, jobId, terminal, reportDigest) {
|
|
62
|
+
const existing = events.find(e => (e.event_type === "job_accepted" || e.event_type === "job_rejected")
|
|
63
|
+
&& e.payload.job_id === jobId
|
|
64
|
+
&& e.payload.report_digest === reportDigest);
|
|
65
|
+
if (existing) {
|
|
66
|
+
return {
|
|
67
|
+
event_type: existing.event_type,
|
|
68
|
+
accepted: existing.event_type === "job_accepted",
|
|
69
|
+
message: "幂等返回:同 report 已提交",
|
|
70
|
+
job_state: existing.event_type === "job_accepted" ? "accepted" : "rejected",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
event_type: "job_rejected",
|
|
75
|
+
accepted: false,
|
|
76
|
+
message: `工作项 ${jobId} 已终态(${terminal}),不接受新报告。需要新工作项请重跑 transition。`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, events, reportContent, reportDigest) {
|
|
80
|
+
const checks = [];
|
|
81
|
+
let parsedReport = null;
|
|
82
|
+
try {
|
|
83
|
+
const report = JSON.parse(reportContent);
|
|
84
|
+
if (!report || typeof report !== "object" || Array.isArray(report)) {
|
|
85
|
+
checks.push("报告必须是 JSON object");
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
parsedReport = report;
|
|
89
|
+
const obj = parsedReport;
|
|
90
|
+
for (const field of REVIEW_REPORT_REQUIRED_FIELDS) {
|
|
91
|
+
if (!(field in obj))
|
|
92
|
+
checks.push(`报告缺少必填字段 ${field}`);
|
|
93
|
+
}
|
|
94
|
+
if (obj.role !== job.role) {
|
|
95
|
+
checks.push(`报告角色 ${String(obj.role)} 与工作项角色 ${job.role} 不匹配`);
|
|
96
|
+
}
|
|
97
|
+
if (obj.verdict !== "pass" && obj.verdict !== "fail") {
|
|
98
|
+
checks.push("报告 verdict 必须是 pass 或 fail");
|
|
99
|
+
}
|
|
100
|
+
if (!Array.isArray(obj.findings)) {
|
|
101
|
+
checks.push("报告 findings 必须是数组");
|
|
102
|
+
}
|
|
103
|
+
if (obj.verdict === "fail") {
|
|
104
|
+
checks.push("报告 verdict=fail,工作项未通过");
|
|
105
|
+
}
|
|
106
|
+
if (requiresReviewer(job.role)) {
|
|
107
|
+
if (!("reviewer" in obj))
|
|
108
|
+
checks.push("报告缺少必填字段 reviewer");
|
|
109
|
+
const reviewer = obj.reviewer;
|
|
110
|
+
if (!reviewer || typeof reviewer !== "object" || Array.isArray(reviewer)) {
|
|
111
|
+
checks.push("报告 reviewer 必须是包含 kind/id 的对象");
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
if (typeof reviewer.kind !== "string" || !REVIEWER_KINDS.has(reviewer.kind)) {
|
|
115
|
+
checks.push(`报告 reviewer.kind 必须是 ${[...REVIEWER_KINDS].join("|")} 之一`);
|
|
116
|
+
}
|
|
117
|
+
if (typeof reviewer.id !== "string" || reviewer.id.trim() === "") {
|
|
118
|
+
checks.push("报告 reviewer.id 必须是非空字符串");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
checks.push("报告必须是有效 JSON");
|
|
126
|
+
}
|
|
127
|
+
for (const bf of job.boundFiles) {
|
|
128
|
+
const currentSha = sha256File(join(changeRoot, bf.path)) ?? "sha256:missing";
|
|
129
|
+
if (currentSha !== bf.sha) {
|
|
130
|
+
checks.push(`绑定文件 ${bf.path} 已变化(${bf.sha} → ${currentSha})`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const reviewStaleReason = reviewVerifierStaleReason(job, changeRoot, reviewEvidenceDigest(events));
|
|
134
|
+
if (reviewStaleReason && !checks.includes(reviewStaleReason)) {
|
|
135
|
+
checks.push(reviewStaleReason);
|
|
136
|
+
}
|
|
137
|
+
if (!reportContent.trim()) {
|
|
138
|
+
checks.push("报告内容为空");
|
|
139
|
+
}
|
|
140
|
+
if (checks.length > 0) {
|
|
141
|
+
const rejectEvent = makeEvent(change, "job_rejected", {
|
|
142
|
+
job_id: jobId,
|
|
143
|
+
role: job.role,
|
|
144
|
+
report_digest: reportDigest,
|
|
145
|
+
reason: checks.join("; "),
|
|
146
|
+
});
|
|
147
|
+
appendEvent(projectRoot, change, rejectEvent);
|
|
148
|
+
return {
|
|
149
|
+
event_type: "job_rejected",
|
|
150
|
+
accepted: false,
|
|
151
|
+
message: `工作项 ${jobId} 被拒绝:${checks.join("; ")}`,
|
|
152
|
+
job_state: "rejected",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const rawRef = appendRawRecord(projectRoot, change, "review-reports", parsedReport);
|
|
156
|
+
const acceptEvent = makeEvent(change, "job_accepted", {
|
|
157
|
+
job_id: jobId,
|
|
158
|
+
role: job.role,
|
|
159
|
+
report_digest: reportDigest,
|
|
160
|
+
accepted_at: new Date().toISOString(),
|
|
161
|
+
...rawRef,
|
|
162
|
+
});
|
|
163
|
+
appendEvent(projectRoot, change, acceptEvent);
|
|
164
|
+
return {
|
|
165
|
+
event_type: "job_accepted",
|
|
166
|
+
accepted: true,
|
|
167
|
+
message: `工作项 ${jobId}(${job.role})已接受`,
|
|
168
|
+
job_state: "accepted",
|
|
169
|
+
};
|
|
170
|
+
}
|
|
60
171
|
/** record job-submit:登记工作项结果 */
|
|
61
172
|
export function recordJobSubmit(projectRoot, change, changeRoot, jobId, reportFile) {
|
|
62
173
|
return withLock(projectRoot, change, () => {
|
|
63
174
|
ensureChangeLayout(projectRoot, change);
|
|
64
175
|
const events = readEvents(projectRoot, change);
|
|
65
|
-
// 查找 job
|
|
66
176
|
const job = findJob(events, jobId);
|
|
67
177
|
if (!job) {
|
|
68
178
|
return { event_type: "job_rejected", accepted: false, message: `工作项 ${jobId} 不存在` };
|
|
69
179
|
}
|
|
70
|
-
// 检查终态
|
|
71
180
|
const terminal = jobTerminalState(events, jobId);
|
|
72
181
|
if (terminal) {
|
|
73
|
-
// 幂等检查:同 report_digest → 返回旧结果
|
|
74
182
|
const reportDigest = sha256File(reportFile) ?? "sha256:unknown";
|
|
75
|
-
|
|
76
|
-
&& e.payload.job_id === jobId
|
|
77
|
-
&& e.payload.report_digest === reportDigest);
|
|
78
|
-
if (existing) {
|
|
79
|
-
return {
|
|
80
|
-
event_type: existing.event_type,
|
|
81
|
-
accepted: existing.event_type === "job_accepted",
|
|
82
|
-
message: "幂等返回:同 report 已提交",
|
|
83
|
-
job_state: existing.event_type === "job_accepted" ? "accepted" : "rejected",
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
// 终态 job + 不同 report → 拒绝
|
|
87
|
-
return {
|
|
88
|
-
event_type: "job_rejected",
|
|
89
|
-
accepted: false,
|
|
90
|
-
message: `工作项 ${jobId} 已终态(${terminal}),不接受新报告。需要新工作项请重跑 transition。`,
|
|
91
|
-
};
|
|
183
|
+
return terminalJobSubmitResult(events, jobId, terminal, reportDigest);
|
|
92
184
|
}
|
|
93
|
-
// 读报告
|
|
94
185
|
if (!existsSync(reportFile)) {
|
|
95
186
|
return { event_type: "job_rejected", accepted: false, message: `报告文件不存在:${reportFile}` };
|
|
96
187
|
}
|
|
97
188
|
const reportContent = readFileSync(reportFile, "utf8");
|
|
98
189
|
const reportDigest = sha256File(reportFile) ?? "sha256:unknown";
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const obj = parsedReport;
|
|
111
|
-
for (const field of REVIEW_REPORT_REQUIRED_FIELDS) {
|
|
112
|
-
if (!(field in obj))
|
|
113
|
-
checks.push(`报告缺少必填字段 ${field}`);
|
|
114
|
-
}
|
|
115
|
-
if (obj.role !== job.role) {
|
|
116
|
-
checks.push(`报告角色 ${String(obj.role)} 与工作项角色 ${job.role} 不匹配`);
|
|
117
|
-
}
|
|
118
|
-
if (obj.verdict !== "pass" && obj.verdict !== "fail") {
|
|
119
|
-
checks.push("报告 verdict 必须是 pass 或 fail");
|
|
120
|
-
}
|
|
121
|
-
if (!Array.isArray(obj.findings)) {
|
|
122
|
-
checks.push("报告 findings 必须是数组");
|
|
123
|
-
}
|
|
124
|
-
if (obj.verdict === "fail") {
|
|
125
|
-
checks.push("报告 verdict=fail,工作项未通过");
|
|
126
|
-
}
|
|
127
|
-
if (requiresReviewer(job.role)) {
|
|
128
|
-
if (!("reviewer" in obj))
|
|
129
|
-
checks.push("报告缺少必填字段 reviewer");
|
|
130
|
-
const reviewer = obj.reviewer;
|
|
131
|
-
if (!reviewer || typeof reviewer !== "object" || Array.isArray(reviewer)) {
|
|
132
|
-
checks.push("报告 reviewer 必须是包含 kind/id 的对象");
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
if (typeof reviewer.kind !== "string" || !REVIEWER_KINDS.has(reviewer.kind)) {
|
|
136
|
-
checks.push(`报告 reviewer.kind 必须是 ${[...REVIEWER_KINDS].join("|")} 之一`);
|
|
137
|
-
}
|
|
138
|
-
if (typeof reviewer.id !== "string" || reviewer.id.trim() === "") {
|
|
139
|
-
checks.push("报告 reviewer.id 必须是非空字符串");
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
checks.push("报告必须是有效 JSON");
|
|
147
|
-
}
|
|
148
|
-
// 1. boundFiles 仍匹配当前文档(missing 也算不匹配)
|
|
149
|
-
for (const bf of job.boundFiles) {
|
|
150
|
-
const currentSha = sha256File(join(changeRoot, bf.path)) ?? "sha256:missing";
|
|
151
|
-
if (currentSha !== bf.sha) {
|
|
152
|
-
checks.push(`绑定文件 ${bf.path} 已变化(${bf.sha} → ${currentSha})`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
const reviewStaleReason = reviewVerifierStaleReason(job, changeRoot, reviewEvidenceDigest(events));
|
|
156
|
-
if (reviewStaleReason && !checks.includes(reviewStaleReason)) {
|
|
157
|
-
checks.push(reviewStaleReason);
|
|
158
|
-
}
|
|
159
|
-
// 2. 报告格式基本校验(非空 JSON 或文本)
|
|
160
|
-
if (!reportContent.trim()) {
|
|
161
|
-
checks.push("报告内容为空");
|
|
190
|
+
return recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, events, reportContent, reportDigest);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/** record job-submit:从 JSON 内容登记工作项结果 */
|
|
194
|
+
export function recordJobSubmitContent(projectRoot, change, changeRoot, jobId, reportContent) {
|
|
195
|
+
return withLock(projectRoot, change, () => {
|
|
196
|
+
ensureChangeLayout(projectRoot, change);
|
|
197
|
+
const events = readEvents(projectRoot, change);
|
|
198
|
+
const job = findJob(events, jobId);
|
|
199
|
+
if (!job) {
|
|
200
|
+
return { event_type: "job_rejected", accepted: false, message: `工作项 ${jobId} 不存在` };
|
|
162
201
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
role: job.role,
|
|
168
|
-
report_digest: reportDigest,
|
|
169
|
-
reason: checks.join("; "),
|
|
170
|
-
});
|
|
171
|
-
appendEvent(projectRoot, change, rejectEvent);
|
|
172
|
-
return {
|
|
173
|
-
event_type: "job_rejected",
|
|
174
|
-
accepted: false,
|
|
175
|
-
message: `工作项 ${jobId} 被拒绝:${checks.join("; ")}`,
|
|
176
|
-
job_state: "rejected",
|
|
177
|
-
};
|
|
202
|
+
const reportDigest = sha256Text(reportContent);
|
|
203
|
+
const terminal = jobTerminalState(events, jobId);
|
|
204
|
+
if (terminal) {
|
|
205
|
+
return terminalJobSubmitResult(events, jobId, terminal, reportDigest);
|
|
178
206
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
appendEvent(projectRoot, change,
|
|
207
|
+
return recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, events, reportContent, reportDigest);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
function recordUserDecisionLoaded(projectRoot, change, events, content, inputDigest) {
|
|
211
|
+
let decision;
|
|
212
|
+
try {
|
|
213
|
+
decision = JSON.parse(content);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", { accepted: false, reason: "invalid_json" }));
|
|
217
|
+
return { event_type: "user_decision_recorded", accepted: false, message: "决策文件不是有效 JSON" };
|
|
218
|
+
}
|
|
219
|
+
if (!decision.scope || !decision.answer) {
|
|
220
|
+
appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", { accepted: false, reason: "missing_scope_or_answer" }));
|
|
221
|
+
return { event_type: "user_decision_recorded", accepted: false, message: "决策文件缺少 scope 或 answer" };
|
|
222
|
+
}
|
|
223
|
+
const existing = events.find(e => e.event_type === "user_decision_recorded"
|
|
224
|
+
&& e.payload.input_digest === inputDigest);
|
|
225
|
+
if (existing) {
|
|
189
226
|
return {
|
|
190
|
-
event_type: "
|
|
227
|
+
event_type: "user_decision_recorded",
|
|
191
228
|
accepted: true,
|
|
192
|
-
message:
|
|
193
|
-
job_state: "accepted",
|
|
229
|
+
message: "幂等返回:同 user decision 已登记",
|
|
194
230
|
};
|
|
231
|
+
}
|
|
232
|
+
const normalizedDecision = {
|
|
233
|
+
scope: decision.scope,
|
|
234
|
+
question: decision.question ?? "",
|
|
235
|
+
answer: decision.answer,
|
|
236
|
+
};
|
|
237
|
+
const rawRef = appendRawRecord(projectRoot, change, "user-decisions", normalizedDecision);
|
|
238
|
+
const event = makeEvent(change, "user_decision_recorded", {
|
|
239
|
+
...normalizedDecision,
|
|
240
|
+
input_digest: inputDigest,
|
|
241
|
+
...rawRef,
|
|
195
242
|
});
|
|
243
|
+
appendEvent(projectRoot, change, event);
|
|
244
|
+
return {
|
|
245
|
+
event_type: "user_decision_recorded",
|
|
246
|
+
accepted: true,
|
|
247
|
+
message: `用户决策已登记:scope=${decision.scope}`,
|
|
248
|
+
};
|
|
196
249
|
}
|
|
197
250
|
/** record user-decision:登记用户决策 */
|
|
198
251
|
export function recordUserDecision(projectRoot, change, inputFile) {
|
|
@@ -204,45 +257,16 @@ export function recordUserDecision(projectRoot, change, inputFile) {
|
|
|
204
257
|
return { event_type: "user_decision_recorded", accepted: false, message: `决策文件不存在:${inputFile}` };
|
|
205
258
|
}
|
|
206
259
|
const content = readFileSync(inputFile, "utf8");
|
|
207
|
-
let decision;
|
|
208
|
-
try {
|
|
209
|
-
decision = JSON.parse(content);
|
|
210
|
-
}
|
|
211
|
-
catch {
|
|
212
|
-
appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", { accepted: false, reason: "invalid_json" }));
|
|
213
|
-
return { event_type: "user_decision_recorded", accepted: false, message: "决策文件不是有效 JSON" };
|
|
214
|
-
}
|
|
215
|
-
if (!decision.scope || !decision.answer) {
|
|
216
|
-
appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", { accepted: false, reason: "missing_scope_or_answer" }));
|
|
217
|
-
return { event_type: "user_decision_recorded", accepted: false, message: "决策文件缺少 scope 或 answer" };
|
|
218
|
-
}
|
|
219
260
|
const inputDigest = sha256File(inputFile) ?? "sha256:unknown";
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const normalizedDecision = {
|
|
230
|
-
scope: decision.scope,
|
|
231
|
-
question: decision.question ?? "",
|
|
232
|
-
answer: decision.answer,
|
|
233
|
-
};
|
|
234
|
-
const rawRef = appendRawRecord(projectRoot, change, "user-decisions", normalizedDecision);
|
|
235
|
-
const event = makeEvent(change, "user_decision_recorded", {
|
|
236
|
-
...normalizedDecision,
|
|
237
|
-
input_digest: inputDigest,
|
|
238
|
-
...rawRef,
|
|
239
|
-
});
|
|
240
|
-
appendEvent(projectRoot, change, event);
|
|
241
|
-
return {
|
|
242
|
-
event_type: "user_decision_recorded",
|
|
243
|
-
accepted: true,
|
|
244
|
-
message: `用户决策已登记:scope=${decision.scope}`,
|
|
245
|
-
};
|
|
261
|
+
return recordUserDecisionLoaded(projectRoot, change, events, content, inputDigest);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
/** record user-decision:从 JSON 内容登记用户决策 */
|
|
265
|
+
export function recordUserDecisionContent(projectRoot, change, content) {
|
|
266
|
+
return withLock(projectRoot, change, () => {
|
|
267
|
+
ensureChangeLayout(projectRoot, change);
|
|
268
|
+
const events = readEvents(projectRoot, change);
|
|
269
|
+
return recordUserDecisionLoaded(projectRoot, change, events, content, sha256Text(content));
|
|
246
270
|
});
|
|
247
271
|
}
|
|
248
272
|
/** jobs list(HIGH-1 修复:从 transition_commit.new_jobs 提取,不再依赖已删除的 job_requested 事件) */
|
|
@@ -296,12 +320,16 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
296
320
|
...(job.review_evidence_digest ? { review_evidence_digest: job.review_evidence_digest } : {}),
|
|
297
321
|
packet_digest: job.packet_digest,
|
|
298
322
|
required_output_kind: "job_report_json",
|
|
323
|
+
preferred_input_mode: "stdin",
|
|
324
|
+
submission_command: `superspec record job-submit --change "${change}" --job "${job.job_id}" --report -`,
|
|
325
|
+
submission_argv: jobSubmitArgv(change, job.job_id),
|
|
326
|
+
file_fallback: true,
|
|
299
327
|
output_contract_fields: requiresReviewer(job.role) ? [...REVIEW_REPORT_REQUIRED_FIELDS, "reviewer"] : [...REVIEW_REPORT_REQUIRED_FIELDS],
|
|
300
328
|
output_contract_optional_fields: [...REVIEW_REPORT_OPTIONAL_FIELDS],
|
|
301
329
|
output_instructions: `${roleDescription(job.role)}。请审查 ${job.boundFiles.map(f => f.path).join(", ")},` +
|
|
302
330
|
(job.review_evidence_digest ? `本工作项绑定的执行证据版本为 ${job.review_evidence_digest},` : "") +
|
|
303
331
|
(requiresReviewer(job.role) ? `必须由独立 ${recommendedAgentForRole(job.role)} reviewer 执行并在 reviewer.kind/id 中记录来源,` : "") +
|
|
304
|
-
`产出 JSON
|
|
332
|
+
`产出 JSON 报告内容并优先通过 --report - 从 stdin 登记;文件路径模式仍可作为 fallback。` +
|
|
305
333
|
(requiresReviewer(job.role)
|
|
306
334
|
? `最小格式:{"role":"${job.role}","verdict":"pass|fail","findings":[],"reviewer":{"kind":"codex-subagent","id":"<thread-or-agent-id>"}}`
|
|
307
335
|
: `最小格式:{"role":"${job.role}","verdict":"pass|fail","findings":[]}`),
|
package/dist/task.d.ts
CHANGED
|
@@ -5,3 +5,8 @@ export declare function recordTestRun(projectRoot: string, change: string, input
|
|
|
5
5
|
accepted: boolean;
|
|
6
6
|
message: string;
|
|
7
7
|
};
|
|
8
|
+
/** record test-run:从 JSON 内容登记测试运行记录 */
|
|
9
|
+
export declare function recordTestRunContent(projectRoot: string, change: string, content: string): {
|
|
10
|
+
accepted: boolean;
|
|
11
|
+
message: string;
|
|
12
|
+
};
|
package/dist/task.js
CHANGED
|
@@ -11,39 +11,49 @@ export function tasksStructureDigestOf(changeRoot) {
|
|
|
11
11
|
const content = readFileSync(p, "utf8");
|
|
12
12
|
return formatDigest(content, sha256Text);
|
|
13
13
|
}
|
|
14
|
+
function recordTestRunLoaded(projectRoot, change, content) {
|
|
15
|
+
let tr;
|
|
16
|
+
try {
|
|
17
|
+
tr = JSON.parse(content);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return { accepted: false, message: "无效 JSON" };
|
|
21
|
+
}
|
|
22
|
+
if (!tr.test_id || !tr.task_structure_digest) {
|
|
23
|
+
return { accepted: false, message: "缺少 test_id 或 task_structure_digest" };
|
|
24
|
+
}
|
|
25
|
+
const normalizedTestRun = {
|
|
26
|
+
test_id: tr.test_id,
|
|
27
|
+
task_structure_digest: tr.task_structure_digest,
|
|
28
|
+
attempt_id: tr.attempt_id ?? null,
|
|
29
|
+
command: tr.command ?? "",
|
|
30
|
+
cwd: tr.cwd ?? "",
|
|
31
|
+
exit_code: tr.exit_code ?? -1,
|
|
32
|
+
semantic_status: tr.semantic_status ?? "unknown",
|
|
33
|
+
target_fingerprint: tr.target_fingerprint ?? null,
|
|
34
|
+
raw_log_ref: tr.raw_log_ref ?? null,
|
|
35
|
+
};
|
|
36
|
+
const rawRef = appendRawRecord(projectRoot, change, "test-runs", normalizedTestRun);
|
|
37
|
+
const event = makeEvent(change, "test_run_recorded", {
|
|
38
|
+
...normalizedTestRun,
|
|
39
|
+
...rawRef,
|
|
40
|
+
});
|
|
41
|
+
appendEvent(projectRoot, change, event);
|
|
42
|
+
return { accepted: true, message: `测试运行已登记:test_id=${tr.test_id}` };
|
|
43
|
+
}
|
|
14
44
|
/** record test-run:登记测试运行记录 */
|
|
15
45
|
export function recordTestRun(projectRoot, change, inputFile) {
|
|
16
46
|
return withLock(projectRoot, change, () => {
|
|
17
47
|
ensureChangeLayout(projectRoot, change);
|
|
18
48
|
if (!existsSync(inputFile))
|
|
19
49
|
return { accepted: false, message: `文件不存在:${inputFile}` };
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return { accepted: false, message: "缺少 test_id 或 task_structure_digest" };
|
|
29
|
-
}
|
|
30
|
-
const normalizedTestRun = {
|
|
31
|
-
test_id: tr.test_id,
|
|
32
|
-
task_structure_digest: tr.task_structure_digest,
|
|
33
|
-
attempt_id: tr.attempt_id ?? null,
|
|
34
|
-
command: tr.command ?? "",
|
|
35
|
-
cwd: tr.cwd ?? "",
|
|
36
|
-
exit_code: tr.exit_code ?? -1,
|
|
37
|
-
semantic_status: tr.semantic_status ?? "unknown",
|
|
38
|
-
target_fingerprint: tr.target_fingerprint ?? null,
|
|
39
|
-
raw_log_ref: tr.raw_log_ref ?? null,
|
|
40
|
-
};
|
|
41
|
-
const rawRef = appendRawRecord(projectRoot, change, "test-runs", normalizedTestRun);
|
|
42
|
-
const event = makeEvent(change, "test_run_recorded", {
|
|
43
|
-
...normalizedTestRun,
|
|
44
|
-
...rawRef,
|
|
45
|
-
});
|
|
46
|
-
appendEvent(projectRoot, change, event);
|
|
47
|
-
return { accepted: true, message: `测试运行已登记:test_id=${tr.test_id}` };
|
|
50
|
+
return recordTestRunLoaded(projectRoot, change, readFileSync(inputFile, "utf8"));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** record test-run:从 JSON 内容登记测试运行记录 */
|
|
54
|
+
export function recordTestRunContent(projectRoot, change, content) {
|
|
55
|
+
return withLock(projectRoot, change, () => {
|
|
56
|
+
ensureChangeLayout(projectRoot, change);
|
|
57
|
+
return recordTestRunLoaded(projectRoot, change, content);
|
|
48
58
|
});
|
|
49
59
|
}
|
package/dist/transition.d.ts
CHANGED
|
@@ -13,15 +13,22 @@ interface Decision {
|
|
|
13
13
|
details?: Record<string, unknown>;
|
|
14
14
|
postCommit?: (projectRoot: string, change: string, changeRoot: string) => void;
|
|
15
15
|
}
|
|
16
|
+
interface SkipDecision {
|
|
17
|
+
skip: true;
|
|
18
|
+
message: string;
|
|
19
|
+
}
|
|
20
|
+
interface BlockedDecision {
|
|
21
|
+
blocked: true;
|
|
22
|
+
reason: string;
|
|
23
|
+
jobs: Job[];
|
|
24
|
+
details?: Record<string, unknown>;
|
|
25
|
+
}
|
|
16
26
|
/**
|
|
17
27
|
* 统一 transition 提交协议——所有校验在锁内。
|
|
18
28
|
*/
|
|
19
29
|
export declare function commitTransition(projectRoot: string, change: string, changeRoot: string, opts: {
|
|
20
30
|
name: string;
|
|
21
|
-
decide: (snapshot: Snapshot) => Decision |
|
|
22
|
-
skip: true;
|
|
23
|
-
message: string;
|
|
24
|
-
};
|
|
31
|
+
decide: (snapshot: Snapshot) => Decision | SkipDecision | BlockedDecision;
|
|
25
32
|
idempotencyInputs?: Record<string, unknown>;
|
|
26
33
|
}): TransitionResult;
|
|
27
34
|
export declare function proposeReady(projectRoot: string, change: string, changeRoot: string, risk?: "minimal" | "normal" | "strict"): TransitionResult;
|
package/dist/transition.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
|
|
4
4
|
import { ensureChangeLayout, readEvents, appendEvent, makeEvent, writeSnapshot, snapshotDigest, withLock, idempotencyKey, sha256File, sha256Text, } from "./store.js";
|
|
5
5
|
import { rebuildSnapshot } from "./sync.js";
|
|
6
|
+
import { requiredJobActions } from "./job_action.js";
|
|
6
7
|
import { assertCommitPayloadExtension, isFreshReviewVerifier, isReviewReadyVerifier, readReviewPolicyFromEvents, reviewBoundFiles, reviewEvidenceDigest, reviewPolicyForRisk, } from "./review.js";
|
|
7
8
|
import { validateDiscovery, collectProposeOpenQuestions, findTaskInLines, parseTasksMd, pendingTasksInContent, tasksStructureDigest } from "./format.js";
|
|
8
9
|
let transitionSeq = 0;
|
|
@@ -31,7 +32,7 @@ function checkOrCreateReviewJobs(snapshot, requiredRoles, changeRoot, change, tr
|
|
|
31
32
|
for (const role of requiredRoles) {
|
|
32
33
|
const openForRole = snapshot.open_jobs.find(j => j.role === role && j.created_from_transition === transitionName);
|
|
33
34
|
if (openForRole)
|
|
34
|
-
return {
|
|
35
|
+
return { blocked: true, reason: `状态未推进;已有待完成工作项 ${role}(${openForRole.job_id})`, jobs: [openForRole] };
|
|
35
36
|
const fresh = snapshot.accepted_jobs.find(j => j.role === role && j.created_from_transition === transitionName);
|
|
36
37
|
if (!fresh) {
|
|
37
38
|
staleRoles.push({ role, reason: `需求 ${role} 无已接受的工作项` });
|
|
@@ -114,13 +115,28 @@ export function commitTransition(projectRoot, change, changeRoot, opts) {
|
|
|
114
115
|
const existing = events.find(e => e.idempotency_key === idemKey && e.event_type === "transition_commit");
|
|
115
116
|
if (existing) {
|
|
116
117
|
const p = existing.payload;
|
|
118
|
+
const newJobs = p.new_jobs ?? [];
|
|
117
119
|
return {
|
|
118
120
|
transition: name, outcome: p.outcome,
|
|
119
121
|
from_state: p.from_state, to_state: p.to_state,
|
|
120
122
|
created_jobs: p.created_job_ids ?? [], message: "幂等返回", events_written: 0,
|
|
123
|
+
...(newJobs.length > 0 ? { required_jobs: requiredJobActions(change, newJobs) } : {}),
|
|
121
124
|
};
|
|
122
125
|
}
|
|
123
126
|
const decision = opts.decide(snapshot);
|
|
127
|
+
if ("blocked" in decision) {
|
|
128
|
+
return {
|
|
129
|
+
transition: name,
|
|
130
|
+
outcome: "blocked",
|
|
131
|
+
from_state: snapshot.state,
|
|
132
|
+
to_state: snapshot.state,
|
|
133
|
+
created_jobs: [],
|
|
134
|
+
required_jobs: requiredJobActions(change, decision.jobs),
|
|
135
|
+
message: decision.reason,
|
|
136
|
+
events_written: 0,
|
|
137
|
+
...(decision.details ? { details: decision.details } : {}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
124
140
|
if ("skip" in decision) {
|
|
125
141
|
return {
|
|
126
142
|
transition: name, outcome: "advanced",
|
|
@@ -163,6 +179,7 @@ export function commitTransition(projectRoot, change, changeRoot, opts) {
|
|
|
163
179
|
return {
|
|
164
180
|
transition: name, outcome, from_state: fromState, to_state: toState,
|
|
165
181
|
created_jobs: newJobs.map(j => j.job_id),
|
|
182
|
+
...(newJobs.length > 0 ? { required_jobs: requiredJobActions(change, newJobs) } : {}),
|
|
166
183
|
message: outcome === "advanced" ? `状态推进:${fromState} → ${toState}` : `状态不变(${fromState}),创建了 ${newJobs.length} 个工作项`,
|
|
167
184
|
events_written: 1 + extraEvents.length,
|
|
168
185
|
...(details ? { details } : {}),
|
|
@@ -348,7 +365,7 @@ export function reviewReady(projectRoot, change, changeRoot, risk = "strict") {
|
|
|
348
365
|
if (snapshot.state === "apply_done" || snapshot.state === "review") {
|
|
349
366
|
const verifierOpen = snapshot.open_jobs.find(isReviewReadyVerifier);
|
|
350
367
|
if (verifierOpen)
|
|
351
|
-
return {
|
|
368
|
+
return { blocked: true, reason: `状态未推进;已有待完成最终验证工作项 ${verifierOpen.job_id}`, jobs: [verifierOpen] };
|
|
352
369
|
const verifierAccepted = snapshot.accepted_jobs.find(job => isFreshReviewVerifier(job, changeRoot, currentEvidenceDigest));
|
|
353
370
|
if (!policy.requires_verifier) {
|
|
354
371
|
if (snapshot.state === "apply_done") {
|
package/dist/types.d.ts
CHANGED
|
@@ -24,11 +24,21 @@ export interface JobPacket {
|
|
|
24
24
|
review_evidence_digest?: string;
|
|
25
25
|
packet_digest: string;
|
|
26
26
|
required_output_kind: string;
|
|
27
|
+
preferred_input_mode?: "stdin" | "file";
|
|
28
|
+
submission_command?: string;
|
|
29
|
+
submission_argv?: string[];
|
|
30
|
+
file_fallback?: boolean;
|
|
27
31
|
output_contract_fields?: string[];
|
|
28
32
|
output_contract_optional_fields?: string[];
|
|
29
33
|
stop_conditions: string[];
|
|
30
34
|
created_from_transition: string;
|
|
31
35
|
}
|
|
36
|
+
export interface RequiredJobAction {
|
|
37
|
+
job_id: string;
|
|
38
|
+
role: JobRole;
|
|
39
|
+
packet_command: string;
|
|
40
|
+
packet_argv: string[];
|
|
41
|
+
}
|
|
32
42
|
export type EventType = "transition_prepare" | "transition_commit" | "job_requested" | "job_invalidated" | "reopen" | "abandon" | "task_started" | "task_completed" | "task_abandoned" | "job_accepted" | "job_rejected" | "user_decision_recorded" | "test_run_recorded" | "task_activation_recorded" | "artifact_recorded";
|
|
33
43
|
export interface Event {
|
|
34
44
|
event_id: string;
|
|
@@ -117,11 +127,7 @@ export type NextOutput = {
|
|
|
117
127
|
missing_inputs: MissingInput[];
|
|
118
128
|
} | {
|
|
119
129
|
path: "required_job";
|
|
120
|
-
required_jobs:
|
|
121
|
-
job_id: string;
|
|
122
|
-
role: JobRole;
|
|
123
|
-
packet_command: string;
|
|
124
|
-
}[];
|
|
130
|
+
required_jobs: RequiredJobAction[];
|
|
125
131
|
reason: string;
|
|
126
132
|
} | {
|
|
127
133
|
path: "ask_user";
|
|
@@ -133,10 +139,11 @@ export type NextOutput = {
|
|
|
133
139
|
});
|
|
134
140
|
export interface TransitionResult {
|
|
135
141
|
transition: string;
|
|
136
|
-
outcome: "advanced" | "job_created";
|
|
142
|
+
outcome: "advanced" | "job_created" | "blocked";
|
|
137
143
|
from_state: State;
|
|
138
144
|
to_state: State;
|
|
139
145
|
created_jobs: string[];
|
|
146
|
+
required_jobs?: RequiredJobAction[];
|
|
140
147
|
message: string;
|
|
141
148
|
events_written: number;
|
|
142
149
|
details?: Record<string, unknown>;
|
package/package.json
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!-- SUPERSPEC:AGENTS:START -->
|
|
2
|
+
# SuperSpec
|
|
3
|
+
|
|
4
|
+
本项目启用 SuperSpec。使用 `superspec-*` 工作流时,以 `superspec transition next --change "<change>"` 返回的下一步为准;流程完成前不得跳阶段、不得自称完成。
|
|
5
|
+
|
|
6
|
+
SuperSpec 创建的独立审查/验证工作项,视为已授权启动对应 subagent;无需再次询问用户。主会话不得自批这些工作项。
|
|
7
|
+
|
|
8
|
+
审查/验证工作项只授权处理该工作项绑定的内容,不得扩大范围、跳过阶段或代替后续流程。若当前环境无法启动 subagent,只能使用本地或用户已授权的独立来源;没有可用独立来源时停在当前工作项并说明阻塞,主会话不得因此自批。
|
|
9
|
+
|
|
10
|
+
执行 CLI 返回命令时优先使用 `*_argv` 字段。用户可见回复使用自然语言;除非用户要求调试信息,不复述内部 JSON 字段。
|
|
11
|
+
<!-- SUPERSPEC:AGENTS:END -->
|
|
@@ -19,7 +19,7 @@ argument-hint: "本次架构审查说明"
|
|
|
19
19
|
|
|
20
20
|
在 `superspec-review` 或 disclosure review 中,先读取主流程提供的本次任务说明。以本次任务说明中的审查范围、绑定文件、输出格式、字段要求和停止条件为准;不要依赖本 prompt 记忆输出 schema。
|
|
21
21
|
|
|
22
|
-
当本次任务说明要求提交 `job_report_json` 报告时,提交给 `superspec record job-submit`
|
|
22
|
+
当本次任务说明要求提交 `job_report_json` 报告时,提交给 `superspec record job-submit` 的报告内容必须是 JSON,并优先通过 `--report -` 从 stdin 登记:
|
|
23
23
|
|
|
24
24
|
```json
|
|
25
25
|
{
|
|
@@ -20,7 +20,7 @@ argument-hint: "本次反方审查说明"
|
|
|
20
20
|
|
|
21
21
|
在 `superspec-review` 或 disclosure review 中,先读取主流程提供的本次任务说明。以本次任务说明中的审查范围、绑定文件、输出格式、字段要求和停止条件为准;不要依赖本 prompt 记忆输出 schema。
|
|
22
22
|
|
|
23
|
-
当本次任务说明要求提交 `job_report_json` 报告时,提交给 `superspec record job-submit`
|
|
23
|
+
当本次任务说明要求提交 `job_report_json` 报告时,提交给 `superspec record job-submit` 的报告内容必须是 JSON,并优先通过 `--report -` 从 stdin 登记:
|
|
24
24
|
|
|
25
25
|
```json
|
|
26
26
|
{
|