@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.
- package/README.md +4 -0
- package/package.json +1 -1
- package/plugins/devflow-core/extensions/devflow-core.ts +211 -19
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +15 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +49 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checker_adapter.rb +83 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checker_registry.rb +19 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checkpoint_meta.rb +46 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +10 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_runtime.rb +32 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_state.rb +44 -41
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_workflow.rb +5 -4
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +15 -4
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +15 -9
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/review_preflight.py +238 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/review_run.py +234 -32
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +63 -11
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/workflow_gate_contract.rb +5 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +40 -31
- package/plugins/devflow-core/skills/workflow-pipeline/SKILL.md +11 -5
- package/plugins/devflow-core/skills/workflow-pipeline/references/pipeline-flow.md +6 -6
- package/plugins/devflow-implementation/skills/workflow-change-plan/SKILL.md +16 -1
- package/plugins/devflow-implementation/skills/workflow-change-plan/references/output-template.md +3 -1
- package/plugins/devflow-quality/agents/code-reviewer.md +7 -1
- package/plugins/devflow-quality/skills/code-review/SKILL.md +10 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/SKILL.md +37 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/SKILL.md +327 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/assets/output-template.md +213 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/bug-rules.md +9 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/coverage-checklist.md +63 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/exception-checklist.md +37 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/api-test.md +58 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/boundary-test.md +151 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/common-test.md +49 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/compatibility-test.md +105 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/data-test.md +67 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/exception-test.md +109 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/function-test.md +65 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/integration-test.md +36 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/performance-test.md +100 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/scenario-test.md +63 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/security-test.md +55 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/ui-test.md +49 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/usability-test.md +46 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/writing-guide.md +111 -0
- 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,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
|
-
|
|
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
|
|
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
|
|
991
|
-
|
|
992
|
-
|
|
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 =
|
|
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((
|
|
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 =
|
|
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
|
|
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"
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|