@peterxiaoyang/superspec 0.1.46 → 0.1.48
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/dist/cli.js +54 -9
- package/dist/code_review.d.ts +11 -1
- package/dist/code_review.js +40 -0
- package/dist/explore_round.d.ts +25 -0
- package/dist/explore_round.js +139 -0
- package/dist/format.d.ts +78 -2
- package/dist/format.js +227 -16
- package/dist/next.d.ts +1 -1
- package/dist/next.js +97 -12
- package/dist/openspec.js +26 -5
- package/dist/phase_confirmation.js +73 -2
- package/dist/phase_plan.d.ts +24 -1
- package/dist/phase_plan.js +194 -34
- package/dist/propose_round.d.ts +15 -0
- package/dist/propose_round.js +137 -0
- package/dist/record.js +313 -57
- package/dist/review.js +2 -0
- package/dist/review_job_gates.d.ts +1 -1
- package/dist/review_job_gates.js +12 -4
- package/dist/skill_loop.js +20 -0
- package/dist/task_evidence.js +5 -3
- package/dist/transition.d.ts +1 -0
- package/dist/transition.js +248 -31
- package/dist/types.d.ts +77 -1
- package/dist/workflow_profile.js +1 -1
- package/package.json +7 -1
- package/templates/workflow/AGENTS.md +17 -5
- package/templates/workflow/prompts/architect.md +4 -2
- package/templates/workflow/prompts/code-reviewer.md +3 -3
- package/templates/workflow/prompts/critic.md +13 -4
- package/templates/workflow/prompts/executor.md +5 -5
- package/templates/workflow/prompts/explore.md +2 -2
- package/templates/workflow/prompts/test-engineer.md +6 -5
- package/templates/workflow/prompts/test-runner.md +5 -5
- package/templates/workflow/prompts/verifier.md +4 -4
- package/templates/workflow/skills/superspec-apply/SKILL.md +26 -12
- package/templates/workflow/skills/superspec-explore/SKILL.md +25 -7
- package/templates/workflow/skills/superspec-propose/SKILL.md +33 -16
- package/templates/workflow/skills/superspec-review/SKILL.md +9 -9
package/dist/transition.js
CHANGED
|
@@ -5,11 +5,11 @@ import { ensureChangeLayout, readEvents, appendEvent, makeEvent, writeSnapshot,
|
|
|
5
5
|
import { rebuildSnapshot } from "./sync.js";
|
|
6
6
|
import { requiredJobActions } from "./job_action.js";
|
|
7
7
|
import { assertCommitPayloadExtension, isFreshReviewVerifier, isReviewReadyVerifier, latestReviewHistoryForGateRole, readReviewPolicyFromEvents, reviewBoundFiles, reviewEvidenceDigest, reviewPolicyForRisk, REVIEW_DOC_PATHS, } from "./review.js";
|
|
8
|
-
import { REVIEW_CODE_REVIEW_GATE_ID, REVIEW_FINAL_VERIFIER_GATE_ID, reviewScopeForGateRole, } from "./review_job_gates.js";
|
|
9
|
-
import { codeReviewBoundFiles, codeReviewDecisionScope, codeReviewJobStaleReason, codeReviewPacketContext, codeReviewPacketDigest, collectCodeReviewGateFacts, computeCodeStateCheck, currentCodeReviewWorkingPaths, dismissedCodeReviewSummary, latestCodeReviewDecision, latestCodeReviewFailedStatus, missingCoverageExemptionTestIds, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, } from "./code_review.js";
|
|
8
|
+
import { REVIEW_CODE_REVIEW_GATE_ID, REVIEW_FINAL_VERIFIER_GATE, REVIEW_FINAL_VERIFIER_GATE_ID, reviewScopeForGateRole, } from "./review_job_gates.js";
|
|
9
|
+
import { codeReviewBoundFiles, codeReviewDecisionScope, codeReviewJobStaleReason, codeReviewPacketContext, codeReviewPacketDigest, collectCodeReviewGateFacts, computeCodeStateCheck, currentCodeReviewWorkingPaths, dismissedCodeReviewSummary, effectiveCoverageExemptionRefsFromEvents, latestCodeReviewGateEvidence, latestCodeReviewDecision, latestCodeReviewFailedStatus, missingCoverageExemptionTestIds, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, taskExecutionIndexForReview, } from "./code_review.js";
|
|
10
10
|
import { taskEvidenceReadiness } from "./task_evidence.js";
|
|
11
|
-
import { adoptedContractForTask, findTaskInLines,
|
|
12
|
-
import { applyRequirementModeForCurrentRound, executionRequirementVersionForCurrentRound, blockingJobsForApplyDone, executionPolicyForCurrentRound, formatPendingTaskMessage, latestAcceptedProposalBaseline, pendingTaskStatusForApply, planningValidationProfileForNewRound, planTransition, discoveryDocsBaseline, proposalDocsBaseline, } from "./phase_plan.js";
|
|
11
|
+
import { adoptedContractForTask, findTaskInLines, isFixTaskId, parseTasksMd, parseTestContractEntries, } from "./format.js";
|
|
12
|
+
import { applyRequirementModeForCurrentRound, executionRequirementVersionForCurrentRound, blockingJobsForApplyDone, executionPolicyForCurrentRound, formatPendingTaskMessage, latestAcceptedProposalBaseline, pendingTaskStatusForApply, planningValidationProfileForNewRound, planTransition, discoveryDocsBaseline, exploreAnswerRegistrationPayloadForChange, proposeAnswerRegistrationPayloadForChange, proposalDocsBaseline, } from "./phase_plan.js";
|
|
13
13
|
import { latestAcceptedPhaseDecision, phaseConfirmationCommitPayload, phaseConfirmationForBoundary, phaseConfirmationMissingMessage, } from "./phase_confirmation.js";
|
|
14
14
|
import { currentGitHead, dirtyCodeFiles, stageProductionJavaFilesSince } from "./git_state.js";
|
|
15
15
|
import { workflowRiskForProject } from "./workflow_config.js";
|
|
@@ -17,9 +17,9 @@ let transitionSeq = 0;
|
|
|
17
17
|
function newTransitionId() { return `T-${Date.now()}-${++transitionSeq}`; }
|
|
18
18
|
let jobSeq = 0;
|
|
19
19
|
function newJobId(change, role) { return `JOB-${change.slice(0, 8)}-${role.slice(0, 4)}-${Date.now()}-${++jobSeq}`; }
|
|
20
|
-
function createReviewJobsForGate(state, gate, roles, changeRoot, change, reason, events) {
|
|
20
|
+
function createReviewJobsForGate(state, gate, roles, requiredRoles, changeRoot, change, reason, events) {
|
|
21
21
|
const newJobs = roles.map(role => {
|
|
22
|
-
const scope = reviewScopeForGateRole(gate, role);
|
|
22
|
+
const scope = reviewScopeForGateRole(gate, role, requiredRoles);
|
|
23
23
|
// 角色职责目标和显式 freshness 路径绑定时点指纹:单文件缺失使用 sha256:missing,目录缺失使用稳定空指纹。
|
|
24
24
|
const boundPaths = [...new Set(scope.boundPaths)];
|
|
25
25
|
const boundFiles = boundPaths
|
|
@@ -173,13 +173,13 @@ function validateTaskStartContract(changeRoot, taskId, parsedContract) {
|
|
|
173
173
|
*/
|
|
174
174
|
function validateLegacyTaskStartContract(changeRoot, taskId, tddRequired, parsedContract) {
|
|
175
175
|
if (!parsedContract) {
|
|
176
|
-
return tddRequired && !
|
|
176
|
+
return tddRequired && !isFixTaskId(taskId)
|
|
177
177
|
? `执行依据模式下,普通 TDD 任务 ${taskId} 缺少执行依据`
|
|
178
178
|
: null;
|
|
179
179
|
}
|
|
180
180
|
if (parsedContract.errors.length > 0)
|
|
181
181
|
return parsedContract.errors.join(";");
|
|
182
|
-
if (tddRequired && !
|
|
182
|
+
if (tddRequired && !isFixTaskId(taskId) && parsedContract.contract.tests.length === 0) {
|
|
183
183
|
return `${taskId} 是普通 TDD 任务,执行依据缺少测试`;
|
|
184
184
|
}
|
|
185
185
|
if (parsedContract.contract.tests.length === 0)
|
|
@@ -199,7 +199,7 @@ function validateLegacyTaskStartContract(changeRoot, taskId, tddRequired, parsed
|
|
|
199
199
|
* RED 是否要求由这里读取已冻结的 execution_policy 决定,之后不再依赖 tasks.md。
|
|
200
200
|
*/
|
|
201
201
|
function compileRequiredEvidence(executionPolicy, testIds, requiresVerificationWithoutDeclaredTest) {
|
|
202
|
-
//
|
|
202
|
+
// Fix task 没有计划阶段声明的 TEST,但仍必须登记一次真实回归验证。
|
|
203
203
|
// 是否需要 RED 始终由已冻结的 execution_policy 决定。
|
|
204
204
|
const requiresVerification = testIds.length > 0 || requiresVerificationWithoutDeclaredTest;
|
|
205
205
|
return {
|
|
@@ -209,6 +209,28 @@ function compileRequiredEvidence(executionPolicy, testIds, requiresVerificationW
|
|
|
209
209
|
accepted_green_statuses: ["expected_success"],
|
|
210
210
|
};
|
|
211
211
|
}
|
|
212
|
+
function evidenceActionsForAttempt(change, attemptId, required) {
|
|
213
|
+
const testIds = required.test_ids.length > 0 ? required.test_ids : [undefined];
|
|
214
|
+
const statuses = [];
|
|
215
|
+
if (required.red_required)
|
|
216
|
+
statuses.push("expected_failure");
|
|
217
|
+
if (required.green_required)
|
|
218
|
+
statuses.push(required.accepted_green_statuses[0] ?? "expected_success");
|
|
219
|
+
return testIds.flatMap(testId => statuses.map(semanticStatus => ({
|
|
220
|
+
kind: "test_run",
|
|
221
|
+
...(testId ? { test_id: testId } : {}),
|
|
222
|
+
record_argv: ["superspec", "record", "test-run", "--change", change, "--input", "-"],
|
|
223
|
+
record_input: {
|
|
224
|
+
...(testId ? { test_id: testId } : {}),
|
|
225
|
+
attempt_id: attemptId,
|
|
226
|
+
command: null,
|
|
227
|
+
cwd: null,
|
|
228
|
+
exit_code: null,
|
|
229
|
+
semantic_status: semanticStatus,
|
|
230
|
+
},
|
|
231
|
+
required_fields: ["command", "cwd", "exit_code"],
|
|
232
|
+
})));
|
|
233
|
+
}
|
|
212
234
|
function hasRejectedReviewReadyVerifier(events) {
|
|
213
235
|
const reviewReadyVerifierIds = new Set();
|
|
214
236
|
for (const ev of events) {
|
|
@@ -279,26 +301,133 @@ function findReviewFailedFinding(events, ref) {
|
|
|
279
301
|
return null;
|
|
280
302
|
return { event: status.terminal.event, finding };
|
|
281
303
|
}
|
|
282
|
-
function
|
|
283
|
-
|
|
304
|
+
function reviewFixDescriptor(ref, finding) {
|
|
305
|
+
const reason = typeof finding.description === "string" && finding.description.trim()
|
|
306
|
+
? finding.description.trim().replace(/\s+/g, " ")
|
|
307
|
+
: `修复代码审查问题 ${ref.findingId}`;
|
|
308
|
+
return {
|
|
309
|
+
fix_id: `REVIEW-FIX-${ref.jobId}#${ref.findingId}`,
|
|
310
|
+
source: "code_review",
|
|
311
|
+
parent_task_id: null,
|
|
312
|
+
reason,
|
|
313
|
+
review_finding: { job_id: ref.jobId, finding_id: ref.findingId },
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function selfTestFixBaseId(parentTaskId, reason) {
|
|
317
|
+
const normalizedReason = reason.trim().replace(/\s+/g, " ");
|
|
318
|
+
const normalizedTaskId = parentTaskId.replace(/[^A-Za-z0-9_-]/g, "-");
|
|
319
|
+
const digest = sha256Text(`${parentTaskId}\n${normalizedReason}`).replace(/^sha256:/, "").slice(0, 12);
|
|
320
|
+
return `FIX-SELFTEST-${normalizedTaskId}-${digest}`;
|
|
321
|
+
}
|
|
322
|
+
function selfTestFixDescriptor(parentTaskId, reason, occurrence) {
|
|
323
|
+
const normalizedReason = reason.trim().replace(/\s+/g, " ");
|
|
324
|
+
const baseId = selfTestFixBaseId(parentTaskId, normalizedReason);
|
|
325
|
+
return {
|
|
326
|
+
fix_id: occurrence === 1 ? baseId : `${baseId}-${occurrence}`,
|
|
327
|
+
source: "self_test",
|
|
328
|
+
parent_task_id: parentTaskId,
|
|
329
|
+
reason: normalizedReason,
|
|
330
|
+
};
|
|
284
331
|
}
|
|
285
|
-
function
|
|
286
|
-
|
|
332
|
+
function selfTestFixOccurrence(taskId, baseId) {
|
|
333
|
+
if (taskId === baseId)
|
|
334
|
+
return 1;
|
|
335
|
+
const suffix = taskId.slice(baseId.length + 1);
|
|
336
|
+
if (!taskId.startsWith(`${baseId}-`) || !/^\d+$/.test(suffix))
|
|
337
|
+
return null;
|
|
338
|
+
const occurrence = Number(suffix);
|
|
339
|
+
return Number.isSafeInteger(occurrence) && occurrence >= 2 ? occurrence : null;
|
|
287
340
|
}
|
|
288
|
-
function
|
|
341
|
+
function nextSelfTestFixDescriptor(changeRoot, events, parentTaskId, reason) {
|
|
342
|
+
const normalizedReason = reason.trim().replace(/\s+/g, " ");
|
|
343
|
+
const baseId = selfTestFixBaseId(parentTaskId, normalizedReason);
|
|
344
|
+
let maxOccurrence = 0;
|
|
345
|
+
const completion = pendingTaskStatusForApply(changeRoot, events);
|
|
346
|
+
const pendingTaskIds = new Set(completion.pending);
|
|
347
|
+
const taskInfos = parseTasksMd(readFileSync(join(changeRoot, "tasks.md"), "utf8"));
|
|
348
|
+
for (const task of taskInfos) {
|
|
349
|
+
const occurrence = selfTestFixOccurrence(task.taskId, baseId);
|
|
350
|
+
if (occurrence == null)
|
|
351
|
+
continue;
|
|
352
|
+
maxOccurrence = Math.max(maxOccurrence, occurrence);
|
|
353
|
+
if (pendingTaskIds.has(task.taskId))
|
|
354
|
+
return { activeFixTaskId: task.taskId };
|
|
355
|
+
}
|
|
356
|
+
for (const event of events) {
|
|
357
|
+
if (event.event_type !== "transition_commit")
|
|
358
|
+
continue;
|
|
359
|
+
const fix = event.payload.fix;
|
|
360
|
+
if (!fix || typeof fix !== "object" || Array.isArray(fix))
|
|
361
|
+
continue;
|
|
362
|
+
const candidate = fix;
|
|
363
|
+
if (candidate.source !== "self_test" || candidate.parent_task_id !== parentTaskId || candidate.reason !== normalizedReason)
|
|
364
|
+
continue;
|
|
365
|
+
if (typeof candidate.fix_id !== "string")
|
|
366
|
+
continue;
|
|
367
|
+
const occurrence = selfTestFixOccurrence(candidate.fix_id, baseId);
|
|
368
|
+
if (occurrence != null)
|
|
369
|
+
maxOccurrence = Math.max(maxOccurrence, occurrence);
|
|
370
|
+
}
|
|
371
|
+
return { fix: selfTestFixDescriptor(parentTaskId, normalizedReason, maxOccurrence + 1) };
|
|
372
|
+
}
|
|
373
|
+
function fixMarker(fix) {
|
|
374
|
+
if (fix.source === "code_review" && fix.review_finding) {
|
|
375
|
+
return `review_fix_of:${fix.review_finding.job_id}#${fix.review_finding.finding_id}`;
|
|
376
|
+
}
|
|
377
|
+
return `self_test_fix_of:${fix.parent_task_id ?? "unknown"}:${fix.fix_id}`;
|
|
378
|
+
}
|
|
379
|
+
function appendFixTask(changeRoot, fix) {
|
|
289
380
|
const tasksPath = join(changeRoot, "tasks.md");
|
|
290
381
|
const content = readFileSync(tasksPath, "utf8");
|
|
291
|
-
const marker =
|
|
292
|
-
if (content.includes(marker))
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
382
|
+
const marker = fixMarker(fix);
|
|
383
|
+
if (content.includes(marker)) {
|
|
384
|
+
const matchingTask = content.split("\n").some(line => line.includes(marker) && line.startsWith(`- [ ] ${fix.fix_id} `));
|
|
385
|
+
if (matchingTask)
|
|
386
|
+
return "exists";
|
|
387
|
+
throw new Error(`修复标记 ${marker} 已被其它 task 占用,拒绝创建 ${fix.fix_id}`);
|
|
388
|
+
}
|
|
389
|
+
if (parseTasksMd(content).some(task => task.taskId === fix.fix_id)) {
|
|
390
|
+
throw new Error(`修复 task ID ${fix.fix_id} 已被其它任务占用,拒绝创建重复修复`);
|
|
391
|
+
}
|
|
392
|
+
const description = fix.source === "self_test"
|
|
393
|
+
? `修复自测问题(关联 ${fix.parent_task_id}):${fix.reason}`
|
|
394
|
+
: fix.reason;
|
|
395
|
+
const line = `- [ ] ${fix.fix_id} ${description} ${marker}`;
|
|
298
396
|
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
299
397
|
writeFileSync(tasksPath, `${content}${suffix}${line}\n`);
|
|
300
398
|
return "created";
|
|
301
399
|
}
|
|
400
|
+
function fixDescriptorForTask(events, taskId) {
|
|
401
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
402
|
+
const event = events[i];
|
|
403
|
+
if (event.event_type !== "transition_commit")
|
|
404
|
+
continue;
|
|
405
|
+
const fix = event.payload.fix;
|
|
406
|
+
if (!fix || typeof fix !== "object" || Array.isArray(fix))
|
|
407
|
+
continue;
|
|
408
|
+
const candidate = fix;
|
|
409
|
+
if (candidate.fix_id !== taskId)
|
|
410
|
+
continue;
|
|
411
|
+
if (candidate.source !== "code_review" && candidate.source !== "self_test")
|
|
412
|
+
continue;
|
|
413
|
+
if (typeof candidate.parent_task_id !== "string" && candidate.parent_task_id !== null)
|
|
414
|
+
continue;
|
|
415
|
+
if (typeof candidate.reason !== "string")
|
|
416
|
+
continue;
|
|
417
|
+
return candidate;
|
|
418
|
+
}
|
|
419
|
+
// 兼容发布前已写入 tasks.md、但 transition payload 尚未保存修复描述符的记录。
|
|
420
|
+
const legacy = /^REVIEW-FIX-(.+)#([^#]+)$/.exec(taskId);
|
|
421
|
+
return legacy
|
|
422
|
+
? {
|
|
423
|
+
fix_id: taskId,
|
|
424
|
+
source: "code_review",
|
|
425
|
+
parent_task_id: null,
|
|
426
|
+
reason: "历史代码审查修复任务",
|
|
427
|
+
review_finding: { job_id: legacy[1], finding_id: legacy[2] },
|
|
428
|
+
}
|
|
429
|
+
: null;
|
|
430
|
+
}
|
|
302
431
|
function isFreshOpenCodeReviewerJob(job, projectRoot, currentWorkingPaths) {
|
|
303
432
|
return codeReviewJobStaleReason(projectRoot, job, currentWorkingPaths) == null;
|
|
304
433
|
}
|
|
@@ -356,6 +485,17 @@ function evaluateApplyDoneCodeReviewGate(input) {
|
|
|
356
485
|
};
|
|
357
486
|
}
|
|
358
487
|
if (latest?.state === "rejected" && latest.result_kind === "review_failed") {
|
|
488
|
+
const staleReason = codeReviewJobStaleReason(input.projectRoot, latest.job, currentWorkingPaths);
|
|
489
|
+
if (staleReason) {
|
|
490
|
+
const { job, scanReason } = createCodeReviewerJob(input.change, input.projectRoot, input.changeRoot, input.events);
|
|
491
|
+
return {
|
|
492
|
+
fromState: "apply_done",
|
|
493
|
+
toState: "apply_done",
|
|
494
|
+
outcome: "job_created",
|
|
495
|
+
newJobs: [job],
|
|
496
|
+
reason: `代码审查结论已不再匹配当前代码状态,重新创建代码审查工作项;${staleReason};${scanReason}`,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
359
499
|
const reviewFailedStatus = latestCodeReviewFailedStatus(input.events);
|
|
360
500
|
if (reviewFailedStatus && reviewFailedStatus.findings.length > 0 && reviewFailedStatus.unresolved.length === 0) {
|
|
361
501
|
const { job, scanReason } = createCodeReviewerJob(input.change, input.projectRoot, input.changeRoot, input.events);
|
|
@@ -400,9 +540,14 @@ function evaluateApplyDoneCodeReviewGate(input) {
|
|
|
400
540
|
}
|
|
401
541
|
function createFinalVerifierJob(change, projectRoot, changeRoot, events, currentEvidenceDigest) {
|
|
402
542
|
const boundFiles = reviewBoundFiles(changeRoot);
|
|
543
|
+
const codeReviewGate = latestCodeReviewGateEvidence(events);
|
|
403
544
|
const packetContext = {
|
|
404
545
|
code_state_check: computeCodeStateCheck(projectRoot, events),
|
|
546
|
+
coverage_exemption_refs: effectiveCoverageExemptionRefsFromEvents(events),
|
|
547
|
+
task_execution_index: taskExecutionIndexForReview(projectRoot, events),
|
|
548
|
+
...(codeReviewGate ? { code_review_gate: codeReviewGate } : {}),
|
|
405
549
|
};
|
|
550
|
+
const previousRejection = latestReviewHistoryForGateRole(events, REVIEW_FINAL_VERIFIER_GATE, "verifier");
|
|
406
551
|
return {
|
|
407
552
|
job_id: newJobId(change, "verifier"),
|
|
408
553
|
role: "verifier",
|
|
@@ -418,9 +563,11 @@ function createFinalVerifierJob(change, projectRoot, changeRoot, events, current
|
|
|
418
563
|
review_evidence_digest: currentEvidenceDigest,
|
|
419
564
|
packet_context: packetContext,
|
|
420
565
|
created_from_transition: "review-ready",
|
|
566
|
+
...(previousRejection ? { previous_rejection: previousRejection } : {}),
|
|
421
567
|
})),
|
|
422
568
|
created_from_transition: "review-ready",
|
|
423
569
|
created_at: new Date().toISOString(),
|
|
570
|
+
...(previousRejection ? { previous_rejection: previousRejection } : {}),
|
|
424
571
|
};
|
|
425
572
|
}
|
|
426
573
|
function evaluateFinalVerifierGate(input) {
|
|
@@ -502,7 +649,7 @@ function transitionPlanToDecision(snapshot, changeRoot, change, plan, events) {
|
|
|
502
649
|
case "blocked":
|
|
503
650
|
return { blocked: true, reason: plan.reason, jobs: plan.jobs, ...(plan.details ? { details: plan.details } : {}) };
|
|
504
651
|
case "create_gate_jobs":
|
|
505
|
-
return createReviewJobsForGate(snapshot.state, plan.gate, plan.roles, changeRoot, change, plan.reason, events);
|
|
652
|
+
return createReviewJobsForGate(snapshot.state, plan.gate, plan.roles, plan.requiredRoles, changeRoot, change, plan.reason, events);
|
|
506
653
|
case "advance":
|
|
507
654
|
return {
|
|
508
655
|
fromState: plan.fromState,
|
|
@@ -695,11 +842,15 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
|
|
|
695
842
|
// 统一出口:adopted.contract 非 null 当且仅当契约模式下有绑定块;
|
|
696
843
|
// legacy 轮即使 task 带执行依据文本也输出 null,避免"看似契约、实按 legacy 校验"的误导态
|
|
697
844
|
const adopted = adoptedContractForTask(tasksContent, taskId, contractMode);
|
|
698
|
-
const
|
|
699
|
-
|
|
845
|
+
const fix = fixDescriptorForTask(events, taskId);
|
|
846
|
+
const isFixTask = isFixTaskId(taskId);
|
|
847
|
+
if (isFixTask && !fix) {
|
|
848
|
+
return { skip: true, message: `Fix task ${taskId} 缺少状态机创建记录,不能直接手工追加` };
|
|
849
|
+
}
|
|
850
|
+
if (contractMode && executionRequirementVersion === 2 && !isFixTask && !adopted.parsed) {
|
|
700
851
|
return { skip: true, message: `执行依据模式下,任务 ${taskId} 缺少执行依据` };
|
|
701
852
|
}
|
|
702
|
-
if (contractMode && executionRequirementVersion === 2 && !
|
|
853
|
+
if (contractMode && executionRequirementVersion === 2 && !isFixTask && adopted.parsed) {
|
|
703
854
|
const contractError = validateTaskStartContract(changeRoot, taskId, adopted.parsed);
|
|
704
855
|
if (contractError)
|
|
705
856
|
return { skip: true, message: contractError };
|
|
@@ -711,17 +862,19 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
|
|
|
711
862
|
}
|
|
712
863
|
const structureDigest = sha256Text(tasksContent.replace(/- \[[xX]\]/g, "- [ ]"));
|
|
713
864
|
const effectivePolicy = executionPolicy;
|
|
714
|
-
const requiredEvidence = contractMode && executionRequirementVersion === 2
|
|
715
|
-
? compileRequiredEvidence(effectivePolicy, adopted.contract?.tests ?? [],
|
|
865
|
+
const requiredEvidence = isFixTask || (contractMode && executionRequirementVersion === 2)
|
|
866
|
+
? compileRequiredEvidence(effectivePolicy, adopted.contract?.tests ?? [], isFixTask)
|
|
716
867
|
: null;
|
|
717
868
|
const attempt = {
|
|
718
869
|
attempt_id: `ATT-${taskId}-${Date.now()}-${++attemptSeq}`,
|
|
719
870
|
task_id: taskId, state: "active",
|
|
720
871
|
task_structure_digest: structureDigest,
|
|
872
|
+
...(fix ? { fix } : {}),
|
|
721
873
|
contract_mode: contractMode,
|
|
722
874
|
contract: adopted.contract,
|
|
723
875
|
...(requiredEvidence ? { required_evidence: requiredEvidence } : {
|
|
724
|
-
//
|
|
876
|
+
// 非 Fix 的历史模式以及 v1 契约轮继续使用 task 行标记回放;新建 Fix
|
|
877
|
+
// 无论来自哪个版本,均只消费本轮冻结的有效证据要求。
|
|
725
878
|
tdd_required: taskInfo.tddRequired,
|
|
726
879
|
no_tdd_reason: taskInfo.noTddReason,
|
|
727
880
|
}),
|
|
@@ -736,6 +889,9 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
|
|
|
736
889
|
...attempt,
|
|
737
890
|
...boundarySnapshotPayload(projectRoot),
|
|
738
891
|
};
|
|
892
|
+
const evidenceActions = requiredEvidence
|
|
893
|
+
? evidenceActionsForAttempt(change, attempt.attempt_id, requiredEvidence)
|
|
894
|
+
: null;
|
|
739
895
|
return {
|
|
740
896
|
fromState: "apply", toState: "apply", outcome: "advanced",
|
|
741
897
|
reason: `创建任务 ${taskId} 执行尝试`,
|
|
@@ -745,8 +901,10 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
|
|
|
745
901
|
task_id: taskId,
|
|
746
902
|
execution_policy: effectivePolicy,
|
|
747
903
|
contract: adopted.contract,
|
|
904
|
+
...(fix ? { fix } : {}),
|
|
748
905
|
...(requiredEvidence ? { required_evidence: requiredEvidence } : {}),
|
|
749
|
-
|
|
906
|
+
...(evidenceActions ? { evidence_actions: evidenceActions } : {}),
|
|
907
|
+
// legacy 轮统一标注历史模式(无论有无执行依据文本);契约轮无块时标 false(如 Fix task)
|
|
750
908
|
...(adopted.contract ? {} : { legacy_contract: !contractMode }),
|
|
751
909
|
},
|
|
752
910
|
};
|
|
@@ -792,11 +950,20 @@ function canReopenToPropose(from) {
|
|
|
792
950
|
}
|
|
793
951
|
export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
794
952
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
795
|
-
name: "reopen", idempotencyInputs: {
|
|
953
|
+
name: "reopen", idempotencyInputs: {
|
|
954
|
+
to,
|
|
955
|
+
reason,
|
|
956
|
+
reviewFix: opts.reviewFix ?? "",
|
|
957
|
+
reviewFinding: opts.reviewFinding ?? "",
|
|
958
|
+
selfTestFix: opts.selfTestFix ?? "",
|
|
959
|
+
},
|
|
796
960
|
decide: (snapshot) => {
|
|
797
961
|
if (!reason || reason.trim() === "")
|
|
798
962
|
return { skip: true, message: "reopen 需要非空 --reason" };
|
|
799
963
|
const events = readEvents(projectRoot, change);
|
|
964
|
+
const specialFixCount = [opts.reviewFix, opts.reviewFinding, opts.selfTestFix].filter(Boolean).length;
|
|
965
|
+
if (specialFixCount > 1)
|
|
966
|
+
return { skip: true, message: "一次 reopen 只能指定一种修复或审查引用" };
|
|
800
967
|
if (opts.reviewFinding) {
|
|
801
968
|
if (to !== "propose")
|
|
802
969
|
return { skip: true, message: "--review-finding 只能用于回到计划阶段(reopen --to propose)" };
|
|
@@ -828,6 +995,7 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
828
995
|
baseline_docs: proposalDocsBaseline(changeRoot),
|
|
829
996
|
planning_validation_version: 2,
|
|
830
997
|
planning_validation_profile: planningValidationProfileForNewRound(projectRoot),
|
|
998
|
+
...proposeAnswerRegistrationPayloadForChange(changeRoot),
|
|
831
999
|
},
|
|
832
1000
|
};
|
|
833
1001
|
}
|
|
@@ -852,6 +1020,7 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
852
1020
|
else if (type !== "implementation") {
|
|
853
1021
|
return { skip: true, message: "这个代码审查问题不能直接回到实现阶段处理" };
|
|
854
1022
|
}
|
|
1023
|
+
const fix = reviewFixDescriptor(ref, found.finding);
|
|
855
1024
|
return {
|
|
856
1025
|
fromState: "apply_done",
|
|
857
1026
|
toState: "apply",
|
|
@@ -861,9 +1030,56 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
861
1030
|
review_fix_of: `${ref.jobId}#${ref.findingId}`,
|
|
862
1031
|
source_job_id: ref.jobId,
|
|
863
1032
|
finding_id: ref.findingId,
|
|
1033
|
+
fix,
|
|
1034
|
+
},
|
|
1035
|
+
postCommit: (_pr, _ch, cr) => {
|
|
1036
|
+
appendFixTask(cr, fix);
|
|
864
1037
|
},
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
if (opts.selfTestFix) {
|
|
1041
|
+
if (to !== "apply")
|
|
1042
|
+
return { skip: true, message: "--self-test-fix 只能用于回到实现阶段(reopen --to apply)" };
|
|
1043
|
+
const allowedStates = ["apply", "apply_done", "review", "accepted"];
|
|
1044
|
+
if (!allowedStates.includes(snapshot.state)) {
|
|
1045
|
+
return { skip: true, message: `当前状态 ${snapshot.state},不能通过自测问题回到实现阶段` };
|
|
1046
|
+
}
|
|
1047
|
+
const parentTaskId = opts.selfTestFix.trim();
|
|
1048
|
+
const parentTask = parseTasksMd(readFileSync(join(changeRoot, "tasks.md"), "utf8"))
|
|
1049
|
+
.find(task => task.taskId === parentTaskId);
|
|
1050
|
+
if (!parentTask)
|
|
1051
|
+
return { skip: true, message: `自测修复关联的 task ${parentTaskId} 不存在` };
|
|
1052
|
+
const nextFix = nextSelfTestFixDescriptor(changeRoot, events, parentTaskId, reason);
|
|
1053
|
+
if ("activeFixTaskId" in nextFix) {
|
|
1054
|
+
return { skip: true, message: `同一自测问题的修复 ${nextFix.activeFixTaskId} 尚未完成,不能重复创建` };
|
|
1055
|
+
}
|
|
1056
|
+
const fix = nextFix.fix;
|
|
1057
|
+
const pendingStatus = pendingTaskStatusForApply(changeRoot, events);
|
|
1058
|
+
if (pendingStatus.pending.length > 0 || snapshot.active_task_attempts.some(attempt => attempt.state === "active")) {
|
|
1059
|
+
return { skip: true, message: "自测修复只允许在当前 task 全部完成且没有活跃执行尝试后创建" };
|
|
1060
|
+
}
|
|
1061
|
+
if (pendingStatus.mode === "contract" && !pendingStatus.completedByEvent.includes(parentTaskId)) {
|
|
1062
|
+
return { skip: true, message: `自测修复关联的 task ${parentTaskId} 缺少完成事件,不能只依赖 checkbox` };
|
|
1063
|
+
}
|
|
1064
|
+
if (pendingStatus.mode === "legacy" && !parentTask.done) {
|
|
1065
|
+
return { skip: true, message: `自测修复关联的 task ${parentTaskId} 尚未完成` };
|
|
1066
|
+
}
|
|
1067
|
+
const failedReview = latestCodeReviewFailedStatus(events);
|
|
1068
|
+
if (failedReview?.unresolved.length) {
|
|
1069
|
+
return {
|
|
1070
|
+
skip: true,
|
|
1071
|
+
message: `已有未处理的代码审查问题 ${failedReview.unresolved[0].id};请先按 next 返回的 --review-fix 处理`,
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
return {
|
|
1075
|
+
fromState: snapshot.state,
|
|
1076
|
+
toState: "apply",
|
|
1077
|
+
outcome: "advanced",
|
|
1078
|
+
reason: reason.trim(),
|
|
1079
|
+
commitPayload: { fix },
|
|
1080
|
+
extraEvents: invalidateOpenJobs(snapshot, "apply", reason.trim()),
|
|
865
1081
|
postCommit: (_pr, _ch, cr) => {
|
|
866
|
-
|
|
1082
|
+
appendFixTask(cr, fix);
|
|
867
1083
|
},
|
|
868
1084
|
};
|
|
869
1085
|
}
|
|
@@ -880,6 +1096,7 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
880
1096
|
reopen_target: "explore",
|
|
881
1097
|
reopen_source: snapshot.state,
|
|
882
1098
|
baseline_docs: discoveryDocsBaseline(changeRoot),
|
|
1099
|
+
...exploreAnswerRegistrationPayloadForChange(changeRoot),
|
|
883
1100
|
},
|
|
884
1101
|
extraEvents: planningReopenExtraEvents(snapshot, "explore", reason.trim()),
|
|
885
1102
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -50,6 +50,21 @@ export interface EffectiveEvidencePlan {
|
|
|
50
50
|
green_required: boolean;
|
|
51
51
|
accepted_green_statuses: Array<"expected_success" | "characterization_pass">;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* 状态机创建的实现修复工作项。它只纠正已批准 task 的实现,不产生新的计划需求。
|
|
55
|
+
* 旧事件没有该字段时按历史行为回放。
|
|
56
|
+
*/
|
|
57
|
+
export type FixSource = "code_review" | "self_test";
|
|
58
|
+
export interface FixDescriptor {
|
|
59
|
+
fix_id: string;
|
|
60
|
+
source: FixSource;
|
|
61
|
+
parent_task_id: string | null;
|
|
62
|
+
reason: string;
|
|
63
|
+
review_finding?: {
|
|
64
|
+
job_id: string;
|
|
65
|
+
finding_id: string;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
53
68
|
export interface DirtyFileFingerprint {
|
|
54
69
|
path: string;
|
|
55
70
|
status: "added" | "modified" | "deleted";
|
|
@@ -78,9 +93,19 @@ export interface CodeStateCheck {
|
|
|
78
93
|
changed_paths: string[];
|
|
79
94
|
scope_reason: string;
|
|
80
95
|
}
|
|
96
|
+
/** 最终验证读取的最新代码审查门禁事实。 */
|
|
97
|
+
export interface CodeReviewGateEvidence {
|
|
98
|
+
decision: "passed" | "skipped";
|
|
99
|
+
job_id: string | null;
|
|
100
|
+
packet_digest: string | null;
|
|
101
|
+
reason?: "no_code_changes";
|
|
102
|
+
event_id: string;
|
|
103
|
+
event_digest: string;
|
|
104
|
+
}
|
|
81
105
|
export interface TaskExecutionIndexEntry {
|
|
82
106
|
task_id: string;
|
|
83
107
|
attempt_id: string;
|
|
108
|
+
fix?: FixDescriptor | null;
|
|
84
109
|
execution_policy: ExecutionPolicy;
|
|
85
110
|
changed_paths: string[] | null;
|
|
86
111
|
changed_paths_partial_reason?: string;
|
|
@@ -99,6 +124,7 @@ export interface CoverageExemptionRef {
|
|
|
99
124
|
}
|
|
100
125
|
export interface JobPacketContext {
|
|
101
126
|
code_review_scope?: CodeReviewScope;
|
|
127
|
+
code_review_gate?: CodeReviewGateEvidence;
|
|
102
128
|
coverage_exemption_refs?: CoverageExemptionRef[];
|
|
103
129
|
task_execution_index?: TaskExecutionIndexEntry[];
|
|
104
130
|
unattributed_paths?: string[];
|
|
@@ -117,6 +143,7 @@ export interface JobPacket {
|
|
|
117
143
|
previous_rejection?: ReviewPreviousRejection;
|
|
118
144
|
packet_context?: JobPacketContext;
|
|
119
145
|
code_review_scope?: CodeReviewScope;
|
|
146
|
+
code_review_gate?: CodeReviewGateEvidence;
|
|
120
147
|
coverage_exemption_refs?: CoverageExemptionRef[];
|
|
121
148
|
task_execution_index?: TaskExecutionIndexEntry[];
|
|
122
149
|
unattributed_paths?: string[];
|
|
@@ -141,7 +168,7 @@ export interface RequiredJobAction {
|
|
|
141
168
|
packet_command: string;
|
|
142
169
|
packet_argv: string[];
|
|
143
170
|
}
|
|
144
|
-
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";
|
|
171
|
+
export type EventType = "transition_prepare" | "transition_commit" | "job_requested" | "job_invalidated" | "reopen" | "abandon" | "task_started" | "task_completed" | "task_abandoned" | "job_accepted" | "job_rejected" | "user_question_presented" | "user_decision_recorded" | "test_run_recorded" | "task_activation_recorded" | "artifact_recorded";
|
|
145
172
|
export interface Event {
|
|
146
173
|
event_id: string;
|
|
147
174
|
event_type: EventType;
|
|
@@ -165,6 +192,9 @@ export type OpenSpecValidationProfile = {
|
|
|
165
192
|
export interface PlanningValidationProfile {
|
|
166
193
|
version: 2;
|
|
167
194
|
openspec: OpenSpecValidationProfile;
|
|
195
|
+
design?: {
|
|
196
|
+
schema_version: 1;
|
|
197
|
+
};
|
|
168
198
|
}
|
|
169
199
|
export interface TransitionCommitPayload {
|
|
170
200
|
transition: string;
|
|
@@ -227,6 +257,7 @@ export interface TaskAttempt {
|
|
|
227
257
|
task_id: string;
|
|
228
258
|
state: AttemptState;
|
|
229
259
|
task_structure_digest: string;
|
|
260
|
+
fix?: FixDescriptor | null;
|
|
230
261
|
contract?: ExecutionContract | null;
|
|
231
262
|
contract_mode?: boolean;
|
|
232
263
|
/** task-start 编译出的有效执行要求;缺失表示历史 attempt,按旧字段回放。 */
|
|
@@ -290,6 +321,14 @@ export interface AskUser {
|
|
|
290
321
|
allowed_answers: string[];
|
|
291
322
|
scope: string;
|
|
292
323
|
actions?: AskUserAction[];
|
|
324
|
+
/** 自由文本问题的直接登记入口;固定选项继续使用 actions。 */
|
|
325
|
+
record_argv?: string[];
|
|
326
|
+
record_input?: {
|
|
327
|
+
scope: string;
|
|
328
|
+
question: string;
|
|
329
|
+
answer: null;
|
|
330
|
+
};
|
|
331
|
+
required_fields?: Array<"answer">;
|
|
293
332
|
}
|
|
294
333
|
export interface AcceptedMaterialFollowupContinuation {
|
|
295
334
|
kind: "accepted_material_followup";
|
|
@@ -303,6 +342,33 @@ export interface AcceptedMaterialFollowupContinuation {
|
|
|
303
342
|
};
|
|
304
343
|
plan_docs_changed_since_accept: boolean | null;
|
|
305
344
|
}
|
|
345
|
+
export type WorkflowArtifactKind = "discovery" | "test_contract";
|
|
346
|
+
export interface RequiredWorkflowArtifact {
|
|
347
|
+
kind: WorkflowArtifactKind;
|
|
348
|
+
/** Repository-relative canonical path owned by the workflow engine. */
|
|
349
|
+
path: string;
|
|
350
|
+
operation: "create_or_update";
|
|
351
|
+
}
|
|
352
|
+
export interface ArtifactRequiredResume {
|
|
353
|
+
argv: string[];
|
|
354
|
+
}
|
|
355
|
+
export interface MaterialUpdateRequiredResume {
|
|
356
|
+
argv: string[];
|
|
357
|
+
}
|
|
358
|
+
export interface TestEvidenceAction {
|
|
359
|
+
kind: "test_run";
|
|
360
|
+
test_id?: string;
|
|
361
|
+
record_argv: string[];
|
|
362
|
+
record_input: {
|
|
363
|
+
test_id?: string;
|
|
364
|
+
attempt_id: string;
|
|
365
|
+
command: null;
|
|
366
|
+
cwd: null;
|
|
367
|
+
exit_code: null;
|
|
368
|
+
semantic_status: "expected_failure" | "expected_success" | "characterization_pass";
|
|
369
|
+
};
|
|
370
|
+
required_fields: Array<"command" | "cwd" | "exit_code">;
|
|
371
|
+
}
|
|
306
372
|
export type NextOutput = {
|
|
307
373
|
state: State;
|
|
308
374
|
} & ({
|
|
@@ -314,6 +380,16 @@ export type NextOutput = {
|
|
|
314
380
|
path: "required_job";
|
|
315
381
|
required_jobs: RequiredJobAction[];
|
|
316
382
|
reason: string;
|
|
383
|
+
} | {
|
|
384
|
+
path: "artifact_required";
|
|
385
|
+
artifact: RequiredWorkflowArtifact;
|
|
386
|
+
resume: ArtifactRequiredResume;
|
|
387
|
+
reason: string;
|
|
388
|
+
} | {
|
|
389
|
+
path: "material_update_required";
|
|
390
|
+
errors: string[];
|
|
391
|
+
resume: MaterialUpdateRequiredResume;
|
|
392
|
+
reason: string;
|
|
317
393
|
} | {
|
|
318
394
|
path: "ask_user";
|
|
319
395
|
ask_user: AskUser;
|
package/dist/workflow_profile.js
CHANGED
|
@@ -6,7 +6,7 @@ const CURRENT_GATE_ROLES_BY_RISK = {
|
|
|
6
6
|
"review.final_verifier": ["verifier"],
|
|
7
7
|
},
|
|
8
8
|
normal: {
|
|
9
|
-
"explore.discovery_review": [],
|
|
9
|
+
"explore.discovery_review": ["critic"],
|
|
10
10
|
"propose.final_review": ["critic"],
|
|
11
11
|
"review.code_review": ["code-reviewer"],
|
|
12
12
|
"review.final_verifier": ["verifier"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peterxiaoyang/superspec",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.48",
|
|
4
4
|
"description": "SuperSpec 流程引擎 — transition engine with lightweight fact-sync",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
"build": "node build.js",
|
|
25
25
|
"typecheck": "tsc --noEmit",
|
|
26
26
|
"test": "node --test tests/*.ts",
|
|
27
|
+
"eval:probe": "node evals/probe.mjs",
|
|
28
|
+
"eval:dynamic": "node evals/probe.mjs --scenario evals/scenarios/probe-dynamic-accepted.json",
|
|
29
|
+
"eval:m2": "node evals/m2.mjs",
|
|
30
|
+
"eval:m2:validate": "node evals/m2.mjs --validate-faults",
|
|
31
|
+
"eval:arena": "node evals/arena.mjs",
|
|
32
|
+
"eval:delegation": "node evals/delegation-probe.mjs",
|
|
27
33
|
"prepack": "npm run build",
|
|
28
34
|
"prepublishOnly": "npm run build"
|
|
29
35
|
},
|