@guanlin4924/devflow-pi 0.1.85 → 0.1.88

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.
Files changed (46) hide show
  1. package/README.md +4 -0
  2. package/package.json +1 -1
  3. package/plugins/devflow-core/extensions/devflow-core.ts +211 -19
  4. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +15 -0
  5. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +49 -3
  6. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checker_adapter.rb +83 -0
  7. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checker_registry.rb +19 -0
  8. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checkpoint_meta.rb +46 -0
  9. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +10 -1
  10. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_runtime.rb +32 -0
  11. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_state.rb +44 -41
  12. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_workflow.rb +5 -4
  13. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +15 -4
  14. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +15 -9
  15. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/review_preflight.py +238 -0
  16. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/review_run.py +234 -32
  17. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +63 -11
  18. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/workflow_gate_contract.rb +5 -1
  19. package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +40 -31
  20. package/plugins/devflow-core/skills/workflow-pipeline/SKILL.md +11 -5
  21. package/plugins/devflow-core/skills/workflow-pipeline/references/pipeline-flow.md +6 -6
  22. package/plugins/devflow-implementation/skills/workflow-change-plan/SKILL.md +16 -1
  23. package/plugins/devflow-implementation/skills/workflow-change-plan/references/output-template.md +3 -1
  24. package/plugins/devflow-quality/agents/code-reviewer.md +7 -1
  25. package/plugins/devflow-quality/skills/code-review/SKILL.md +10 -0
  26. package/plugins/devflow-quality/skills/workflow-gen-test-guide/SKILL.md +37 -0
  27. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/SKILL.md +327 -0
  28. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/assets/output-template.md +213 -0
  29. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/bug-rules.md +9 -0
  30. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/coverage-checklist.md +63 -0
  31. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/exception-checklist.md +37 -0
  32. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/api-test.md +58 -0
  33. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/boundary-test.md +151 -0
  34. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/common-test.md +49 -0
  35. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/compatibility-test.md +105 -0
  36. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/data-test.md +67 -0
  37. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/exception-test.md +109 -0
  38. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/function-test.md +65 -0
  39. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/integration-test.md +36 -0
  40. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/performance-test.md +100 -0
  41. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/scenario-test.md +63 -0
  42. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/security-test.md +55 -0
  43. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/ui-test.md +49 -0
  44. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/usability-test.md +46 -0
  45. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/writing-guide.md +111 -0
  46. package/src/constants.ts +1 -1
package/README.md CHANGED
@@ -117,6 +117,10 @@ Devflow 会在输入框上方显示“当前 Feature 交付链路”:它基于
117
117
 
118
118
  正常使用不需要记忆底层命令:在 Devflow 工作流中按步骤继续即可。需要排查时,可用 `devflow_acceptance_tests` 查看冻结套件,或由工作流调用 `devflow_tdd_evidence` 与 `devflow_run_acceptance_tests` 生成 Core-owned 证据;不要手写“已测试”来替代结果文件。
119
119
 
120
+ ### Core 软门禁模式
121
+
122
+ 默认情况下,Core 生成的测试绑定、追溯、merge gate、产物有效性和图谱新鲜度问题会记录为 advisory,交给 AI 审查决定继续、修改或阻塞;只有控制面损坏、越权路径、并发冲突等安全问题仍会硬阻断。需要临时恢复旧的严格门禁时,启动前设置 `DEVFLOW_SOFT_GATES=0`。
123
+
120
124
  ## 更新
121
125
 
122
126
  升级 npm 发行版:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanlin4924/devflow-pi",
3
- "version": "0.1.85",
3
+ "version": "0.1.88",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -1,4 +1,4 @@
1
- import { createHash, randomUUID } from "node:crypto";
1
+ import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
2
2
  import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
3
3
  import { appendFile, mkdir, rename, writeFile } from "node:fs/promises";
4
4
  import path from "node:path";
@@ -183,6 +183,7 @@ const READ_ONLY_TOOLS = new Set([
183
183
  "devflow_usage_stats",
184
184
  "usage_stats",
185
185
  "devflow_project_context",
186
+ "devflow_review_preflight",
186
187
  ]);
187
188
  const REQUIREMENT_DRAFT_TOOLS = new Set([...READ_ONLY_TOOLS, "ask"]);
188
189
  // A Todo update changes only the client-visible task list, never the project
@@ -526,7 +527,7 @@ const CONTROL_PATHS = [
526
527
  ".planning/artifacts/index.yaml",
527
528
  ];
528
529
 
529
- const CONTROL_PREFIXES = [".planning/workflows/", ".planning/contracts/", ".planning/evidence/"];
530
+ const CONTROL_PREFIXES = [".planning/workflows/", ".planning/contracts/", ".planning/evidence/", ".planning/review-runs/"];
530
531
 
