@peterxiaoyang/superspec 0.1.45 → 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 +67 -2
- package/dist/format.js +273 -22
- package/dist/openspec.d.ts +13 -0
- package/dist/openspec.js +53 -4
- package/dist/phase_confirmation.js +71 -2
- package/dist/phase_plan.d.ts +6 -1
- package/dist/phase_plan.js +180 -32
- 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 +222 -28
- package/dist/types.d.ts +42 -0
- package/package.json +1 -1
- package/templates/workflow/AGENTS.md +15 -5
- package/templates/workflow/agents/architect.toml +1 -1
- package/templates/workflow/agents/code-reviewer.toml +1 -1
- package/templates/workflow/agents/critic.toml +1 -1
- package/templates/workflow/agents/executor.toml +1 -1
- package/templates/workflow/agents/explore.toml +1 -1
- package/templates/workflow/agents/test-engineer.toml +1 -1
- package/templates/workflow/agents/test-runner.toml +1 -1
- package/templates/workflow/agents/verifier.toml +1 -1
- package/templates/workflow/prompts/architect.md +25 -33
- package/templates/workflow/prompts/code-reviewer.md +19 -67
- package/templates/workflow/prompts/critic.md +36 -86
- package/templates/workflow/prompts/executor.md +17 -19
- package/templates/workflow/prompts/explore.md +12 -46
- package/templates/workflow/prompts/test-engineer.md +22 -34
- package/templates/workflow/prompts/test-runner.md +11 -21
- package/templates/workflow/prompts/verifier.md +13 -37
- package/templates/workflow/skills/superspec-apply/SKILL.md +26 -26
- package/templates/workflow/skills/superspec-explore/SKILL.md +69 -60
- package/templates/workflow/skills/superspec-propose/SKILL.md +85 -133
- package/templates/workflow/skills/superspec-review/SKILL.md +14 -44
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, 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)" };
|
|
@@ -826,6 +967,8 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
826
967
|
finding_id: ref.findingId,
|
|
827
968
|
decision_scope: scope,
|
|
828
969
|
baseline_docs: proposalDocsBaseline(changeRoot),
|
|
970
|
+
planning_validation_version: 2,
|
|
971
|
+
planning_validation_profile: planningValidationProfileForNewRound(projectRoot),
|
|
829
972
|
},
|
|
830
973
|
};
|
|
831
974
|
}
|
|
@@ -850,6 +993,7 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
850
993
|
else if (type !== "implementation") {
|
|
851
994
|
return { skip: true, message: "这个代码审查问题不能直接回到实现阶段处理" };
|
|
852
995
|
}
|
|
996
|
+
const fix = reviewFixDescriptor(ref, found.finding);
|
|
853
997
|
return {
|
|
854
998
|
fromState: "apply_done",
|
|
855
999
|
toState: "apply",
|
|
@@ -859,9 +1003,56 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
859
1003
|
review_fix_of: `${ref.jobId}#${ref.findingId}`,
|
|
860
1004
|
source_job_id: ref.jobId,
|
|
861
1005
|
finding_id: ref.findingId,
|
|
1006
|
+
fix,
|
|
1007
|
+
},
|
|
1008
|
+
postCommit: (_pr, _ch, cr) => {
|
|
1009
|
+
appendFixTask(cr, fix);
|
|
862
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()),
|
|
863
1054
|
postCommit: (_pr, _ch, cr) => {
|
|
864
|
-
|
|
1055
|
+
appendFixTask(cr, fix);
|
|
865
1056
|
},
|
|
866
1057
|
};
|
|
867
1058
|
}
|
|
@@ -878,6 +1069,7 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
878
1069
|
reopen_target: "explore",
|
|
879
1070
|
reopen_source: snapshot.state,
|
|
880
1071
|
baseline_docs: discoveryDocsBaseline(changeRoot),
|
|
1072
|
+
...exploreAnswerRegistrationPayloadForChange(changeRoot),
|
|
881
1073
|
},
|
|
882
1074
|
extraEvents: planningReopenExtraEvents(snapshot, "explore", reason.trim()),
|
|
883
1075
|
};
|
|
@@ -915,6 +1107,8 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
915
1107
|
reopen_source: snapshot.state,
|
|
916
1108
|
baseline_source: acceptedBaseline ? (baselineNeedsBackfill ? "accepted_backfill" : "accepted") : "reopen_fallback",
|
|
917
1109
|
baseline_docs: baselineDocs,
|
|
1110
|
+
planning_validation_version: 2,
|
|
1111
|
+
planning_validation_profile: planningValidationProfileForNewRound(projectRoot),
|
|
918
1112
|
},
|
|
919
1113
|
extraEvents: planningReopenExtraEvents(snapshot, "propose", reason.trim()),
|
|
920
1114
|
};
|
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[];
|
|
@@ -156,6 +183,16 @@ export interface Event {
|
|
|
156
183
|
payload: Record<string, unknown>;
|
|
157
184
|
event_digest: string;
|
|
158
185
|
}
|
|
186
|
+
export type OpenSpecValidationProfile = {
|
|
187
|
+
mode: "disabled";
|
|
188
|
+
} | {
|
|
189
|
+
mode: "strict";
|
|
190
|
+
config_digest: string;
|
|
191
|
+
};
|
|
192
|
+
export interface PlanningValidationProfile {
|
|
193
|
+
version: 2;
|
|
194
|
+
openspec: OpenSpecValidationProfile;
|
|
195
|
+
}
|
|
159
196
|
export interface TransitionCommitPayload {
|
|
160
197
|
transition: string;
|
|
161
198
|
from_state: State;
|
|
@@ -190,6 +227,10 @@ export interface TransitionCommitPayload {
|
|
|
190
227
|
workflow_mode?: "minimal" | "normal" | "strict";
|
|
191
228
|
/** v2 起所有普通任务必须有五字段执行依据;缺失表示旧 change,沿用旧规则回放。 */
|
|
192
229
|
execution_requirement_version?: 2;
|
|
230
|
+
/** 新 planning round 的格式协议版本;缺失表示升级前的 v1 change。 */
|
|
231
|
+
planning_validation_version?: 2;
|
|
232
|
+
/** propose-ready 成功时冻结的 OpenSpec 校验 profile,start-apply 只回放此快照。 */
|
|
233
|
+
planning_validation_profile?: PlanningValidationProfile;
|
|
193
234
|
execution_policy?: ExecutionPolicy;
|
|
194
235
|
}
|
|
195
236
|
export interface Snapshot {
|
|
@@ -213,6 +254,7 @@ export interface TaskAttempt {
|
|
|
213
254
|
task_id: string;
|
|
214
255
|
state: AttemptState;
|
|
215
256
|
task_structure_digest: string;
|
|
257
|
+
fix?: FixDescriptor | null;
|
|
216
258
|
contract?: ExecutionContract | null;
|
|
217
259
|
contract_mode?: boolean;
|
|
218
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 -->
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# SuperSpec Codex agent: architect
|
|
2
2
|
name = "architect"
|
|
3
3
|
description = "System design, boundaries, interfaces, long-horizon tradeoffs"
|
|
4
|
-
model_reasoning_effort = "
|
|
4
|
+
model_reasoning_effort = "medium"
|
|
5
5
|
developer_instructions = """
|
|
6
6
|
Role: Architect. Review system boundaries, interface contracts, data flow, maintenance risk, rollback risk, and design tradeoffs.
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# SuperSpec Codex agent: code-reviewer
|
|
2
2
|
name = "code-reviewer"
|
|
3
3
|
description = "Code-level review for spec fit, bugs, safety, and test gaps"
|
|
4
|
-
model_reasoning_effort = "
|
|
4
|
+
model_reasoning_effort = "medium"
|
|
5
5
|
developer_instructions = """
|
|
6
6
|
Role: Code Reviewer. Check spec fit, correctness, security, test adequacy, code quality, performance, and maintainability without making the workflow heavy.
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# SuperSpec Codex agent: critic
|
|
2
2
|
name = "critic"
|
|
3
3
|
description = "Plan/design critical challenge and review"
|
|
4
|
-
model_reasoning_effort = "
|
|
4
|
+
model_reasoning_effort = "medium"
|
|
5
5
|
developer_instructions = """
|
|
6
6
|
Role: Critic. Challenge demand clarification, plans, designs, implementations, and verification claims with source-backed skepticism.
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# SuperSpec Codex agent: executor
|
|
2
2
|
name = "executor"
|
|
3
3
|
description = "Bounded SuperSpec apply implementation worker"
|
|
4
|
-
model_reasoning_effort = "
|
|
4
|
+
model_reasoning_effort = "medium"
|
|
5
5
|
developer_instructions = """
|
|
6
6
|
Role: Executor. Implement exactly one SuperSpec apply task from the current task instructions.
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# SuperSpec Codex agent: explore
|
|
2
2
|
name = "explore"
|
|
3
3
|
description = "Repo-local read-only factual scan for SuperSpec discovery"
|
|
4
|
-
model_reasoning_effort = "
|
|
4
|
+
model_reasoning_effort = "medium"
|
|
5
5
|
developer_instructions = """
|
|
6
6
|
Role: Explore. Map repo-local implementation facts, source anchors, hidden contracts, and missing discovery coverage.
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# SuperSpec Codex agent: test-engineer
|
|
2
2
|
name = "test-engineer"
|
|
3
3
|
description = "Test strategy, coverage, flaky-test hardening"
|
|
4
|
-
model_reasoning_effort = "
|
|
4
|
+
model_reasoning_effort = "medium"
|
|
5
5
|
developer_instructions = """
|
|
6
6
|
Role: Test Engineer. Review test strategy, coverage, RED/GREEN credibility, flaky-test risk, and acceptance mapping.
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# SuperSpec Codex agent: test-runner
|
|
2
2
|
name = "test-runner"
|
|
3
3
|
description = "Bounded SuperSpec apply test execution worker"
|
|
4
|
-
model_reasoning_effort = "
|
|
4
|
+
model_reasoning_effort = "medium"
|
|
5
5
|
developer_instructions = """
|
|
6
6
|
Role: Test Runner. Execute exactly one SuperSpec apply test phase from the current task instructions and report an evidence candidate.
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# SuperSpec Codex agent: verifier
|
|
2
2
|
name = "verifier"
|
|
3
3
|
description = "Completion evidence, claim validation, test adequacy"
|
|
4
|
-
model_reasoning_effort = "
|
|
4
|
+
model_reasoning_effort = "medium"
|
|
5
5
|
developer_instructions = """
|
|
6
6
|
Role: Verifier. Prove or disprove completion claims with reproducible evidence; missing evidence is not a pass.
|
|
7
7
|
|