@peterxiaoyang/superspec 0.1.46 → 0.1.47
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 +2 -1
- package/dist/code_review.d.ts +11 -1
- package/dist/code_review.js +40 -0
- package/dist/explore_round.d.ts +23 -0
- package/dist/explore_round.js +94 -0
- package/dist/format.d.ts +51 -2
- package/dist/format.js +106 -12
- package/dist/openspec.js +26 -5
- package/dist/phase_confirmation.js +71 -2
- package/dist/phase_plan.d.ts +3 -0
- package/dist/phase_plan.js +56 -12
- package/dist/record.js +191 -57
- package/dist/review.js +2 -0
- package/dist/task_evidence.js +5 -3
- package/dist/transition.d.ts +1 -0
- package/dist/transition.js +218 -28
- package/dist/types.d.ts +28 -0
- package/package.json +1 -1
- package/templates/workflow/AGENTS.md +15 -5
- package/templates/workflow/prompts/architect.md +2 -2
- package/templates/workflow/prompts/code-reviewer.md +3 -3
- package/templates/workflow/prompts/critic.md +2 -2
- package/templates/workflow/prompts/executor.md +5 -5
- package/templates/workflow/prompts/explore.md +2 -2
- package/templates/workflow/prompts/test-engineer.md +5 -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 +20 -10
- package/templates/workflow/skills/superspec-explore/SKILL.md +19 -5
- package/templates/workflow/skills/superspec-propose/SKILL.md +21 -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, 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";
|
|
@@ -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 {
|
|
@@ -279,26 +279,133 @@ function findReviewFailedFinding(events, ref) {
|
|
|
279
279
|
return null;
|
|
280
280
|
return { event: status.terminal.event, finding };
|
|
281
281
|
}
|
|
282
|
-
function
|
|
283
|
-
|
|
282
|
+
function reviewFixDescriptor(ref, finding) {
|
|
283
|
+
const reason = typeof finding.description === "string" && finding.description.trim()
|
|
284
|
+
? finding.description.trim().replace(/\s+/g, " ")
|
|
285
|
+
: `修复代码审查问题 ${ref.findingId}`;
|
|
286
|
+
return {
|
|
287
|
+
fix_id: `REVIEW-FIX-${ref.jobId}#${ref.findingId}`,
|
|
288
|
+
source: "code_review",
|
|
289
|
+
parent_task_id: null,
|
|
290
|
+
reason,
|
|
291
|
+
review_finding: { job_id: ref.jobId, finding_id: ref.findingId },
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function selfTestFixBaseId(parentTaskId, reason) {
|
|
295
|
+
const normalizedReason = reason.trim().replace(/\s+/g, " ");
|
|
296
|
+
const normalizedTaskId = parentTaskId.replace(/[^A-Za-z0-9_-]/g, "-");
|
|
297
|
+
const digest = sha256Text(`${parentTaskId}\n${normalizedReason}`).replace(/^sha256:/, "").slice(0, 12);
|
|
298
|
+
return `FIX-SELFTEST-${normalizedTaskId}-${digest}`;
|
|
299
|
+
}
|
|
300
|
+
function selfTestFixDescriptor(parentTaskId, reason, occurrence) {
|
|
301
|
+
const normalizedReason = reason.trim().replace(/\s+/g, " ");
|
|
302
|
+
const baseId = selfTestFixBaseId(parentTaskId, normalizedReason);
|
|
303
|
+
return {
|
|
304
|
+
fix_id: occurrence === 1 ? baseId : `${baseId}-${occurrence}`,
|
|
305
|
+
source: "self_test",
|
|
306
|
+
parent_task_id: parentTaskId,
|
|
307
|
+
reason: normalizedReason,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function selfTestFixOccurrence(taskId, baseId) {
|
|
311
|
+
if (taskId === baseId)
|
|
312
|
+
return 1;
|
|
313
|
+
const suffix = taskId.slice(baseId.length + 1);
|
|
314
|
+
if (!taskId.startsWith(`${baseId}-`) || !/^\d+$/.test(suffix))
|
|
315
|
+
return null;
|
|
316
|
+
const occurrence = Number(suffix);
|
|
317
|
+
return Number.isSafeInteger(occurrence) && occurrence >= 2 ? occurrence : null;
|
|
318
|
+
}
|
|
319
|
+
function nextSelfTestFixDescriptor(changeRoot, events, parentTaskId, reason) {
|
|
320
|
+
const normalizedReason = reason.trim().replace(/\s+/g, " ");
|
|
321
|
+
const baseId = selfTestFixBaseId(parentTaskId, normalizedReason);
|
|
322
|
+
let maxOccurrence = 0;
|
|
323
|
+
const completion = pendingTaskStatusForApply(changeRoot, events);
|
|
324
|
+
const pendingTaskIds = new Set(completion.pending);
|
|
325
|
+
const taskInfos = parseTasksMd(readFileSync(join(changeRoot, "tasks.md"), "utf8"));
|
|
326
|
+
for (const task of taskInfos) {
|
|
327
|
+
const occurrence = selfTestFixOccurrence(task.taskId, baseId);
|
|
328
|
+
if (occurrence == null)
|
|
329
|
+
continue;
|
|
330
|
+
maxOccurrence = Math.max(maxOccurrence, occurrence);
|
|
331
|
+
if (pendingTaskIds.has(task.taskId))
|
|
332
|
+
return { activeFixTaskId: task.taskId };
|
|
333
|
+
}
|
|
334
|
+
for (const event of events) {
|
|
335
|
+
if (event.event_type !== "transition_commit")
|
|
336
|
+
continue;
|
|
337
|
+
const fix = event.payload.fix;
|
|
338
|
+
if (!fix || typeof fix !== "object" || Array.isArray(fix))
|
|
339
|
+
continue;
|
|
340
|
+
const candidate = fix;
|
|
341
|
+
if (candidate.source !== "self_test" || candidate.parent_task_id !== parentTaskId || candidate.reason !== normalizedReason)
|
|
342
|
+
continue;
|
|
343
|
+
if (typeof candidate.fix_id !== "string")
|
|
344
|
+
continue;
|
|
345
|
+
const occurrence = selfTestFixOccurrence(candidate.fix_id, baseId);
|
|
346
|
+
if (occurrence != null)
|
|
347
|
+
maxOccurrence = Math.max(maxOccurrence, occurrence);
|
|
348
|
+
}
|
|
349
|
+
return { fix: selfTestFixDescriptor(parentTaskId, normalizedReason, maxOccurrence + 1) };
|
|
284
350
|
}
|
|
285
|
-
function
|
|
286
|
-
|
|
351
|
+
function fixMarker(fix) {
|
|
352
|
+
if (fix.source === "code_review" && fix.review_finding) {
|
|
353
|
+
return `review_fix_of:${fix.review_finding.job_id}#${fix.review_finding.finding_id}`;
|
|
354
|
+
}
|
|
355
|
+
return `self_test_fix_of:${fix.parent_task_id ?? "unknown"}:${fix.fix_id}`;
|
|
287
356
|
}
|
|
288
|
-
function
|
|
357
|
+
function appendFixTask(changeRoot, fix) {
|
|
289
358
|
const tasksPath = join(changeRoot, "tasks.md");
|
|
290
359
|
const content = readFileSync(tasksPath, "utf8");
|
|
291
|
-
const marker =
|
|
292
|
-
if (content.includes(marker))
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
360
|
+
const marker = fixMarker(fix);
|
|
361
|
+
if (content.includes(marker)) {
|
|
362
|
+
const matchingTask = content.split("\n").some(line => line.includes(marker) && line.startsWith(`- [ ] ${fix.fix_id} `));
|
|
363
|
+
if (matchingTask)
|
|
364
|
+
return "exists";
|
|
365
|
+
throw new Error(`修复标记 ${marker} 已被其它 task 占用,拒绝创建 ${fix.fix_id}`);
|
|
366
|
+
}
|
|
367
|
+
if (parseTasksMd(content).some(task => task.taskId === fix.fix_id)) {
|
|
368
|
+
throw new Error(`修复 task ID ${fix.fix_id} 已被其它任务占用,拒绝创建重复修复`);
|
|
369
|
+
}
|
|
370
|
+
const description = fix.source === "self_test"
|
|
371
|
+
? `修复自测问题(关联 ${fix.parent_task_id}):${fix.reason}`
|
|
372
|
+
: fix.reason;
|
|
373
|
+
const line = `- [ ] ${fix.fix_id} ${description} ${marker}`;
|
|
298
374
|
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
299
375
|
writeFileSync(tasksPath, `${content}${suffix}${line}\n`);
|
|
300
376
|
return "created";
|
|
301
377
|
}
|
|
378
|
+
function fixDescriptorForTask(events, taskId) {
|
|
379
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
380
|
+
const event = events[i];
|
|
381
|
+
if (event.event_type !== "transition_commit")
|
|
382
|
+
continue;
|
|
383
|
+
const fix = event.payload.fix;
|
|
384
|
+
if (!fix || typeof fix !== "object" || Array.isArray(fix))
|
|
385
|
+
continue;
|
|
386
|
+
const candidate = fix;
|
|
387
|
+
if (candidate.fix_id !== taskId)
|
|
388
|
+
continue;
|
|
389
|
+
if (candidate.source !== "code_review" && candidate.source !== "self_test")
|
|
390
|
+
continue;
|
|
391
|
+
if (typeof candidate.parent_task_id !== "string" && candidate.parent_task_id !== null)
|
|
392
|
+
continue;
|
|
393
|
+
if (typeof candidate.reason !== "string")
|
|
394
|
+
continue;
|
|
395
|
+
return candidate;
|
|
396
|
+
}
|
|
397
|
+
// 兼容发布前已写入 tasks.md、但 transition payload 尚未保存修复描述符的记录。
|
|
398
|
+
const legacy = /^REVIEW-FIX-(.+)#([^#]+)$/.exec(taskId);
|
|
399
|
+
return legacy
|
|
400
|
+
? {
|
|
401
|
+
fix_id: taskId,
|
|
402
|
+
source: "code_review",
|
|
403
|
+
parent_task_id: null,
|
|
404
|
+
reason: "历史代码审查修复任务",
|
|
405
|
+
review_finding: { job_id: legacy[1], finding_id: legacy[2] },
|
|
406
|
+
}
|
|
407
|
+
: null;
|
|
408
|
+
}
|
|
302
409
|
function isFreshOpenCodeReviewerJob(job, projectRoot, currentWorkingPaths) {
|
|
303
410
|
return codeReviewJobStaleReason(projectRoot, job, currentWorkingPaths) == null;
|
|
304
411
|
}
|
|
@@ -356,6 +463,17 @@ function evaluateApplyDoneCodeReviewGate(input) {
|
|
|
356
463
|
};
|
|
357
464
|
}
|
|
358
465
|
if (latest?.state === "rejected" && latest.result_kind === "review_failed") {
|
|
466
|
+
const staleReason = codeReviewJobStaleReason(input.projectRoot, latest.job, currentWorkingPaths);
|
|
467
|
+
if (staleReason) {
|
|
468
|
+
const { job, scanReason } = createCodeReviewerJob(input.change, input.projectRoot, input.changeRoot, input.events);
|
|
469
|
+
return {
|
|
470
|
+
fromState: "apply_done",
|
|
471
|
+
toState: "apply_done",
|
|
472
|
+
outcome: "job_created",
|
|
473
|
+
newJobs: [job],
|
|
474
|
+
reason: `代码审查结论已不再匹配当前代码状态,重新创建代码审查工作项;${staleReason};${scanReason}`,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
359
477
|
const reviewFailedStatus = latestCodeReviewFailedStatus(input.events);
|
|
360
478
|
if (reviewFailedStatus && reviewFailedStatus.findings.length > 0 && reviewFailedStatus.unresolved.length === 0) {
|
|
361
479
|
const { job, scanReason } = createCodeReviewerJob(input.change, input.projectRoot, input.changeRoot, input.events);
|
|
@@ -400,9 +518,14 @@ function evaluateApplyDoneCodeReviewGate(input) {
|
|
|
400
518
|
}
|
|
401
519
|
function createFinalVerifierJob(change, projectRoot, changeRoot, events, currentEvidenceDigest) {
|
|
402
520
|
const boundFiles = reviewBoundFiles(changeRoot);
|
|
521
|
+
const codeReviewGate = latestCodeReviewGateEvidence(events);
|
|
403
522
|
const packetContext = {
|
|
404
523
|
code_state_check: computeCodeStateCheck(projectRoot, events),
|
|
524
|
+
coverage_exemption_refs: effectiveCoverageExemptionRefsFromEvents(events),
|
|
525
|
+
task_execution_index: taskExecutionIndexForReview(projectRoot, events),
|
|
526
|
+
...(codeReviewGate ? { code_review_gate: codeReviewGate } : {}),
|
|
405
527
|
};
|
|
528
|
+
const previousRejection = latestReviewHistoryForGateRole(events, REVIEW_FINAL_VERIFIER_GATE, "verifier");
|
|
406
529
|
return {
|
|
407
530
|
job_id: newJobId(change, "verifier"),
|
|
408
531
|
role: "verifier",
|
|
@@ -418,9 +541,11 @@ function createFinalVerifierJob(change, projectRoot, changeRoot, events, current
|
|
|
418
541
|
review_evidence_digest: currentEvidenceDigest,
|
|
419
542
|
packet_context: packetContext,
|
|
420
543
|
created_from_transition: "review-ready",
|
|
544
|
+
...(previousRejection ? { previous_rejection: previousRejection } : {}),
|
|
421
545
|
})),
|
|
422
546
|
created_from_transition: "review-ready",
|
|
423
547
|
created_at: new Date().toISOString(),
|
|
548
|
+
...(previousRejection ? { previous_rejection: previousRejection } : {}),
|
|
424
549
|
};
|
|
425
550
|
}
|
|
426
551
|
function evaluateFinalVerifierGate(input) {
|
|
@@ -695,11 +820,15 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
|
|
|
695
820
|
// 统一出口:adopted.contract 非 null 当且仅当契约模式下有绑定块;
|
|
696
821
|
// legacy 轮即使 task 带执行依据文本也输出 null,避免"看似契约、实按 legacy 校验"的误导态
|
|
697
822
|
const adopted = adoptedContractForTask(tasksContent, taskId, contractMode);
|
|
698
|
-
const
|
|
699
|
-
|
|
823
|
+
const fix = fixDescriptorForTask(events, taskId);
|
|
824
|
+
const isFixTask = isFixTaskId(taskId);
|
|
825
|
+
if (isFixTask && !fix) {
|
|
826
|
+
return { skip: true, message: `Fix task ${taskId} 缺少状态机创建记录,不能直接手工追加` };
|
|
827
|
+
}
|
|
828
|
+
if (contractMode && executionRequirementVersion === 2 && !isFixTask && !adopted.parsed) {
|
|
700
829
|
return { skip: true, message: `执行依据模式下,任务 ${taskId} 缺少执行依据` };
|
|
701
830
|
}
|
|
702
|
-
if (contractMode && executionRequirementVersion === 2 && !
|
|
831
|
+
if (contractMode && executionRequirementVersion === 2 && !isFixTask && adopted.parsed) {
|
|
703
832
|
const contractError = validateTaskStartContract(changeRoot, taskId, adopted.parsed);
|
|
704
833
|
if (contractError)
|
|
705
834
|
return { skip: true, message: contractError };
|
|
@@ -711,17 +840,19 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
|
|
|
711
840
|
}
|
|
712
841
|
const structureDigest = sha256Text(tasksContent.replace(/- \[[xX]\]/g, "- [ ]"));
|
|
713
842
|
const effectivePolicy = executionPolicy;
|
|
714
|
-
const requiredEvidence = contractMode && executionRequirementVersion === 2
|
|
715
|
-
? compileRequiredEvidence(effectivePolicy, adopted.contract?.tests ?? [],
|
|
843
|
+
const requiredEvidence = isFixTask || (contractMode && executionRequirementVersion === 2)
|
|
844
|
+
? compileRequiredEvidence(effectivePolicy, adopted.contract?.tests ?? [], isFixTask)
|
|
716
845
|
: null;
|
|
717
846
|
const attempt = {
|
|
718
847
|
attempt_id: `ATT-${taskId}-${Date.now()}-${++attemptSeq}`,
|
|
719
848
|
task_id: taskId, state: "active",
|
|
720
849
|
task_structure_digest: structureDigest,
|
|
850
|
+
...(fix ? { fix } : {}),
|
|
721
851
|
contract_mode: contractMode,
|
|
722
852
|
contract: adopted.contract,
|
|
723
853
|
...(requiredEvidence ? { required_evidence: requiredEvidence } : {
|
|
724
|
-
//
|
|
854
|
+
// 非 Fix 的历史模式以及 v1 契约轮继续使用 task 行标记回放;新建 Fix
|
|
855
|
+
// 无论来自哪个版本,均只消费本轮冻结的有效证据要求。
|
|
725
856
|
tdd_required: taskInfo.tddRequired,
|
|
726
857
|
no_tdd_reason: taskInfo.noTddReason,
|
|
727
858
|
}),
|
|
@@ -745,8 +876,9 @@ export function taskStart(projectRoot, change, changeRoot, taskId) {
|
|
|
745
876
|
task_id: taskId,
|
|
746
877
|
execution_policy: effectivePolicy,
|
|
747
878
|
contract: adopted.contract,
|
|
879
|
+
...(fix ? { fix } : {}),
|
|
748
880
|
...(requiredEvidence ? { required_evidence: requiredEvidence } : {}),
|
|
749
|
-
// legacy 轮统一标注历史模式(无论有无执行依据文本);契约轮无块时标 false(如
|
|
881
|
+
// legacy 轮统一标注历史模式(无论有无执行依据文本);契约轮无块时标 false(如 Fix task)
|
|
750
882
|
...(adopted.contract ? {} : { legacy_contract: !contractMode }),
|
|
751
883
|
},
|
|
752
884
|
};
|
|
@@ -792,11 +924,20 @@ function canReopenToPropose(from) {
|
|
|
792
924
|
}
|
|
793
925
|
export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
794
926
|
return commitTransition(projectRoot, change, changeRoot, {
|
|
795
|
-
name: "reopen", idempotencyInputs: {
|
|
927
|
+
name: "reopen", idempotencyInputs: {
|
|
928
|
+
to,
|
|
929
|
+
reason,
|
|
930
|
+
reviewFix: opts.reviewFix ?? "",
|
|
931
|
+
reviewFinding: opts.reviewFinding ?? "",
|
|
932
|
+
selfTestFix: opts.selfTestFix ?? "",
|
|
933
|
+
},
|
|
796
934
|
decide: (snapshot) => {
|
|
797
935
|
if (!reason || reason.trim() === "")
|
|
798
936
|
return { skip: true, message: "reopen 需要非空 --reason" };
|
|
799
937
|
const events = readEvents(projectRoot, change);
|
|
938
|
+
const specialFixCount = [opts.reviewFix, opts.reviewFinding, opts.selfTestFix].filter(Boolean).length;
|
|
939
|
+
if (specialFixCount > 1)
|
|
940
|
+
return { skip: true, message: "一次 reopen 只能指定一种修复或审查引用" };
|
|
800
941
|
if (opts.reviewFinding) {
|
|
801
942
|
if (to !== "propose")
|
|
802
943
|
return { skip: true, message: "--review-finding 只能用于回到计划阶段(reopen --to propose)" };
|
|
@@ -852,6 +993,7 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
852
993
|
else if (type !== "implementation") {
|
|
853
994
|
return { skip: true, message: "这个代码审查问题不能直接回到实现阶段处理" };
|
|
854
995
|
}
|
|
996
|
+
const fix = reviewFixDescriptor(ref, found.finding);
|
|
855
997
|
return {
|
|
856
998
|
fromState: "apply_done",
|
|
857
999
|
toState: "apply",
|
|
@@ -861,9 +1003,56 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
861
1003
|
review_fix_of: `${ref.jobId}#${ref.findingId}`,
|
|
862
1004
|
source_job_id: ref.jobId,
|
|
863
1005
|
finding_id: ref.findingId,
|
|
1006
|
+
fix,
|
|
1007
|
+
},
|
|
1008
|
+
postCommit: (_pr, _ch, cr) => {
|
|
1009
|
+
appendFixTask(cr, fix);
|
|
864
1010
|
},
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
if (opts.selfTestFix) {
|
|
1014
|
+
if (to !== "apply")
|
|
1015
|
+
return { skip: true, message: "--self-test-fix 只能用于回到实现阶段(reopen --to apply)" };
|
|
1016
|
+
const allowedStates = ["apply", "apply_done", "review", "accepted"];
|
|
1017
|
+
if (!allowedStates.includes(snapshot.state)) {
|
|
1018
|
+
return { skip: true, message: `当前状态 ${snapshot.state},不能通过自测问题回到实现阶段` };
|
|
1019
|
+
}
|
|
1020
|
+
const parentTaskId = opts.selfTestFix.trim();
|
|
1021
|
+
const parentTask = parseTasksMd(readFileSync(join(changeRoot, "tasks.md"), "utf8"))
|
|
1022
|
+
.find(task => task.taskId === parentTaskId);
|
|
1023
|
+
if (!parentTask)
|
|
1024
|
+
return { skip: true, message: `自测修复关联的 task ${parentTaskId} 不存在` };
|
|
1025
|
+
const nextFix = nextSelfTestFixDescriptor(changeRoot, events, parentTaskId, reason);
|
|
1026
|
+
if ("activeFixTaskId" in nextFix) {
|
|
1027
|
+
return { skip: true, message: `同一自测问题的修复 ${nextFix.activeFixTaskId} 尚未完成,不能重复创建` };
|
|
1028
|
+
}
|
|
1029
|
+
const fix = nextFix.fix;
|
|
1030
|
+
const pendingStatus = pendingTaskStatusForApply(changeRoot, events);
|
|
1031
|
+
if (pendingStatus.pending.length > 0 || snapshot.active_task_attempts.some(attempt => attempt.state === "active")) {
|
|
1032
|
+
return { skip: true, message: "自测修复只允许在当前 task 全部完成且没有活跃执行尝试后创建" };
|
|
1033
|
+
}
|
|
1034
|
+
if (pendingStatus.mode === "contract" && !pendingStatus.completedByEvent.includes(parentTaskId)) {
|
|
1035
|
+
return { skip: true, message: `自测修复关联的 task ${parentTaskId} 缺少完成事件,不能只依赖 checkbox` };
|
|
1036
|
+
}
|
|
1037
|
+
if (pendingStatus.mode === "legacy" && !parentTask.done) {
|
|
1038
|
+
return { skip: true, message: `自测修复关联的 task ${parentTaskId} 尚未完成` };
|
|
1039
|
+
}
|
|
1040
|
+
const failedReview = latestCodeReviewFailedStatus(events);
|
|
1041
|
+
if (failedReview?.unresolved.length) {
|
|
1042
|
+
return {
|
|
1043
|
+
skip: true,
|
|
1044
|
+
message: `已有未处理的代码审查问题 ${failedReview.unresolved[0].id};请先按 next 返回的 --review-fix 处理`,
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
return {
|
|
1048
|
+
fromState: snapshot.state,
|
|
1049
|
+
toState: "apply",
|
|
1050
|
+
outcome: "advanced",
|
|
1051
|
+
reason: reason.trim(),
|
|
1052
|
+
commitPayload: { fix },
|
|
1053
|
+
extraEvents: invalidateOpenJobs(snapshot, "apply", reason.trim()),
|
|
865
1054
|
postCommit: (_pr, _ch, cr) => {
|
|
866
|
-
|
|
1055
|
+
appendFixTask(cr, fix);
|
|
867
1056
|
},
|
|
868
1057
|
};
|
|
869
1058
|
}
|
|
@@ -880,6 +1069,7 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
880
1069
|
reopen_target: "explore",
|
|
881
1070
|
reopen_source: snapshot.state,
|
|
882
1071
|
baseline_docs: discoveryDocsBaseline(changeRoot),
|
|
1072
|
+
...exploreAnswerRegistrationPayloadForChange(changeRoot),
|
|
883
1073
|
},
|
|
884
1074
|
extraEvents: planningReopenExtraEvents(snapshot, "explore", reason.trim()),
|
|
885
1075
|
};
|
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[];
|
|
@@ -227,6 +254,7 @@ export interface TaskAttempt {
|
|
|
227
254
|
task_id: string;
|
|
228
255
|
state: AttemptState;
|
|
229
256
|
task_structure_digest: string;
|
|
257
|
+
fix?: FixDescriptor | null;
|
|
230
258
|
contract?: ExecutionContract | null;
|
|
231
259
|
contract_mode?: boolean;
|
|
232
260
|
/** task-start 编译出的有效执行要求;缺失表示历史 attempt,按旧字段回放。 */
|
package/package.json
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
<!-- SUPERSPEC:AGENTS:START -->
|
|
2
|
-
|
|
2
|
+
只有当用户显式调用 `superspec-*`,或明确要求继续处理某个已有 SuperSpec change 时,才进入或续转 SuperSpec 工作流。普通开发、修复、排查、测试或审查请求,即使项目已安装 SuperSpec,也不得自行启动工作流、创建 change、执行 `transition next`,或切换到某个 `superspec-*` 阶段。
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
一旦用户已显式启动工作流或明确指定 change,使用 `superspec-*` 工作流时一律以 `superspec transition next --change "<change>"` 返回的下一步推进;主流程执行内部命令,不要求用户手动运行工作流命令。流程完成前不得跳阶段、不得自称完成。
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
每完成 `next` 返回的当前事项(材料更新、用户答复回写、实现、验证、审查或修复时),立即再次运行 `superspec transition next --change "<change>"` 并继续处理。完成单个事项不等于完成整个 change;只有工作流明确需要用户决定、当前独立工作项尚未返回结果、遇到真实阻塞或整个 change 已完成时才暂停。
|
|
7
7
|
|
|
8
|
-
当用户显式调用 `$superspec-explore
|
|
8
|
+
当用户显式调用 `$superspec-explore`,或明确要求继续处于 Explore 的已有 change 时,视为已明确授权启动 `explore` subagent 做只读深扫;其他 `$superspec-*` 阶段仅在工作流引擎创建独立工作项时,视为授权启动对应 subagent。
|
|
9
|
+
|
|
10
|
+
Explore 中需要用户决定业务、验收、范围或关键取舍时,先简要说明当前理解、影响和建议,再一次只请用户决定一件事;收到明确答复后,更新相关 discovery 结论,再继续工作流。其他阶段要求用户确认、选择处理方向或补齐材料时,按当前工作流返回的要求登记结论或更新相应材料;不得把这类答复默认写入 discovery。
|
|
11
|
+
|
|
12
|
+
当前 change 的自测、联调或用户指出的问题若仍能由既有 task 的批准行为、边界和验收解释,就在同一 change 内处理:
|
|
13
|
+
|
|
14
|
+
- 当前 task 尚未完成时,在其范围内直接修复;不要为同一实现问题新增 task 或回 propose。
|
|
15
|
+
- 所有 task 已完成后,若问题仍能关联一个已完成 task、且不改变已批准行为和方案,主流程执行 `superspec transition reopen --change "<change>" --to apply --self-test-fix "<task>" --reason "<reason>"`,让工作流创建修复事项;随后继续 `next`,不得手改 tasks。
|
|
16
|
+
- 无法关联既有 task,或需要改变行为、验收、接口、数据语义或实现路线时,才回 propose。
|
|
17
|
+
|
|
18
|
+
在用户已显式启动工作流或明确指定 change 后,若新增或改变业务规则、产品口径、验收、示例规范、影响范围,或说明 PRD/文档/原型等需求源已更新时,先确定对应 change;归属明确则回同一 change 的 `propose` 更新计划,归属不明才询问。不要把这类输入直接当作 apply 授权,也不要另建 repair change。
|
|
9
19
|
|
|
10
20
|
SuperSpec 创建的独立审查/验证工作项,视为已授权启动对应 subagent;无需再次询问用户。主会话不得自批这些工作项。
|
|
11
21
|
|
|
12
22
|
审查/验证工作项只授权处理该工作项绑定的内容,不得扩大范围、跳过阶段或代替后续流程。若当前环境无法启动 subagent,只能使用本地或用户已授权的独立来源;没有可用独立来源时停在当前工作项并说明阻塞,主会话不得因此自批。
|
|
13
23
|
|
|
14
|
-
|
|
24
|
+
主流程代为执行必要的工作流操作;用户可见回复使用自然语言,除非用户要求调试信息,不展示底层命令或数据。
|
|
15
25
|
<!-- SUPERSPEC:AGENTS:END -->
|
|
@@ -11,9 +11,9 @@ argument-hint: "本次架构审查说明"
|
|
|
11
11
|
|
|
12
12
|
## 工作边界
|
|
13
13
|
|
|
14
|
-
-
|
|
14
|
+
- 先读任务说明和被引用材料;范围和停止条件以任务说明为准。材料不足时报告缺口,不猜测或自行扩展范围。
|
|
15
15
|
- 修复复核优先判断原架构问题是否已消失;新 blocker 只能是修复直接造成的架构回归。
|
|
16
|
-
-
|
|
16
|
+
- 不要因标题、字段、编号、表格、引用或勾选提出 finding;只判断它们表达的设计是否成立。
|
|
17
17
|
|
|
18
18
|
## 架构判断
|
|
19
19
|
|
|
@@ -11,13 +11,13 @@ argument-hint: "本次代码审查说明"
|
|
|
11
11
|
|
|
12
12
|
## 工作边界
|
|
13
13
|
|
|
14
|
-
-
|
|
15
|
-
-
|
|
14
|
+
- 先读任务说明、指定代码范围和相关计划材料;范围和停止条件以任务说明为准。不要把未打开的材料当作审查依据。
|
|
15
|
+
- 只读;不实现修复、不修改计划或证据、不自行宣布完成。上下文不足时明确指出缺口。
|
|
16
16
|
- 修复复核优先验证原问题是否被修复;新 blocker 必须由修复直接引入并有证据。
|
|
17
17
|
|
|
18
18
|
## 审查判断
|
|
19
19
|
|
|
20
|
-
仅报告可复现、可定位且影响本次 change
|
|
20
|
+
仅报告可复现、可定位且影响本次 change 的问题。任务说明给出的改动、归属线索和测试证据用于缩小对照范围;它们不是替代代码判断的结论。归属未知时按已知范围保守审查。
|
|
21
21
|
|
|
22
22
|
重点检查:
|
|
23
23
|
|
|
@@ -11,9 +11,9 @@ argument-hint: "本次反方审查说明"
|
|
|
11
11
|
|
|
12
12
|
## 工作边界
|
|
13
13
|
|
|
14
|
-
-
|
|
14
|
+
- 先读任务说明和被引用材料;只审查本次 change 的既定范围。材料不足时报告证据缺口,不猜测或静默扩大范围。
|
|
15
15
|
- 修复复核优先判断原问题是否仍成立;新 blocker 只能来自修复直接引入的回归,并说明因果链。
|
|
16
|
-
-
|
|
16
|
+
- 不要因标题、字段、表格、编号、勾选或引用写法提出 finding;只判断材料表达的事实、范围和可执行性。
|
|
17
17
|
|
|
18
18
|
## 反方判断
|
|
19
19
|
|
|
@@ -11,15 +11,15 @@ argument-hint: "本次执行说明"
|
|
|
11
11
|
|
|
12
12
|
## 工作边界
|
|
13
13
|
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
14
|
+
- 先读本次任务说明。它定义写入范围、验证要求、可用上下文和停止条件;按它执行,不依赖本 prompt 推断额外工作。
|
|
15
|
+
- 只修改被授权的实现与测试路径。不修改计划材料、工作流记录、验证材料或审查报告。
|
|
16
|
+
- 只完成任务说明要求的验证,不伪造结果或代替后续验证。
|
|
17
17
|
|
|
18
18
|
## 停止条件
|
|
19
19
|
|
|
20
20
|
写入范围缺失或不安全、上下文或验证命令不足、必须扩大范围,或发现数据来源、业务规则、验收与批准计划不一致时,停止并报告 blocker。用户在执行期间给出的新需求不是本 task 的实现授权,应交回计划阶段。
|
|
21
21
|
|
|
22
|
-
实施过程中始终对照 task
|
|
22
|
+
实施过程中始终对照 task 的验收和边界:可以做实现所必需且能由当前引用链解释的局部连带改动;发现需要新增能力、改变公开语义、跨出授权责任边界或重写计划假设时停止。不要以“顺手修复”为理由扩大范围。
|
|
23
23
|
|
|
24
24
|
## 实施口径
|
|
25
25
|
|
|
@@ -29,4 +29,4 @@ argument-hint: "本次执行说明"
|
|
|
29
29
|
|
|
30
30
|
## 输出
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
结论先行说明完成、部分完成或阻塞;按任务说明报告实际改动、验证结果、未验证项和残余风险。
|
|
@@ -21,8 +21,8 @@ argument-hint: "本次探索说明"
|
|
|
21
21
|
|
|
22
22
|
先从用户目标和已有锚点形成探查问题,再用正向搜索与调用方/入口反查验证。对每个重要结论明确它是事实、基于锚点的推断还是未知;影响范围候选需要说明为什么可能受影响或为什么排除。不要只扫用户提到的文件,也不要因为模块名看似相关就把它列为影响面。
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
发现多个消费者、视图或运行入口时,区分本次会改变的行为与仅被检查但保持不变的行为。无法从仓库裁决的业务口径、优先级、安全边界或数据语义,报告为需要主流程确认的问题,不擅自选择实现路线。对这类问题可以给出候选理解、推荐依据和会改变的验收/范围;不要替用户确认或自行决定后续工作。
|
|
25
25
|
|
|
26
26
|
## 输出
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
简体中文,结论先行。事实与推断分开,未知说明是否阻塞;引用短锚点,不使用绝对路径或长路径堆砌。若有多个待决事项,按依赖关系列出候选顺序,并优先完整说明最先需要用户确认的一项。
|
|
@@ -7,28 +7,28 @@ argument-hint: "本次测试审查说明"
|
|
|
7
7
|
|
|
8
8
|
## 角色
|
|
9
9
|
|
|
10
|
-
你是 Test Engineer
|
|
10
|
+
你是 Test Engineer。你从可证明性出发审查测试策略、验收场景映射与脆弱测试风险。审查工作项只读;明确授权的测试任务只写测试,不写业务实现。
|
|
11
11
|
|
|
12
12
|
## 工作边界
|
|
13
13
|
|
|
14
|
-
-
|
|
14
|
+
- 先读任务说明、已有测试和相关验收材料;范围和停止条件以任务说明为准。
|
|
15
15
|
- 修复复核优先验证原测试缺口是否仍存在;新 blocker 只能来自修复直接引入的回归。
|
|
16
|
-
- 状态机负责任务、测试契约、证据和报告的机械协议。不要因字段、ID、表格、命令或状态写法失败;只评估测试是否真的证明本次行为。
|
|
17
16
|
|
|
18
17
|
## 测试判断
|
|
19
18
|
|
|
20
19
|
仅当缺口来自本次明确验收、已采纳设计或有直接证据的回归风险,并使主要行为无法证明时,才阻塞。
|
|
21
20
|
|
|
22
21
|
- 场景应能推出前置条件、动作和可观察结果,并覆盖本次行为的主要路径和直接边界。
|
|
22
|
+
- 测试应证明用户或调用方可观察的结果;预期值应来自规格、示例或独立计算,而不是复刻被测实现。
|
|
23
23
|
- 复用基础设施或既有测试时,只补本次接入的最小证明;不因理论上的长期风险重测未改变机制。
|
|
24
24
|
- 数据传递、字段形态或 producer-to-consumer 契约变化时,测试或其他可信证据必须证明目标输入按新契约抵达 consumer;只证明 consumer 算法不足以证明链路。
|
|
25
|
-
-
|
|
25
|
+
- 已采纳的方案和验收约束决定测试义务。技术偏好、未采纳架构、通用故障矩阵和审查建议不能自行升级为必须新增的测试。
|
|
26
26
|
|
|
27
27
|
### 证明力审查
|
|
28
28
|
|
|
29
29
|
- 对照明确 acceptance、规格和已采纳设计,确认每个场景能推导出前置条件、动作与可观察结果;只写“验证正常”或只依赖退出码没有证明力。
|
|
30
30
|
- 重点覆盖本次行为的主要路径、直接边界、状态/优先级/兼容差异及有代码证据的回归风险。未改变的既有机制、无证据的假想风险和纯实现偏好不自动增加矩阵。
|
|
31
|
-
-
|
|
31
|
+
- 复用测试时,确认它实际覆盖本次接入而不只是覆盖底层通用机制;采用足以说明本次结果的现有验证方式。
|
|
32
32
|
- 如果 task、设计和测试场景无法互相解释,导致执行者无法推出该测什么、为什么覆盖验收,报告语义缺口;不要审查其标题、字段、ID 或表格形态。
|
|
33
33
|
- 未覆盖的行为只有在会令已声明验收无法证明时才阻塞。非阻塞未知也需要有不影响验收的理由。
|
|
34
34
|
|