531
532
  const REQUIRED_EXTENSION_TOOLS = [
532
533
  "devflow_doctor",
@@ -570,6 +571,7 @@ const REQUIRED_SKILLS = [
570
571
  "workflow-execute-phase",
571
572
  "workflow-feature-breakdown",
572
573
  "workflow-frontend-tech-spec",
574
+ "workflow-gen-test-guide",
573
575
  "workflow-generate-mainflow-cases",
574
576
  "workflow-integration-verify",
575
577
  "workflow-orchestrator",
@@ -621,6 +623,7 @@ const REQUIRED_SCRIPTS = [
621
623
  "workflow-orchestrator/scripts/tdd_evidence.py",
622
624
  "workflow-orchestrator/scripts/acceptance_test_runner.py",
623
625
  "workflow-orchestrator/scripts/machine_evidence.rb",
626
+ "workflow-orchestrator/scripts/review_preflight.py",
624
627
  "workflow-orchestrator/scripts/cross_track_consistency_check.rb",
625
628
  "workflow-orchestrator/scripts/execution_contract.rb",
626
629
  "workflow-code-index/scripts/code_index.py",
@@ -699,6 +702,7 @@ function assertRuntimeProjectBoundary(cwd: string): void {
699
702
  ".planning/.devflow.lock",
700
703
  ".planning/workflows",
701
704
  ".planning/artifacts",
705
+ ".planning/review-runs",
702
706
  ]) {
703
707
  const target = path.join(resolvedCwd, relative);
704
708
  try {
@@ -746,7 +750,9 @@ function isCoreOwnedGovernancePath(filePath: string): boolean {
746
750
  // same ownership extends to its views, machine evidence, and audit records.
747
751
  return isCanonicalRequirementGraphPath(filePath)
748
752
  || /^\.planning\/evidence\/[^/\\\x00-\x1F\x7F]+\/[^/\\]+\.machine\.json$/u.test(filePath)
749
- || /^\.planning\/contracts\/[^/\\\x00-\x1F\x7F]+\/(?:EC-[A-Fa-f0-9]+\.json|control-events\.jsonl)$/u.test(filePath);
753
+ || /^\.planning\/contracts\/[^/\\\x00-\x1F\x7F]+\/(?:EC-[A-Fa-f0-9]+\.json|control-events\.jsonl)$/u.test(filePath)
754
+ || filePath.startsWith(".planning/review-runs/")
755
+ || /^\.planning\/phases\/[^/\\\x00-\x1F\x7F]+\/REVIEW\.md$/u.test(filePath);
750
756
  }
751
757
 
752
758
  function planningFeatureFromPath(filePath: string): string | null {
@@ -892,6 +898,17 @@ function parseJsonResult(result: ProcessResult): Record<string, unknown> | null
892
898
  }
893
899
  }
894
900
 
901
+ export function canonicalReviewJson(value: unknown): string {
902
+ if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
903
+ if (typeof value === "number" && Number.isFinite(value)) return JSON.stringify(value);
904
+ if (Array.isArray(value)) return `[${value.map(canonicalReviewJson).join(",")}]`;
905
+ if (value && typeof value === "object") {
906
+ const record = value as Record<string, unknown>;
907
+ return `{${Object.keys(record).sort().map(key => `${JSON.stringify(key)}:${canonicalReviewJson(record[key])}`).join(",")}}`;
908
+ }
909
+ throw new Error("Review JSON contains a non-JSON value");
910
+ }
911
+
895
912
  /** Ruby control-plane scripts report expected validation failures as JSON with
896
913
  * `ok: false` and intentionally keep a zero process exit code. Never mistake
897
914
  * that structured failure for a successful state mutation. */
@@ -917,12 +934,19 @@ function compactStep(value: unknown): Record<string, unknown> | null {
917
934
  required: record.required,
918
935
  exists: record.exists,
919
936
  artifact_status: record.artifact_status,
937
+ content_valid: record.content_valid,
920
938
  };
921
939
  })
922
940
  : undefined,
923
941
  };
924
942
  }
925
943
 
944
+ function selectedWorkflowStep(snapshot: Record<string, unknown>): Record<string, unknown> | null {
945
+ return snapshot.action === "run_parallel_window"
946
+ ? compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step)
947
+ : compactStep(snapshot.current_step) ?? compactStep(snapshot.next_step);
948
+ }
949
+
926
950
  /**
927
951
  * The approval prompt is session-local, while the completed step outputs are
928
952
  * durable. After a rollover, compaction, or runtime restart the in-memory
@@ -937,6 +961,10 @@ function isReviewableWorkflowSnapshot(snapshot: Record<string, unknown>): boolea
937
961
  if (!step || typeof step !== "object") return false;
938
962
  const stepRecord = step as Record<string, unknown>;
939
963
  if (typeof stepRecord.id !== "string" || !stepRecord.id) return false;
964
+ // Core-generated quality findings are advisory by default. Re-arm the AI
965
+ // review even when an output is stale or its generated validity metadata is
966
+ // incomplete; only the control-plane shape itself remains a hard boundary.
967
+ if (process.env.DEVFLOW_SOFT_GATES !== "0" && snapshot.action === "await_user_confirmation") return true;
940
968
  const outputs = Array.isArray(stepRecord.outputs) ? stepRecord.outputs : [];
941
969
  const requiredOutputs = outputs.filter(output => {
942
970
  if (!output || typeof output !== "object") return false;
@@ -945,7 +973,13 @@ function isReviewableWorkflowSnapshot(snapshot: Record<string, unknown>): boolea
945
973
  return requiredOutputs.length > 0 && requiredOutputs.every(output => {
946
974
  if (!output || typeof output !== "object") return false;
947
975
  const record = output as Record<string, unknown>;
948
- return record.exists === true && !["failed", "blocked"].includes(String(record.artifact_status ?? ""));
976
+ // Older/current inspect_state payloads may omit content_valid even when
977
+ // Core has already validated and registered the artifact. Treat an
978
+ // explicit false as invalid, but use exists + artifact_status as the
979
+ // durable fallback when the field is absent.
980
+ return record.exists === true
981
+ && record.content_valid !== false
982
+ && !["failed", "blocked", "planned"].includes(String(record.artifact_status ?? ""));
949
983
  });
950
984
  }
951
985
 
@@ -968,9 +1002,7 @@ export function workflowResumeDisposition(snapshot: Record<string, unknown>): Wo
968
1002
 
969
1003
  const feature = typeof snapshot.current_feature === "string" ? snapshot.current_feature : "";
970
1004
  const revision = snapshot.revision;
971
- const selectedStep = snapshot.action === "run_parallel_window"
972
- ? compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step)
973
- : compactStep(snapshot.current_step) ?? compactStep(snapshot.next_step);
1005
+ const selectedStep = selectedWorkflowStep(snapshot);
974
1006
  const stepId = typeof selectedStep?.id === "string" ? selectedStep.id : "";
975
1007
  if (!feature || !stepId || typeof revision !== "number" || !Number.isInteger(revision)) return "idle";
976
1008
 
@@ -987,9 +1019,17 @@ export function workflowResumeDisposition(snapshot: Record<string, unknown>): Wo
987
1019
  }
988
1020
 
989
1021
  function workflowDirective(snapshot: Record<string, unknown>, source: "start" | "approval", requirementDraft?: string): string {
990
- const selectedStep = snapshot.action === "run_parallel_window"
991
- ? compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step)
992
- : compactStep(snapshot.current_step) ?? compactStep(snapshot.next_step);
1022
+ const selectedStep = selectedWorkflowStep(snapshot);
1023
+ const reviewInstructions = selectedStep?.id === "code-review"
1024
+ ? [
1025
+ "派发审查 task 前先调用 devflow_review_preflight,使用当前 Core 冻结契约和审查者 devflow-code-reviewer;只有 ready 才派发,human_confirmation 与 core_contract_conflict 均停止并上报。ready 只是可执行性预检,不是审查通过。",
1026
+ "本步骤由主 Agent 使用 task 显式指定 devflow-code-reviewer 执行独立只读审查;主 Agent 不得代替审查者给出 clean 结论。",
1027
+ "任务包只能引用已确认的 Core 需求/契约、固定版本源码和测试证据;主 Agent 的摘要不是验收事实。审查者须独立回读事实与源码,返回发现、证据、未决人工问题和改进建议。",
1028
+ "若需第二或第三轮审查,任务包必须携带前轮 chain_id,审查者须在 JSON 中原样返回;每轮限一个独立审查 task,最多三轮。P0/P1 影响和触发条件必须引用预先冻结的 Core 测试用例事实,缺失则转人工确认。",
1029
+ "审查 task 返回严格 JSON 后,主 Agent 调用 devflow_review_submit({}) 提交该 task 的原始结果;不得重写观察项或自报 clean。随后将 Core 返回的裁决、证据链、开放 finding 和下一步动作完整交给用户。",
1030
+ "审查报告及 task 返回文本不构成 Core 门禁通过;证据不足或审查服务返回 awaiting_human_confirmation 时必须保留阻断并报告给主 Agent。",
1031
+ ]
1032
+ : [];
993
1033
  const envelope = {
994
1034
  source,
995
1035
  action: snapshot.action,
@@ -1019,6 +1059,7 @@ function workflowDirective(snapshot: Record<string, unknown>, source: "start" |
1019
1059
  "Devflow 已通过启动预检。读取 skill://workflow-pipeline,并只执行当前项目的一个工作流步骤。",
1020
1060
  "先调用 devflow_state(action=\"inspect\") 重新确认 revision,再调用 devflow_contract(action=\"status\");若没有匹配当前 feature/current_step 的 fresh contract,立即调用 devflow_contract(action=\"activate\", feature=<current_feature>, step=<current_step>),激活成功后才能执行或写入。",
1021
1061
  "只按 inspector 返回的当前 step 加载所需 Skill:step.skill 是精确 ID,必须读取 skill://<step.skill>,不得自行添加 workflow- 前缀或改写名称;本回合严禁读取或写入 xd://devflow_transition,严禁调用 devflow_transition,严禁执行 Skill 中的 complete-step/confirm-checkpoint,不得推进下一步骤;状态写回和会话切换由 Runtime 在用户确认后独占处理。",
1062
+ ...reviewInstructions,
1022
1063
  ...(parallelInstructions.length > 0 ? parallelInstructions : [
1023
1064
  "完成本步骤的真实产物后,报告证据、未决项和验证结果并停止。请用户审查后明确回复“批准执行”才可推进;回复修改意见或“拒绝执行”则不得推进。",
1024
1065
  ]),
@@ -1041,7 +1082,7 @@ function projectInitializationDirective(summary: string): string {
1041
1082
 
1042
1083
  function explainWorkflowSnapshot(snapshot: Record<string, unknown> | null): string {
1043
1084
  if (!snapshot) return "Devflow 当前没有可解释的工作流状态。可使用 /devflow start <功能名> 创建新功能。";
1044
- const step = compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step);
1085
+ const step = selectedWorkflowStep(snapshot);
1045
1086
  const lines = [
1046
1087
  `功能:${String(snapshot.current_feature ?? "未开始")}`,
1047
1088
  `阶段:${workflowPhaseName(snapshot.current_phase)}`,
@@ -1133,11 +1174,11 @@ export function isShellControlMutation(toolName: string, input: unknown): boolea
1133
1174
  const looksLikeWrite = /(?:^|[;&|]\s*)(?:rm|mv|cp|install|truncate)\b|(?:^|\s)(?:tee|sed\s+-i|perl\s+-i)\b|(?:^|[\s;&|])(?:1)?>{1,2}(?![>=&])\s*\S/.test(
1134
1175
  command,
1135
1176
  );
1136
- return invokesWriter || (namesControlPath && looksLikeWrite);
1177
+ return invokesWriter || (namesControlPath && (looksLikeWrite || shellMutationPaths(command, ".").mutating));
1137
1178
  }
1138
1179
 
1139
1180
  function shellMutationPaths(command: string, cwd: string): { mutating: boolean; paths: string[] } {
1140
- const mutating = /(?:\bsed\s+-i\b|\bperl\s+-i\b|\b(?:cp|mv|rm|install|truncate|tee)\b|(?:^|\s)>{1,2}\s*\S|\b(?:python3?|node|ruby)\b[^\n]*(?:writeFile|write_text|open\([^\n]*['\"]w|unlink|rename)\b)/i.test(command);
1181
+ const mutating = /(?:\bsed\s+-i\b|\bperl\s+-i\b|\b(?:cp|mv|rm|install|truncate|tee)\b|(?:^|\s)>{1,2}(?![>=&])\s*\S|\b(?:python3?|node|ruby)\b[^\n]*(?:writeFile|write_text|open\([^\n]*['\"]w|unlink|rename)\b)/i.test(command);
1141
1182
  if (!mutating) return { mutating: false, paths: [] };
1142
1183
  const candidates = command.match(/(?:^|\s|>)([A-Za-z0-9_./~-]+)(?=\s|$|2?>)/g) ?? [];
1143
1184
  const paths = candidates
@@ -1162,6 +1203,7 @@ const CONTRACT_READ_TOOLS = new Set([
1162
1203
  "devflow_acceptance_tests",
1163
1204
  "devflow_doctor",
1164
1205
  "devflow_state",
1206
+ "devflow_review_preflight",
1165
1207
  "devflow_usage_stats",
1166
1208
  "usage_stats",
1167
1209
  ]);
@@ -1205,6 +1247,7 @@ function stepScopedMutationOwner(toolName: string, input: unknown): string | nul
1205
1247
  const step = input && typeof input === "object" ? (input as Record<string, unknown>).step : undefined;
1206
1248
  return typeof step === "string" && step ? step : null;
1207
1249
  }
1250
+ if (normalized === "devflow_review_submit" || normalized === "devflow_review_render") return "code-review";
1208
1251
  // New mutating Devflow tools must declare a step above or be explicitly
1209
1252
  // classified read/control-only; they never inherit a broad devflow_* bypass.
1210
1253
  return null;
@@ -1360,6 +1403,10 @@ export default function devflowCore(pi: ExtensionAPI) {
1360
1403
  let controlState: SessionControlState = initialControlState();
1361
1404
  let activeContract: ActiveExecutionContract | undefined;
1362
1405
  let contractAgentCalls = 0;
1406
+ const reviewBindingKey = randomBytes(32).toString("hex");
1407
+ const pendingReviewTasks = new Map<string, { feature: string; contractId: string }>();
1408
+ const completedReviewTasks = new Map<string, { feature: string; contractId: string; report: Record<string, unknown> }>();
1409
+ let latestReviewTaskCallId: string | undefined;
1363
1410
  let projectRoot: string | undefined;
1364
1411
 
1365
1412
  async function appendEngineEvidence(cwd: string | undefined, event: string, extra: Record<string, unknown> = {}) {
@@ -2049,7 +2096,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2049
2096
  snapshot: Record<string, unknown>,
2050
2097
  ): Promise<WorkflowResumeDisposition> {
2051
2098
  const disposition = workflowResumeDisposition(snapshot);
2052
- const stepId = String((compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step))?.id ?? "当前步骤");
2099
+ const stepId = String(selectedWorkflowStep(snapshot)?.id ?? "当前步骤");
2053
2100
  if (disposition === "execute") {
2054
2101
  setControlState(ctx, executingState(`恢复并执行 ${stepId}`));
2055
2102
  ctx.ui.notify(`Devflow 已恢复到 ${stepId},当前步骤产物尚未完成,正在继续执行。`, "info");
@@ -2602,7 +2649,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2602
2649
  }
2603
2650
  const revision = typeof inspected.snapshot.revision === "number" ? inspected.snapshot.revision : undefined;
2604
2651
  const checkpoint = inspected.snapshot.status === "waiting_checkpoint";
2605
- const step = compactStep(inspected.snapshot.next_step) ?? compactStep(inspected.snapshot.current_step);
2652
+ const step = selectedWorkflowStep(inspected.snapshot);
2606
2653
  const stepId = typeof step?.id === "string" ? step.id : undefined;
2607
2654
  if (revision === undefined || (!checkpoint && !stepId)) {
2608
2655
  ctx.ui.notify("当前状态缺少可确认的 revision 或步骤,拒绝推进。", "error");
@@ -2771,10 +2818,36 @@ export default function devflowCore(pi: ExtensionAPI) {
2771
2818
  ) {
2772
2819
  return { block: true, reason: `Active contract ${activeContract.contractId} only allows agents: ${activeContract.allowedAgents.join(", ")}` };
2773
2820
  }
2774
- if (activeContract && controlState.phase === "executing" && contractAgentCalls >= activeContract.toolBudget) {
2821
+ if (activeContract && controlState.phase === "executing" && contractAgentCalls + requested.names.length > activeContract.toolBudget) {
2775
2822
  return { block: true, reason: `Active contract ${activeContract.contractId} exhausted its fixed agent budget (${activeContract.toolBudget}).` };
2776
2823
  }
2777
- if (activeContract && controlState.phase === "executing") contractAgentCalls += 1;
2824
+ if (activeContract && controlState.phase === "executing" && activeContract.step === "code-review" && requested.names.length !== 1) {
2825
+ return { block: true, reason: "Code review requires exactly one independent reviewer task." };
2826
+ }
2827
+ if (activeContract && controlState.phase === "executing" && activeContract.step === "code-review") {
2828
+ const preflight = await runProcess([
2829
+ "python3", path.join(resolveKitRoot(ctx.cwd), "workflow-orchestrator/scripts/review_preflight.py"),
2830
+ "--root", ctx.cwd,
2831
+ "--feature", activeContract.feature,
2832
+ "--step", activeContract.step,
2833
+ "--contract", `.planning/contracts/${activeContract.feature}/${activeContract.contractId}.json`,
2834
+ "--obligations", "[]",
2835
+ "--review-agent", "devflow-code-reviewer",
2836
+ "--stage", "entry",
2837
+ ], ctx.cwd);
2838
+ let result: Record<string, unknown> | null = null;
2839
+ try {
2840
+ result = JSON.parse(preflight.stdout) as Record<string, unknown>;
2841
+ } catch {
2842
+ // An unparseable Core preflight result cannot authorize a reviewer task.
2843
+ }
2844
+ if (!preflight.ok || result?.status !== "ready") {
2845
+ return { block: true, reason: `Code review task preflight did not pass: ${preflight.stdout || preflight.stderr || "unavailable"}` };
2846
+ }
2847
+ if (!toolCallId) return { block: true, reason: "Code review task needs a runtime tool-call identity." };
2848
+ pendingReviewTasks.set(toolCallId, { feature: activeContract.feature, contractId: activeContract.contractId });
2849
+ }
2850
+ if (activeContract && controlState.phase === "executing") contractAgentCalls += requested.names.length;
2778
2851
  } else if (
2779
2852
  controlState.phase === "planning" &&
2780
2853
  toolName !== "devflow_contract" &&
@@ -2922,6 +2995,30 @@ export default function devflowCore(pi: ExtensionAPI) {
2922
2995
  });
2923
2996
 
2924
2997
  pi.on("tool_result", async (event, ctx) => {
2998
+ if (event.toolName === "task") {
2999
+ const binding = pendingReviewTasks.get(event.toolCallId);
3000
+ if (binding) {
3001
+ pendingReviewTasks.delete(event.toolCallId);
3002
+ const details = event.details && typeof event.details === "object" ? event.details as Record<string, unknown> : {};
3003
+ const results = Array.isArray(details.results) ? details.results : [];
3004
+ const child = results.length === 1 && results[0] && typeof results[0] === "object"
3005
+ ? results[0] as Record<string, unknown> : null;
3006
+ if (!event.isError && child?.agent === "devflow-code-reviewer" && child.exitCode === 0 && child.truncated === false && typeof child.output === "string") {
3007
+ try {
3008
+ const report = JSON.parse(child.output) as unknown;
3009
+ if (report && typeof report === "object" && !Array.isArray(report)
3010
+ && !("core_review_binding" in report)
3011
+ && (report as Record<string, unknown>).target && typeof (report as Record<string, unknown>).target === "object"
3012
+ && ((report as Record<string, unknown>).target as Record<string, unknown>).feature === binding.feature) {
3013
+ completedReviewTasks.set(event.toolCallId, { ...binding, report: report as Record<string, unknown> });
3014
+ latestReviewTaskCallId = event.toolCallId;
3015
+ }
3016
+ } catch {
3017
+ // A prose or malformed child response is not an attested review.
3018
+ }
3019
+ }
3020
+ }
3021
+ }
2925
3022
  const taskContext = parallelTaskContexts.get(event.toolCallId);
2926
3023
  if (parallelRunActive && event.toolName === "task") {
2927
3024
  if (taskResultHasSuccessfulDispatch(event.details)) {
@@ -3667,7 +3764,7 @@ export default function devflowCore(pi: ExtensionAPI) {
3667
3764
  parameters: z.object({
3668
3765
  feature: z.string().min(1),
3669
3766
  phase: z.string().min(1).optional(),
3670
- flow: z.string().min(1).default("feature-development"),
3767
+ flow: z.enum(["feature-development", "acceptance-remediation"]).default("feature-development"),
3671
3768
  force: z.boolean().default(false),
3672
3769
  }),
3673
3770
  approval: "write",
@@ -3822,6 +3919,101 @@ export default function devflowCore(pi: ExtensionAPI) {
3822
3919
  },
3823
3920
  });
3824
3921
 
3922
+ pi.registerTool({
3923
+ name: "devflow_review_preflight",
3924
+ label: "Devflow Review Preflight",
3925
+ description: "Read-only Core preflight for current step facts, contract revisions, production obligations, and reviewer task reachability. Ready is not gate approval.",
3926
+ parameters: z.object({
3927
+ feature: z.string().min(1),
3928
+ step: z.string().min(1),
3929
+ contract: z.string().min(1),
3930
+ obligations: z.array(z.record(z.string(), z.unknown())).default([]),
3931
+ reviewAgent: z.string().optional(),
3932
+ stage: z.enum(["entry", "completion"]).default("entry"),
3933
+ }),
3934
+ approval: "read",
3935
+ async execute(_id, params, _signal, _onUpdate, ctx) {
3936
+ assertRuntimeProjectBoundary(ctx.cwd);
3937
+ return renderResult(await runProcess([
3938
+ "python3", path.join(resolveKitRoot(ctx.cwd), "workflow-orchestrator/scripts/review_preflight.py"),
3939
+ "--root", ctx.cwd,
3940
+ "--feature", params.feature,
3941
+ "--step", params.step,
3942
+ "--contract", params.contract,
3943
+ "--obligations", JSON.stringify(params.obligations),
3944
+ "--stage", params.stage,
3945
+ ...(params.reviewAgent ? ["--review-agent", params.reviewAgent] : []),
3946
+ ], ctx.cwd));
3947
+ },
3948
+ });
3949
+
3950
+ pi.registerTool({
3951
+ name: "devflow_review_submit",
3952
+ label: "Devflow Bound Review Submission",
3953
+ description: "Submit the exact JSON returned by the completed independent reviewer task to Core. Caller text cannot replace the child report.",
3954
+ parameters: z.object({ taskCallId: z.string().optional() }),
3955
+ approval: "write",
3956
+ async execute(_id, params, _signal, _onUpdate, ctx) {
3957
+ assertRuntimeProjectBoundary(ctx.cwd);
3958
+ const taskCallId = params.taskCallId || latestReviewTaskCallId;
3959
+ const captured = taskCallId ? completedReviewTasks.get(taskCallId) : undefined;
3960
+ if (!taskCallId || !captured || activeContract?.step !== "code-review"
3961
+ || activeContract.feature !== captured.feature || activeContract.contractId !== captured.contractId) {
3962
+ return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "No completed reviewer task is bound to the active code-review contract." });
3963
+ }
3964
+ const reportSha256 = createHash("sha256").update(canonicalReviewJson(captured.report), "utf8").digest("hex");
3965
+ const identity = {
3966
+ agent: "devflow-code-reviewer",
3967
+ feature: captured.feature,
3968
+ contract_id: captured.contractId,
3969
+ task_call_id: taskCallId,
3970
+ report_sha256: reportSha256,
3971
+ };
3972
+ const signature = createHmac("sha256", reviewBindingKey).update(canonicalReviewJson(identity), "utf8").digest("hex");
3973
+ const request = { ...captured.report, core_review_binding: { ...identity, signature } };
3974
+ const result = await runProcess(
3975
+ ["python3", path.join(resolveKitRoot(ctx.cwd), "workflow-orchestrator/scripts/review_run.py"), ctx.cwd],
3976
+ ctx.cwd,
3977
+ { DEVFLOW_REVIEW_BINDING_KEY: reviewBindingKey },
3978
+ JSON.stringify(request),
3979
+ );
3980
+ const submitted = parseJsonResult(result);
3981
+ if (!result.ok || !submitted) return renderResult(result, { taskCallId, feature: captured.feature, contractId: captured.contractId });
3982
+ completedReviewTasks.delete(taskCallId);
3983
+ if (!["clean", "findings", "terminated_p0"].includes(String(submitted.status))) {
3984
+ return renderResult(result, { taskCallId, feature: captured.feature, contractId: captured.contractId });
3985
+ }
3986
+ const script = path.join(resolveKitRoot(ctx.cwd), "workflow-orchestrator/scripts/review_run.py");
3987
+ const rendered = await runProcess(["python3", script, ctx.cwd, "--render", captured.feature, captured.contractId], ctx.cwd);
3988
+ const renderPayload = parseJsonResult(rendered);
3989
+ if (!rendered.ok || renderPayload?.status !== "rendered") {
3990
+ return renderResult({ ok: false, exitCode: 1, stdout: JSON.stringify({ review_result: submitted, render: renderPayload }), stderr: rendered.stderr || "Core review report rendering failed; call devflow_review_render to retry." }, { taskCallId });
3991
+ }
3992
+ const gated = await runProcess(["python3", script, ctx.cwd, "--gate", captured.feature, captured.contractId], ctx.cwd);
3993
+ const gatePayload = parseJsonResult(gated);
3994
+ if (!gated.ok || !gatePayload) return renderResult(gated, { taskCallId, review_result: submitted });
3995
+ return renderResult({ ok: true, exitCode: 0, stdout: JSON.stringify({ review_result: submitted, gate: gatePayload }), stderr: "" }, { taskCallId, feature: captured.feature, contractId: captured.contractId });
3996
+ },
3997
+ });
3998
+
3999
+ pi.registerTool({
4000
+ name: "devflow_review_render",
4001
+ label: "Devflow Core Review Report",
4002
+ description: "Regenerate the code-review Markdown view from the latest Core-bound review round; it does not change the verdict.",
4003
+ parameters: z.object({}),
4004
+ approval: "write",
4005
+ async execute(_id, _params, _signal, _onUpdate, ctx) {
4006
+ assertRuntimeProjectBoundary(ctx.cwd);
4007
+ if (activeContract?.step !== "code-review") {
4008
+ return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "A current code-review contract is required." });
4009
+ }
4010
+ return renderResult(await runProcess([
4011
+ "python3", path.join(resolveKitRoot(ctx.cwd), "workflow-orchestrator/scripts/review_run.py"),
4012
+ ctx.cwd, "--render", activeContract.feature, activeContract.contractId,
4013
+ ], ctx.cwd));
4014
+ },
4015
+ });
4016
+
3825
4017
  pi.registerTool({
3826
4018
  name: "devflow_review_run",
3827
4019
  label: "Devflow Fact-Bound Review",
@@ -3975,7 +4167,7 @@ export default function devflowCore(pi: ExtensionAPI) {
3975
4167
  const revision = plan?.expected_revision;
3976
4168
  if (!inspected.result.ok || !plan || plan.classification !== "safe_repairable" || typeof revision !== "number") {
3977
4169
  const status = typeof inspected.snapshot?.status === "string" ? inspected.snapshot.status : "unknown";
3978
- const step = compactStep(inspected.snapshot?.next_step) ?? compactStep(inspected.snapshot?.current_step);
4170
+ const step = inspected.snapshot ? selectedWorkflowStep(inspected.snapshot) : null;
3979
4171
  const stepId = typeof step?.id === "string" ? step.id : "当前步骤";
3980
4172
  if (inspected.result.ok && status === "in_progress") {
3981
4173
  ctx.ui.notify(`控制面状态一致,无需恢复。当前步骤是 ${stepId};请审查当前产物后明确回复“批准执行”或提出修改意见。`, "info");
@@ -393,6 +393,21 @@ def run(root: Path, feature: str, write: bool) -> dict[str, Any]:
393
393
  "execution_binding_fingerprint": file_fingerprint(execution_bindings_path), "results": results, "passed": all_passed, "history": history[-20:]}
394
394
  if write:
395
395
  write_json(result_path, payload)
396
+ meta_path = result_path.with_name("acceptance-results.meta.json")
397
+ passed = sum(1 for item in results if item.get("status") == "passed")
398
+ failed = sum(1 for item in results if item.get("status") == "failed")
399
+ blocked = sum(1 for item in results if item.get("status") == "blocked")
400
+ write_json(meta_path, {
401
+ "schema_version": 1,
402
+ "feature": feature,
403
+ "acceptance_tc_passed": passed,
404
+ "acceptance_tc_failed": failed,
405
+ "acceptance_tc_blocked": blocked,
406
+ "acceptance_suite_graph_revision": payload["test_suite"].get("graph_revision"),
407
+ "acceptance_results_fingerprint": file_fingerprint(result_path),
408
+ "built_by": "core_acceptance_test_runner",
409
+ "built_at": payload["executed_at"],
410
+ })
396
411
  return {"ok": all_passed, "feature": feature, "path": str(result_path.relative_to(root)), "results": results,
397
412
  "errors": [] if all_passed else ["one or more frozen TC cases failed or are blocked"]}
398
413
 
@@ -108,7 +108,7 @@ def transition_gate_contract(workflow, step)
108
108
  "topology" => {
109
109
  "allowed_tracks" => allowed_tracks,
110
110
  "allowed_predecessors" => Array(step["depends_on"]),
111
- "allowed_intervening_predecessors" => immediate_prior ? [immediate_prior["id"]] : [],
111
+ "allowed_intervening_predecessors" => ((immediate_prior ? [immediate_prior["id"]] : []) + Array(step["allowed_intervening_predecessors"])).uniq,
112
112
  "allowed_reentry" => reentries,
113
113
  "checkpoint" => checkpoint && checkpoint.merge("id" => checkpoint_id)
114
114
  },
@@ -529,8 +529,40 @@ def repair_legacy_parallel_join_predecessor!(state, workflow)
529
529
  predecessor
530
530
  end
531
531
 
532
+ # Business-quality checkers are advisory by default. The runtime still keeps
533
+ # a small safety floor for malformed control-plane state and unsafe writes, but
534
+ # Core-generated test/traceability/merge findings no longer strand the serial
535
+ # workflow before the AI review can decide what to do.
536
+ SOFT_GATE_MODE = ENV.fetch("DEVFLOW_SOFT_GATES", "1") != "0"
537
+ HARD_FAILURE_REASONS = %w[
538
+ missing_state_file
539
+ missing_state_front_matter
540
+ stale_revision
541
+ invalid_control_path
542
+ unknown_step
543
+ gate_target_step_unknown
544
+ gate_current_step_mismatch
545
+ parallel_transition_requires_write
546
+ feature_not_active
547
+ cancel_reason_required
548
+ session_rollover_in_progress
549
+ failure_branch_incomplete
550
+ parallel_machine_evidence_failed
551
+ parallel_join_failed
552
+ parallel_branch_transition_failed
553
+ cross_track_consistency_failed
554
+ business_cross_model_check_failed
555
+ gate_boundary_rejected
556
+ ].freeze
557
+ $devflow_advisories = []
558
+
532
559
  def fail_result(reason, details = {})
533
- { "ok" => false, "reason" => reason, "details" => details }
560
+ failure = { "ok" => false, "reason" => reason, "details" => details }
561
+ if SOFT_GATE_MODE && !HARD_FAILURE_REASONS.include?(reason.to_s)
562
+ $devflow_advisories << failure
563
+ return nil
564
+ end
565
+ failure
534
566
  end
535
567
 
536
568
  # CP-3 has to validate a *candidate* task/TC binding before it freezes the
@@ -791,6 +823,19 @@ result =
791
823
  # GateRuntime is a read-only boundary check. It runs while the project
792
824
  # lock is held and before any candidate state mutation; the existing
793
825
  # validator and advance_state CAS remain the final write guard.
826
+ if result.nil? && gate_contract_enabled?(workflow) &&
827
+ !%w[prepared claimed].include?(state.dig("rollover", "status")) &&
828
+ (options[:complete_step] || options[:confirm_checkpoint] || options[:enter_failure_branch] || options[:exit_failure_branch] || options[:parallel_action])
829
+ topology_errors = WorkflowGateContract.validate(workflow)
830
+ unless topology_errors.empty?
831
+ result = fail_result("gate_boundary_rejected", "gate" => {
832
+ "verdict" => "deny",
833
+ "reason" => "workflow_gate_contract_invalid",
834
+ "errors" => topology_errors,
835
+ })
836
+ end
837
+ end
838
+
794
839
  if result.nil? && gate_contract_enabled?(workflow) &&
795
840
  !%w[prepared claimed].include?(state.dig("rollover", "status")) &&
796
841
  (options[:complete_step] || options[:confirm_checkpoint] || options[:enter_failure_branch] || options[:exit_failure_branch] || options[:parallel_action])
@@ -1187,7 +1232,7 @@ result =
1187
1232
  phase_owner_override: prospective_owner
1188
1233
  )
1189
1234
 
1190
- if changed && !validation["ok"]
1235
+ if changed && !validation["ok"] && !SOFT_GATE_MODE
1191
1236
  result = fail_result("state_validation_failed", "validation" => validation)
1192
1237
  elsif changed && options[:write]
1193
1238
  ownership = PhaseOwnership.claim(
@@ -1249,6 +1294,7 @@ result =
1249
1294
  "actions" => actions,
1250
1295
  "state_path" => state_path,
1251
1296
  "next_intent_written" => next_intent_written,
1297
+ "advisories" => $devflow_advisories,
1252
1298
  "state_validation" => validation,
1253
1299
  "state" => state
1254
1300
  }
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The single execution boundary for Core checkers. Registry membership alone
4
+ # is not evidence that a checker can run; every non graph-backed checker must
5
+ # have an adapter here.
6
+ require "json"
7
+ require "open3"
8
+ require_relative "checker_registry"
9
+
10
+ module CheckerAdapter
11
+ module_function
12
+
13
+ def execute(root:, feature:, step:, id:, graph_results:)
14
+ CheckerRegistry.validate_executable!([id])
15
+ if CheckerRegistry::GRAPH_BACKED.include?(id.to_s)
16
+ issues = graph_results.fetch(id.to_s, ["checker produced no graph result: #{id}"])
17
+ return result(id, issues.empty?, issues)
18
+ end
19
+
20
+ command = command_for(root, feature, step, id)
21
+ return result(id, false, ["no adapter command for #{id}"]) unless command
22
+
23
+ stdout, stderr, status = Open3.capture3(*command, chdir: root)
24
+ payload = JSON.parse(stdout)
25
+ passed = status.success? && payload.is_a?(Hash) && (payload["ok"] == true || payload["valid"] == true)
26
+ issues = if passed
27
+ []
28
+ elsif payload.is_a?(Hash) && payload["errors"].is_a?(Array)
29
+ payload["errors"].map { |item| item.is_a?(String) ? item : JSON.generate(item) }
30
+ else
31
+ [stderr.strip.empty? ? "#{id} failed" : stderr.strip]
32
+ end
33
+ result(id, passed, issues)
34
+ rescue JSON::ParserError => error
35
+ result(id, false, ["#{id} returned invalid JSON: #{error.message}"])
36
+ rescue StandardError => error
37
+ result(id, false, ["#{id} adapter failed: #{error.message}"])
38
+ end
39
+
40
+ def result(id, passed, issues)
41
+ { "id" => id, "origin" => "core_fixed_checker", "exit_code" => passed ? 0 : 1,
42
+ "passed" => passed, "issues" => passed ? [] : Array(issues),
43
+ "counters" => { "issue_count" => passed ? 0 : Array(issues).length } }
44
+ end
45
+
46
+ def command_for(root, feature, step, id)
47
+ scripts = File.expand_path(__dir__)
48
+ case id.to_s
49
+ when "test-task-binding-check"
50
+ stage = step.to_s == "change-plan" ? "candidate" : "frozen"
51
+ ["python3", File.join(scripts, "test_task_binding_check.py"), "--feature", feature, "--stage", stage, "--json"]
52
+ when "tdd-evidence-check"
53
+ ["python3", File.join(scripts, "tdd_evidence.py"), "validate", "--feature", feature, "--json"]
54
+ when "acceptance-test-results-check"
55
+ ["python3", File.join(scripts, "acceptance_test_runner.py"), "validate-results", "--feature", feature, "--json"]
56
+ when "api-test-matrix-check"
57
+ ["python3", File.join(scripts, "api_test_matrix.py"), "validate", "--feature", feature, "--json"]
58
+ when "api-test-schema-check"
59
+ ["python3", File.join(scripts, "api_test_matrix.py"), "check", "--feature", feature, "--json"]
60
+ when "cross-track-consistency-check"
61
+ ["ruby", File.join(scripts, "cross_track_consistency_check.rb"), "--feature", feature, "--json"]
62
+ when "business-cross-model-check"
63
+ business = ".planning/business-model/#{feature}/canonical.json"
64
+ return nil unless File.file?(File.join(root, business))
65
+ command = ["python3", File.join(scripts, "business_cross_model_check.py"), "--business-canonical", business]
66
+ wireframe = ".planning/features/#{feature}/canonical.json"
67
+ claims = ".planning/features/#{feature}/business-claims.yaml"
68
+ traceability = ".planning/requirements/#{feature}/traceability.json"
69
+ command += ["--wireframe", wireframe] if File.file?(File.join(root, wireframe))
70
+ command += ["--claims", claims] if File.file?(File.join(root, claims))
71
+ command += ["--traceability", traceability] if File.file?(File.join(root, traceability))
72
+ command + ["--json"]
73
+ when "business-claim-reference-check"
74
+ claims = ".planning/features/#{feature}/business-claims.yaml"
75
+ business = ".planning/business-model/#{feature}/canonical.json"
76
+ return nil unless File.file?(File.join(root, claims)) && File.file?(File.join(root, business))
77
+ command = ["python3", File.join(scripts, "business_claim_validate.py"), "--claims", claims, "--business-canonical", business]
78
+ traceability = ".planning/requirements/#{feature}/traceability.json"
79
+ command += ["--traceability", traceability] if File.file?(File.join(root, traceability))
80
+ command
81
+ end
82
+ end
83
+ end