@guanlin4924/devflow-pi 0.1.14 → 0.1.16
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/package.json +1 -1
- package/plugins/devflow-core/extensions/devflow-core.ts +179 -54
- package/plugins/devflow-core/skills/workflow-orchestrator/SKILL.md +4 -4
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +93 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_state.rb +2 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +111 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph.rb +471 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/traceability_check.py +160 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +85 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +59 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/traceability.json +23 -0
- package/plugins/devflow-design/skills/workflow-api-contract/SKILL.md +4 -1
- package/plugins/devflow-design/skills/workflow-backend-tech-spec/SKILL.md +4 -0
- package/plugins/devflow-design/skills/workflow-frontend-tech-spec/SKILL.md +4 -0
- package/plugins/devflow-design/skills/workflow-integration-verify/SKILL.md +10 -0
- package/plugins/devflow-implementation/skills/workflow-change-plan/SKILL.md +8 -0
- package/plugins/devflow-requirements/skills/requirement-feasibility-review/SKILL.md +4 -0
- package/plugins/devflow-requirements/skills/workflow-domain-modeling/SKILL.md +4 -0
- package/plugins/devflow-requirements/skills/workflow-requirement-baseline/SKILL.md +13 -0
- package/plugins/devflow-requirements/skills/workflow-requirement-triage/SKILL.md +4 -0
- package/src/cli.ts +2 -1
- package/src/runtime.ts +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
3
|
-
import { mkdir,
|
|
3
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
@@ -20,9 +20,17 @@ type ActiveExecutionContract = {
|
|
|
20
20
|
contractId: string;
|
|
21
21
|
feature: string;
|
|
22
22
|
step: string;
|
|
23
|
+
stateRevision?: number;
|
|
24
|
+
graphRevision?: number;
|
|
23
25
|
allowedPaths: string[];
|
|
24
26
|
allowedAgents: string[];
|
|
27
|
+
allowedTools: string[];
|
|
28
|
+
checkerIds: string[];
|
|
29
|
+
toolBudget: number;
|
|
30
|
+
frozenHash: string;
|
|
31
|
+
contractPath?: string;
|
|
25
32
|
acceptance: string[];
|
|
33
|
+
allowedGraphDelta: string[];
|
|
26
34
|
onUnknown: "block_and_escalate";
|
|
27
35
|
};
|
|
28
36
|
|
|
@@ -198,7 +206,7 @@ const CONTROL_PATHS = [
|
|
|
198
206
|
".planning/artifacts/index.yaml",
|
|
199
207
|
];
|
|
200
208
|
|
|
201
|
-
const CONTROL_PREFIXES = [".planning/workflows/", ".planning/contracts/"];
|
|
209
|
+
const CONTROL_PREFIXES = [".planning/workflows/", ".planning/contracts/", ".planning/evidence/"];
|
|
202
210
|
|
|
203
211
|
const REQUIRED_EXTENSION_TOOLS = [
|
|
204
212
|
"devflow_doctor",
|
|
@@ -206,6 +214,10 @@ const REQUIRED_EXTENSION_TOOLS = [
|
|
|
206
214
|
"devflow_bootstrap",
|
|
207
215
|
"devflow_transition",
|
|
208
216
|
"devflow_artifact",
|
|
217
|
+
"devflow_machine_evidence",
|
|
218
|
+
"devflow_requirement_graph",
|
|
219
|
+
"devflow_evidence",
|
|
220
|
+
"devflow_contract",
|
|
209
221
|
"devflow_recovery",
|
|
210
222
|
"devflow_skill_script",
|
|
211
223
|
"devflow_context_sync",
|
|
@@ -276,6 +288,9 @@ const REQUIRED_SCRIPTS = [
|
|
|
276
288
|
"workflow-orchestrator/scripts/inspect_state.rb",
|
|
277
289
|
"workflow-orchestrator/scripts/advance_state.rb",
|
|
278
290
|
"workflow-orchestrator/scripts/sync_artifacts.rb",
|
|
291
|
+
"workflow-orchestrator/scripts/requirement_graph.rb",
|
|
292
|
+
"workflow-orchestrator/scripts/machine_evidence.rb",
|
|
293
|
+
"workflow-orchestrator/scripts/execution_contract.rb",
|
|
279
294
|
"workflow-code-index/scripts/code_index.py",
|
|
280
295
|
"workflow-context-sync/scripts/context_sync_cursor.py",
|
|
281
296
|
];
|
|
@@ -380,6 +395,19 @@ function safeControlPath(cwd: string, raw: string | undefined, kind: "state" | "
|
|
|
380
395
|
return relative;
|
|
381
396
|
}
|
|
382
397
|
|
|
398
|
+
function isCanonicalRequirementGraphPath(filePath: string): boolean {
|
|
399
|
+
return /^\.planning\/requirements\/[^/\\\x00-\x1F\x7F]+\/(?:governance|evidence|definitions|behavior-graph|requirement-graph|slices|acceptance|traceability|graph\.meta)\.json$/u.test(filePath)
|
|
400
|
+
|| /^\.planning\/requirements\/[^/\\\x00-\x1F\x7F]+\/(?:context\/[^/\\]+\.json|control-events\.jsonl)$/u.test(filePath);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function isCoreOwnedGovernancePath(filePath: string): boolean {
|
|
404
|
+
// 规范需求图谱只能由 Core 的 devflow_requirement_graph writer 写入;the
|
|
405
|
+
// same ownership extends to its views, machine evidence, and audit records.
|
|
406
|
+
return isCanonicalRequirementGraphPath(filePath)
|
|
407
|
+
|| /^\.planning\/evidence\/[^/\\\x00-\x1F\x7F]+\/[^/\\]+\.machine\.json$/u.test(filePath)
|
|
408
|
+
|| /^\.planning\/contracts\/[^/\\\x00-\x1F\x7F]+\/(?:EC-[A-Fa-f0-9]+\.json|control-events\.jsonl)$/u.test(filePath);
|
|
409
|
+
}
|
|
410
|
+
|
|
383
411
|
function resolveIntelSkillRoot(cwd: string): string | undefined {
|
|
384
412
|
if (isEmbeddedRuntime()) {
|
|
385
413
|
const packaged = path.join(resolvePluginRoot(), "devflow-intel", "skills");
|
|
@@ -641,10 +669,10 @@ function isShellControlMutation(toolName: string, input: unknown): boolean {
|
|
|
641
669
|
const command = String((input as Record<string, unknown>).command ?? (input as Record<string, unknown>).cmd ?? "");
|
|
642
670
|
if (!command) return false;
|
|
643
671
|
const invokesWriter =
|
|
644
|
-
/workflow-orchestrator\/scripts\/(?:advance_state|init_state|init_workflow|sync_artifacts)\.rb[^\n]*--write/.test(
|
|
672
|
+
/workflow-orchestrator\/scripts\/(?:advance_state|init_state|init_workflow|sync_artifacts|requirement_graph|machine_evidence|execution_contract)\.rb[^\n]*--write/.test(
|
|
645
673
|
command,
|
|
646
674
|
);
|
|
647
|
-
const namesControlPath = [...CONTROL_PATHS, ...CONTROL_PREFIXES].some(item => command.includes(item));
|
|
675
|
+
const namesControlPath = [...CONTROL_PATHS, ...CONTROL_PREFIXES].some(item => command.includes(item)) || /\.planning\/requirements\//.test(command);
|
|
648
676
|
const looksLikeWrite = /(?:^|[;&|]\s*)(?:rm|mv|cp|install|truncate)\b|(?:^|\s)(?:tee|sed\s+-i|perl\s+-i)\b|>{1,2}/.test(
|
|
649
677
|
command,
|
|
650
678
|
);
|
|
@@ -668,6 +696,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
668
696
|
let sessionId = randomUUID();
|
|
669
697
|
let controlState: SessionControlState = initialControlState();
|
|
670
698
|
let activeContract: ActiveExecutionContract | undefined;
|
|
699
|
+
let contractAgentCalls = 0;
|
|
671
700
|
|
|
672
701
|
function setControlState(ctx: { ui: { setStatus(key: string, text: string | undefined): void } }, state: SessionControlState) {
|
|
673
702
|
controlState = state;
|
|
@@ -686,18 +715,14 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
686
715
|
return contract.allowedPaths.some(root => filePath === root || filePath.startsWith(`${root}/`));
|
|
687
716
|
}
|
|
688
717
|
|
|
689
|
-
async function
|
|
690
|
-
|
|
691
|
-
const
|
|
692
|
-
const
|
|
693
|
-
const
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
const temporary = `${destination}.${randomUUID()}.tmp`;
|
|
698
|
-
await writeFile(temporary, payload, "utf8");
|
|
699
|
-
await rename(temporary, destination);
|
|
700
|
-
return path.relative(cwd, destination).replaceAll("\\", "/");
|
|
718
|
+
async function contractIsFresh(cwd: string, contract: ActiveExecutionContract): Promise<boolean> {
|
|
719
|
+
const state = parseJsonResult(await runRuby(cwd, "workflow-orchestrator/scripts/inspect_state.rb", ["--json"]));
|
|
720
|
+
const stateRevision = typeof state?.revision === "number" ? state.revision : undefined;
|
|
721
|
+
const graphResult = parseJsonResult(await runRuby(cwd, "workflow-orchestrator/scripts/requirement_graph.rb", ["inspect", "--feature", contract.feature, "--json"]));
|
|
722
|
+
const graph = graphResult?.graph;
|
|
723
|
+
const graphRevision = graph && typeof graph === "object" && typeof (graph as Record<string, unknown>).revision === "number"
|
|
724
|
+
? (graph as Record<string, number>).revision : undefined;
|
|
725
|
+
return stateRevision === contract.stateRevision && graphRevision === contract.graphRevision;
|
|
701
726
|
}
|
|
702
727
|
|
|
703
728
|
async function collectProjectInitialization(ctx: {
|
|
@@ -1079,6 +1104,14 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1079
1104
|
|
|
1080
1105
|
pi.on("tool_call", async (event, ctx) => {
|
|
1081
1106
|
const toolName = String(event.toolName).toLowerCase();
|
|
1107
|
+
if (activeContract && controlState.phase === "executing" && !toolName.startsWith("devflow_")) {
|
|
1108
|
+
if (!activeContract.allowedTools.includes(toolName)) {
|
|
1109
|
+
return { block: true, reason: `Active contract ${activeContract.contractId} does not allow tool: ${toolName}` };
|
|
1110
|
+
}
|
|
1111
|
+
if (!(await contractIsFresh(ctx.cwd, activeContract))) {
|
|
1112
|
+
return { block: true, reason: `Active contract ${activeContract.contractId} is stale against STATE or graph revision; synthesize a new contract before continuing.` };
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1082
1115
|
if (toolName === "task") {
|
|
1083
1116
|
const requested = requestedTaskAgents(event.input);
|
|
1084
1117
|
if (requested.missing) {
|
|
@@ -1109,8 +1142,12 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1109
1142
|
controlState.phase === "executing" &&
|
|
1110
1143
|
!requested.names.every(agent => activeContract?.allowedAgents.includes(agent))
|
|
1111
1144
|
) {
|
|
1112
|
-
|
|
1145
|
+
return { block: true, reason: `Active contract ${activeContract.contractId} only allows agents: ${activeContract.allowedAgents.join(", ")}` };
|
|
1113
1146
|
}
|
|
1147
|
+
if (activeContract && controlState.phase === "executing" && contractAgentCalls >= activeContract.toolBudget) {
|
|
1148
|
+
return { block: true, reason: `Active contract ${activeContract.contractId} exhausted its fixed agent budget (${activeContract.toolBudget}).` };
|
|
1149
|
+
}
|
|
1150
|
+
if (activeContract && controlState.phase === "executing") contractAgentCalls += 1;
|
|
1114
1151
|
} else if (
|
|
1115
1152
|
controlState.phase === "planning" &&
|
|
1116
1153
|
toolName !== "devflow_contract" &&
|
|
@@ -1129,6 +1166,16 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1129
1166
|
"Devflow 状态迁移只能由运行时在用户明确批准已展示的步骤或 checkpoint 后执行;LLM 不得自行推进。",
|
|
1130
1167
|
};
|
|
1131
1168
|
}
|
|
1169
|
+
if (
|
|
1170
|
+
!toolName.startsWith("devflow_") &&
|
|
1171
|
+
["write", "edit", "apply_patch", "notebook_edit", "bash", "shell", "exec_command"].includes(toolName) &&
|
|
1172
|
+
inputPaths(event.input, ctx.cwd).some(isCoreOwnedGovernancePath)
|
|
1173
|
+
) {
|
|
1174
|
+
return {
|
|
1175
|
+
block: true,
|
|
1176
|
+
reason: "规范需求图谱、派生视图、机器 Evidence 与合同审计只能由 Core writer 写入;Skill 只能提交候选 delta 或 agent_claim。",
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1132
1179
|
if (toolName.startsWith("devflow_")) return;
|
|
1133
1180
|
if (
|
|
1134
1181
|
activeContract &&
|
|
@@ -1234,6 +1281,73 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1234
1281
|
}
|
|
1235
1282
|
});
|
|
1236
1283
|
|
|
1284
|
+
pi.registerTool({
|
|
1285
|
+
name: "devflow_machine_evidence",
|
|
1286
|
+
label: "Devflow Machine Evidence",
|
|
1287
|
+
description: "Collect Core-owned evidence from the current workflow step's actual required outputs and graph revision.",
|
|
1288
|
+
parameters: z.object({ feature: z.string().min(1), step: z.string().min(1) }),
|
|
1289
|
+
approval: "write",
|
|
1290
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
1291
|
+
return renderResult(
|
|
1292
|
+
await runRuby(ctx.cwd, "workflow-orchestrator/scripts/machine_evidence.rb", ["--feature", params.feature, "--step", params.step, "--write"]),
|
|
1293
|
+
{ feature: params.feature, step: params.step },
|
|
1294
|
+
);
|
|
1295
|
+
},
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
pi.registerTool({
|
|
1299
|
+
name: "devflow_requirement_graph",
|
|
1300
|
+
label: "Devflow Requirement Graph",
|
|
1301
|
+
description: "Submit bounded requirement-graph candidates or inspect/validate the Core-owned canonical graph.",
|
|
1302
|
+
parameters: z.object({
|
|
1303
|
+
action: z.enum(["propose", "inspect", "validate", "context", "confirm"]),
|
|
1304
|
+
feature: z.string().min(1),
|
|
1305
|
+
expectedRevision: z.number().int().min(0).optional(),
|
|
1306
|
+
sliceId: z.string().min(1).optional(),
|
|
1307
|
+
nodeId: z.string().min(1).optional(),
|
|
1308
|
+
evidence: z.object({ cp_record: z.unknown().optional(), evidence_ref: z.unknown().optional() }).optional(),
|
|
1309
|
+
write: z.boolean().optional(),
|
|
1310
|
+
read: z.boolean().optional(),
|
|
1311
|
+
delta: z.object({
|
|
1312
|
+
op: z.enum(["add", "link", "update"]),
|
|
1313
|
+
kind: z.enum(["source", "code", "term", "object", "behavior", "requirement", "rule", "boundary", "slice", "risk", "decision", "acceptance", "api"]).optional(),
|
|
1314
|
+
id: z.string().min(1).optional(),
|
|
1315
|
+
data: z.record(z.string(), z.unknown()).optional(),
|
|
1316
|
+
from: z.string().min(1).optional(),
|
|
1317
|
+
to: z.string().min(1).optional(),
|
|
1318
|
+
relation: z.string().min(1).optional(),
|
|
1319
|
+
propagation: z.enum(["one_hop", "full"]).optional(),
|
|
1320
|
+
}).optional(),
|
|
1321
|
+
}),
|
|
1322
|
+
approval: (args) => args.action === "propose" || args.action === "confirm" ? "write" : "read",
|
|
1323
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
1324
|
+
const args = [params.action, "--feature", params.feature, "--json"];
|
|
1325
|
+
if (params.action === "context") {
|
|
1326
|
+
if (!params.sliceId) return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "context requires sliceId" });
|
|
1327
|
+
args.push("--slice-id", params.sliceId);
|
|
1328
|
+
if (params.write) args.push("--write");
|
|
1329
|
+
if (params.read) args.push("--read");
|
|
1330
|
+
}
|
|
1331
|
+
if (params.action === "propose") {
|
|
1332
|
+
if (params.expectedRevision === undefined || !params.delta) {
|
|
1333
|
+
return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "propose requires expectedRevision and delta" });
|
|
1334
|
+
}
|
|
1335
|
+
args.push("--write", "--expected-revision", String(params.expectedRevision), "--delta-json", JSON.stringify(params.delta));
|
|
1336
|
+
if (activeContract?.allowedGraphDelta?.length) args.push("--allowed-kinds", activeContract.allowedGraphDelta.join(","));
|
|
1337
|
+
}
|
|
1338
|
+
if (params.action === "confirm") {
|
|
1339
|
+
if (params.expectedRevision === undefined || !params.nodeId || !params.evidence) {
|
|
1340
|
+
return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "confirm requires expectedRevision, nodeId and evidence" });
|
|
1341
|
+
}
|
|
1342
|
+
args.push("--write", "--expected-revision", String(params.expectedRevision), "--node-id", params.nodeId, "--evidence-json", JSON.stringify(params.evidence));
|
|
1343
|
+
}
|
|
1344
|
+
return renderResult(
|
|
1345
|
+
await runRuby(ctx.cwd, "workflow-orchestrator/scripts/requirement_graph.rb", args),
|
|
1346
|
+
{ action: params.action, feature: params.feature },
|
|
1347
|
+
);
|
|
1348
|
+
},
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1237
1351
|
pi.registerTool({
|
|
1238
1352
|
name: "devflow_evidence",
|
|
1239
1353
|
label: "Devflow Evidence Pack",
|
|
@@ -1254,7 +1368,10 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1254
1368
|
if (!pack || typeof pack !== "object" || Array.isArray(pack)) return ["pack must be an object"];
|
|
1255
1369
|
const record = pack as Record<string, unknown>;
|
|
1256
1370
|
const errors: string[] = [];
|
|
1257
|
-
|
|
1371
|
+
const legacySchema = record.schemaVersion === 1;
|
|
1372
|
+
if (record.schemaVersion !== 2 && record.schemaVersion !== 1) errors.push("schemaVersion must equal 2 (or 1 during compatibility period)");
|
|
1373
|
+
if (record.origin !== "agent_claim") errors.push("origin must equal agent_claim");
|
|
1374
|
+
if (Object.prototype.hasOwnProperty.call(record, "passed")) errors.push("agent claims cannot declare passed");
|
|
1258
1375
|
for (const key of ["facts", "unknowns", "risks"]) {
|
|
1259
1376
|
if (!Array.isArray(record[key])) errors.push(`${key} must be an array`);
|
|
1260
1377
|
}
|
|
@@ -1272,7 +1389,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1272
1389
|
return { content: [{ type: "text" as const, text: JSON.stringify(parsed, null, 2) }], details: { action: params.action, path: path.relative(ctx.cwd, destination), errors } };
|
|
1273
1390
|
}
|
|
1274
1391
|
if (!params.pack) return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "pack is required for create" }, { action: params.action });
|
|
1275
|
-
const pack = { schemaVersion:
|
|
1392
|
+
const pack = { schemaVersion: 2, taskId, createdAt: new Date().toISOString(), ...params.pack, origin: "agent_claim", evidenceClass: "non_gating_claim" };
|
|
1276
1393
|
const errors = validatePack(pack);
|
|
1277
1394
|
if (errors.length > 0) return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: errors.join("; ") }, { action: params.action, errors });
|
|
1278
1395
|
await mkdir(root, { recursive: true });
|
|
@@ -1284,26 +1401,19 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1284
1401
|
pi.registerTool({
|
|
1285
1402
|
name: "devflow_contract",
|
|
1286
1403
|
label: "Devflow Execution Contract",
|
|
1287
|
-
description: "
|
|
1404
|
+
description: "Inspect or activate a Core-synthesized ExecutionContract v2. Paths, agents, checker IDs, and budgets are fixed by the packaged policy, never model input.",
|
|
1288
1405
|
parameters: z.object({
|
|
1289
|
-
action: z.enum(["status", "activate"]),
|
|
1290
|
-
contractId: z.string().min(1).optional(),
|
|
1406
|
+
action: z.enum(["status", "inspect", "activate"]),
|
|
1291
1407
|
feature: z.string().min(1).optional(),
|
|
1292
1408
|
step: z.string().min(1).optional(),
|
|
1293
|
-
allowedPaths: z.array(z.string().min(1)).default([]),
|
|
1294
|
-
allowedAgents: z.array(z.enum(["implementation-worker", "verification-runner"])).default([]),
|
|
1295
|
-
acceptance: z.array(z.string().min(1)).default([]),
|
|
1296
1409
|
}),
|
|
1297
1410
|
approval: "write",
|
|
1298
1411
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
1299
1412
|
const input = params as {
|
|
1300
|
-
action: "status" | "activate";
|
|
1413
|
+
action: "status" | "inspect" | "activate";
|
|
1301
1414
|
contractId?: string;
|
|
1302
1415
|
feature?: string;
|
|
1303
1416
|
step?: string;
|
|
1304
|
-
allowedPaths: string[];
|
|
1305
|
-
allowedAgents: string[];
|
|
1306
|
-
acceptance: string[];
|
|
1307
1417
|
};
|
|
1308
1418
|
if (input.action === "status") {
|
|
1309
1419
|
return {
|
|
@@ -1311,47 +1421,51 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1311
1421
|
details: { activeContract },
|
|
1312
1422
|
};
|
|
1313
1423
|
}
|
|
1314
|
-
if (
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
try {
|
|
1322
|
-
allowedPaths = input.allowedPaths.map(candidate => {
|
|
1323
|
-
const relative = normalizedProjectPath(candidate, ctx.cwd);
|
|
1324
|
-
if (!relative || relative.startsWith(".planning/")) throw new Error(`Invalid contract path: ${candidate}`);
|
|
1325
|
-
return relative.replace(/\/$/, "");
|
|
1326
|
-
});
|
|
1327
|
-
} catch (error) {
|
|
1328
|
-
return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: error instanceof Error ? error.message : String(error) });
|
|
1329
|
-
}
|
|
1424
|
+
if (!input.feature || !input.step) return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "feature and step are required; Core derives all control fields." });
|
|
1425
|
+
const inspected = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/execution_contract.rb", ["inspect", "--feature", input.feature, "--step", input.step, "--json"]);
|
|
1426
|
+
const payload = parseJsonResult(inspected);
|
|
1427
|
+
const raw = payload?.contract;
|
|
1428
|
+
if (!controlPlaneSucceeded(inspected) || !raw || typeof raw !== "object") return renderResult(inspected, { action: input.action });
|
|
1429
|
+
const record = raw as Record<string, unknown>;
|
|
1430
|
+
const strings = (key: string): string[] => Array.isArray(record[key]) ? record[key].filter((item): item is string => typeof item === "string") : [];
|
|
1330
1431
|
const contract: ActiveExecutionContract = {
|
|
1331
|
-
contractId:
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
allowedPaths:
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
onUnknown: "block_and_escalate",
|
|
1432
|
+
contractId: String(record.contract_id ?? ""), feature: String(record.feature ?? ""), step: String(record.step ?? ""),
|
|
1433
|
+
stateRevision: typeof record.state_revision === "number" ? record.state_revision : undefined,
|
|
1434
|
+
graphRevision: typeof record.graph_revision === "number" ? record.graph_revision : undefined,
|
|
1435
|
+
allowedPaths: strings("allowed_paths"), allowedAgents: strings("allowed_agents"), allowedTools: strings("allowed_tools"), checkerIds: strings("checker_ids"),
|
|
1436
|
+
toolBudget: typeof record.tool_budget === "number" ? record.tool_budget : 0,
|
|
1437
|
+
frozenHash: String(record.frozen_hash ?? ""), acceptance: strings("acceptance"), allowedGraphDelta: strings("allowed_graph_delta"), onUnknown: "block_and_escalate",
|
|
1338
1438
|
};
|
|
1439
|
+
if (!contract.contractId || !contract.frozenHash) return renderResult({ ok: false, exitCode: 1, stdout: "", stderr: "Core returned an invalid execution contract." });
|
|
1440
|
+
if (input.action === "inspect") return { content: [{ type: "text" as const, text: JSON.stringify(contract, null, 2) }], details: { contract } };
|
|
1441
|
+
if (controlState.phase !== "planning") return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "Contracts may only be activated from the planning phase." });
|
|
1339
1442
|
const confirmed = await ctx.ui.confirm(
|
|
1340
1443
|
"激活 Devflow 执行合同",
|
|
1341
1444
|
[
|
|
1342
1445
|
`合同:${contract.contractId}`,
|
|
1343
1446
|
`Feature:${contract.feature}`,
|
|
1344
1447
|
`步骤:${contract.step}`,
|
|
1448
|
+
`STATE revision:${contract.stateRevision ?? "未初始化"}`,
|
|
1449
|
+
`图谱 revision:${contract.graphRevision ?? 0}`,
|
|
1345
1450
|
`可写路径:${contract.allowedPaths.join(", ")}`,
|
|
1346
1451
|
`允许 Agent:${contract.allowedAgents.join(", ")}`,
|
|
1347
|
-
|
|
1452
|
+
`固定检查器:${contract.checkerIds.join(", ") || "无"}`,
|
|
1453
|
+
`冻结哈希:${contract.frozenHash}`,
|
|
1348
1454
|
"确认后低成本模型只能在以上范围执行;未知项必须阻塞升级。",
|
|
1349
1455
|
].join("\n"),
|
|
1350
1456
|
);
|
|
1351
1457
|
if (!confirmed) return renderResult({ ok: false, exitCode: 1, stdout: "", stderr: "User declined execution contract activation." });
|
|
1352
1458
|
try {
|
|
1353
|
-
const
|
|
1459
|
+
const createArgs = ["create", "--feature", input.feature, "--step", input.step, "--write", "--json"];
|
|
1460
|
+
if (contract.stateRevision !== undefined) createArgs.push("--expected-state-revision", String(contract.stateRevision));
|
|
1461
|
+
if (contract.graphRevision !== undefined) createArgs.push("--expected-graph-revision", String(contract.graphRevision));
|
|
1462
|
+
const written = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/execution_contract.rb", createArgs);
|
|
1463
|
+
if (!controlPlaneSucceeded(written)) return renderResult(written, { action: input.action });
|
|
1464
|
+
const writtenPayload = parseJsonResult(written);
|
|
1465
|
+
const destination = typeof writtenPayload?.path === "string" ? writtenPayload.path : ".planning/contracts/";
|
|
1466
|
+
contract.contractPath = destination;
|
|
1354
1467
|
activeContract = contract;
|
|
1468
|
+
contractAgentCalls = 0;
|
|
1355
1469
|
setControlState(ctx, executingState(`合同执行:${contract.contractId}`));
|
|
1356
1470
|
return {
|
|
1357
1471
|
content: [{ type: "text" as const, text: `Execution contract activated: ${destination}` }],
|
|
@@ -1483,6 +1597,17 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1483
1597
|
if (!params.step) {
|
|
1484
1598
|
return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "step is required" });
|
|
1485
1599
|
}
|
|
1600
|
+
const snapshot = parseJsonResult(await runRuby(ctx.cwd, "workflow-orchestrator/scripts/inspect_state.rb", ["--json"]));
|
|
1601
|
+
const feature = snapshot?.snapshot && typeof snapshot.snapshot === "object" ? (snapshot.snapshot as Record<string, unknown>).feature : undefined;
|
|
1602
|
+
if (typeof feature === "string" && feature) {
|
|
1603
|
+
const evidenceArgs = ["--feature", feature, "--step", params.step, "--write"];
|
|
1604
|
+
if (activeContract?.feature === feature && activeContract.step === params.step && activeContract.contractPath) evidenceArgs.push("--contract", activeContract.contractPath);
|
|
1605
|
+
const evidence = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/machine_evidence.rb", evidenceArgs);
|
|
1606
|
+
const evidencePayload = parseJsonResult(evidence);
|
|
1607
|
+
if (!controlPlaneSucceeded(evidence) || evidencePayload?.complete !== true) {
|
|
1608
|
+
return renderResult({ ok: false, exitCode: 1, stdout: "", stderr: "machine evidence collection failed or required outputs are incomplete" }, { action: params.action, evidence: evidence.stdout });
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1486
1611
|
args.push("--complete-step", params.step);
|
|
1487
1612
|
} else if (params.action === "confirm-checkpoint") {
|
|
1488
1613
|
args.push("--confirm-checkpoint");
|
|
@@ -115,14 +115,14 @@ After each step completes, verify the step's gate before advancing:
|
|
|
115
115
|
| requirement-wayfinder | 决策地图.meta.json `status=ready_for_baseline` AND `ready_for_baseline=true` | Reject advance to domain-modeling |
|
|
116
116
|
| domain-modeling | 领域模型记录及 meta 存在且标识当前功能 | Reject advance to requirement-feasibility-review |
|
|
117
117
|
| requirement-feasibility-review | 可实现性审查.meta.json `status=ready_for_baseline`、`baseline_gate=pass`、`gap_count=0` 且无 critical finding | Reject advance to requirement-baseline |
|
|
118
|
-
| requirement-baseline | baseline.md review conclusion is "通过" AND baseline.meta.json `review_status`
|
|
119
|
-
| api-contract | api-contract.meta.json `review_status`
|
|
118
|
+
| requirement-baseline | baseline.md review conclusion is "通过" AND baseline.meta.json `review_status` / `traceability_status` are `passed` AND blocking counters are all 0 | Reject advance to api-contract |
|
|
119
|
+
| api-contract | api-contract.meta.json `review_status` / `traceability_status` are `passed` AND `pending_contract_count` is 0 | Reject advance to dual tracks |
|
|
120
120
|
| ui-spec | UI-SPEC.md review conclusion is "通过" | Reject advance to feature-breakdown |
|
|
121
121
|
| feature-breakdown | 功能点梳理.md review conclusion is "通过" | Reject advance to tech-specs |
|
|
122
122
|
| api-call-guide | api-call-guide.meta.json `frontend_consumable` and `mock_ready` are true, pending_items is empty | Reject merge gate |
|
|
123
123
|
| frontend-tech-spec + backend-tech-spec | Both review conclusions are "通过" | Reject integration-verify |
|
|
124
|
-
| integration-verify | integration-report.meta.json `merge_gate_status`
|
|
125
|
-
| change-plan | 变更清单.md review conclusion is "通过" | Reject advance to execute |
|
|
124
|
+
| integration-verify | integration-report.meta.json `merge_gate_status` / `traceability_status` are `passed` and all mismatch/blocking counters are 0 | Reject advance to change-plan |
|
|
125
|
+
| change-plan | 变更清单.md review conclusion is "通过" AND 变更清单.meta.json traceability_status is `passed` with zero uncovered/orphan counts | Reject advance to execute |
|
|
126
126
|
|
|
127
127
|
If a gate fails, the current step must be re-executed. Do not skip failing gates.
|
|
128
128
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# ExecutionContract v2 is synthesized from current Core truth. There is no
|
|
5
|
+
# CLI input for agents, paths, checkers, or budgets: those are fixed policy.
|
|
6
|
+
require "digest"
|
|
7
|
+
require "json"
|
|
8
|
+
require "optparse"
|
|
9
|
+
require "yaml"
|
|
10
|
+
require_relative "control_io"
|
|
11
|
+
require_relative "requirement_graph"
|
|
12
|
+
|
|
13
|
+
POLICY = {
|
|
14
|
+
"requirement-baseline" => { "checkers" => %w[requirement-graph-check slice-independence-check acceptance-coverage-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[requirement rule boundary slice acceptance] },
|
|
15
|
+
"api-contract" => { "checkers" => %w[requirement-graph-check contract-coverage-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[api] },
|
|
16
|
+
"integration-verify" => { "checkers" => %w[requirement-graph-check slice-independence-check contract-coverage-check change-impact-check], "agents" => ["verification-runner"], "budget" => 1, "allowed_graph_delta" => %w[requirement risk decision] },
|
|
17
|
+
"change-plan" => { "checkers" => %w[requirement-graph-check slice-independence-check contract-coverage-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[acceptance] },
|
|
18
|
+
"execute" => { "checkers" => %w[change-impact-check], "agents" => ["implementation-worker"], "budget" => 1, "allowed_graph_delta" => %w[requirement rule boundary behavior] },
|
|
19
|
+
"verify" => { "checkers" => %w[acceptance-coverage-check change-impact-check], "agents" => ["verification-runner"], "budget" => 1, "allowed_graph_delta" => %w[acceptance] }
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
options = { feature: nil, step: nil, expected_state_revision: nil, expected_graph_revision: nil, write: false, json: false }
|
|
23
|
+
command = ARGV.shift
|
|
24
|
+
OptionParser.new do |o|
|
|
25
|
+
o.on("--feature NAME") { |v| options[:feature] = v }
|
|
26
|
+
o.on("--step ID") { |v| options[:step] = v }
|
|
27
|
+
o.on("--expected-state-revision N", Integer) { |v| options[:expected_state_revision] = v }
|
|
28
|
+
o.on("--expected-graph-revision N", Integer) { |v| options[:expected_graph_revision] = v }
|
|
29
|
+
o.on("--write") { options[:write] = true }
|
|
30
|
+
o.on("--json") { options[:json] = true }
|
|
31
|
+
end.parse!
|
|
32
|
+
|
|
33
|
+
def load_state(root)
|
|
34
|
+
document = File.read(File.join(root, ".planning/STATE.md"))
|
|
35
|
+
front = document.match(/\A---\n(.*?)\n---/m)&.[](1)
|
|
36
|
+
raise "STATE.md has no YAML front matter" unless front
|
|
37
|
+
YAML.safe_load(front, permitted_classes: [], aliases: false)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def interpolate(value, state)
|
|
41
|
+
value.gsub("{feature}", state.fetch("current_feature").to_s).gsub("{phase}", state.fetch("current_phase").to_s)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
root = Dir.pwd
|
|
45
|
+
lock = %w[create inspect].include?(command) ? DevflowControlIO.acquire_project_lock(root) : nil
|
|
46
|
+
begin
|
|
47
|
+
raise ArgumentError, "command must be create or inspect" unless %w[create inspect].include?(command)
|
|
48
|
+
raise ArgumentError, "invalid feature" unless RequirementGraph.valid_feature?(options[:feature])
|
|
49
|
+
state = load_state(root)
|
|
50
|
+
raise ArgumentError, "state feature/step mismatch" unless state["current_feature"] == options[:feature] && state["current_step"] == options[:step]
|
|
51
|
+
workflow_relative = state.fetch("workflow_config")
|
|
52
|
+
workflow_path, = DevflowControlIO.resolve_control_path(root, workflow_relative, :workflow)
|
|
53
|
+
workflow_raw = File.read(workflow_path)
|
|
54
|
+
workflow = YAML.safe_load(workflow_raw, permitted_classes: [], aliases: false)
|
|
55
|
+
step = Array(workflow["steps"]).find { |item| item["id"] == options[:step] }
|
|
56
|
+
raise ArgumentError, "unknown workflow step" unless step
|
|
57
|
+
graph, = RequirementGraph.load_graph(root, options[:feature])
|
|
58
|
+
if command == "create"
|
|
59
|
+
raise ArgumentError, "stale state revision: expected #{options[:expected_state_revision]}, actual #{state["revision"]}" if options[:expected_state_revision] && state["revision"] != options[:expected_state_revision]
|
|
60
|
+
raise ArgumentError, "stale graph revision: expected #{options[:expected_graph_revision]}, actual #{graph["revision"]}" if options[:expected_graph_revision] && graph["revision"] != options[:expected_graph_revision]
|
|
61
|
+
end
|
|
62
|
+
policy = POLICY.fetch(options[:step], { "checkers" => [], "agents" => [], "budget" => 0 })
|
|
63
|
+
paths = (Array(step["outputs"]).map { |output| output["path"] } + [step.dig("meta_output", "path")]).compact.map { |value| interpolate(value, state) }.uniq
|
|
64
|
+
contract = {
|
|
65
|
+
"schema_version" => 2, "origin" => "core_execution_contract", "feature" => options[:feature], "step" => options[:step],
|
|
66
|
+
"state_revision" => state["revision"], "graph_revision" => graph["revision"], "workflow_path" => workflow_relative,
|
|
67
|
+
"workflow_fingerprint" => Digest::SHA256.hexdigest(workflow_raw), "manifest_id" => "devflow-core-execution-policy-v2",
|
|
68
|
+
"allowed_paths" => paths, "allowed_agents" => policy["agents"], "checker_ids" => policy["checkers"],
|
|
69
|
+
"allowed_tools" => %w[read grep find glob lsp search write edit apply_patch bash shell exec_command task],
|
|
70
|
+
"tool_budget" => policy["budget"], "acceptance" => Array(graph["nodes"]).select { |node| node["kind"] == "acceptance" }.map { |node| node["id"] },
|
|
71
|
+
"allowed_graph_delta" => Array(policy["allowed_graph_delta"]),
|
|
72
|
+
"on_unknown" => "block_and_escalate"
|
|
73
|
+
}
|
|
74
|
+
frozen = contract.merge("frozen_hash" => Digest::SHA256.hexdigest(JSON.generate(contract)))
|
|
75
|
+
contract_id = "EC-#{frozen["frozen_hash"][0, 16]}"
|
|
76
|
+
frozen["contract_id"] = contract_id
|
|
77
|
+
if command == "inspect"
|
|
78
|
+
result = { "ok" => true, "contract" => frozen }
|
|
79
|
+
else
|
|
80
|
+
raise ArgumentError, "--write is required" unless options[:write]
|
|
81
|
+
destination = File.join(root, ".planning/contracts", options[:feature], "#{contract_id}.json")
|
|
82
|
+
DevflowControlIO.atomic_write(destination, JSON.pretty_generate(frozen) + "\n")
|
|
83
|
+
audit = File.join(root, ".planning/contracts", options[:feature], "control-events.jsonl")
|
|
84
|
+
existing = File.exist?(audit) ? File.read(audit) : ""
|
|
85
|
+
DevflowControlIO.atomic_write(audit, existing + JSON.generate({ "event" => "execution_contract_frozen", "contract_id" => contract_id, "state_revision" => state["revision"], "graph_revision" => graph["revision"], "frozen_hash" => frozen["frozen_hash"] }) + "\n")
|
|
86
|
+
result = { "ok" => true, "path" => destination.sub("#{root}/", ""), "contract" => frozen }
|
|
87
|
+
end
|
|
88
|
+
rescue StandardError => error
|
|
89
|
+
result = { "ok" => false, "reason" => "execution_contract_failed", "message" => error.message }
|
|
90
|
+
ensure
|
|
91
|
+
DevflowControlIO.release_lock(lock)
|
|
92
|
+
end
|
|
93
|
+
puts JSON.pretty_generate(result)
|
|
@@ -94,7 +94,8 @@ pending_steps = if File.exist?(workflow_path)
|
|
|
94
94
|
current_step = options[:step] || pending_steps.first || "requirement-triage"
|
|
95
95
|
|
|
96
96
|
state = {
|
|
97
|
-
"state_schema_version" =>
|
|
97
|
+
"state_schema_version" => 2,
|
|
98
|
+
"governance_started_revision" => 0,
|
|
98
99
|
"revision" => 0,
|
|
99
100
|
"state_updated_at" => Date.today.to_s,
|
|
100
101
|
"current_flow" => options[:flow],
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Evidence Pack v2 is deliberately assembled from the current STATE/workflow
|
|
5
|
+
# and canonical graph. Agent text is never used to decide completion.
|
|
6
|
+
require "digest"
|
|
7
|
+
require "json"
|
|
8
|
+
require "optparse"
|
|
9
|
+
require "yaml"
|
|
10
|
+
require_relative "control_io"
|
|
11
|
+
require_relative "requirement_graph"
|
|
12
|
+
|
|
13
|
+
options = { feature: nil, step: nil, contract: nil, write: false, json: false }
|
|
14
|
+
OptionParser.new do |o|
|
|
15
|
+
o.on("--feature NAME") { |v| options[:feature] = v }
|
|
16
|
+
o.on("--step ID") { |v| options[:step] = v }
|
|
17
|
+
o.on("--contract PATH") { |v| options[:contract] = v }
|
|
18
|
+
o.on("--write") { options[:write] = true }
|
|
19
|
+
o.on("--json") { options[:json] = true }
|
|
20
|
+
end.parse!
|
|
21
|
+
abort "feature, step and --write are required" unless options[:feature] && options[:step] && options[:write]
|
|
22
|
+
abort "invalid feature" unless RequirementGraph.valid_feature?(options[:feature])
|
|
23
|
+
|
|
24
|
+
def state_front_matter(root)
|
|
25
|
+
document = File.read(File.join(root, ".planning/STATE.md"))
|
|
26
|
+
front = document.match(/\A---\n(.*?)\n---/m)&.[](1)
|
|
27
|
+
raise "STATE.md has no YAML front matter" unless front
|
|
28
|
+
YAML.safe_load(front, permitted_classes: [], aliases: false)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def interpolate(path, state)
|
|
32
|
+
path.gsub("{feature}", state.fetch("current_feature", "").to_s).gsub("{phase}", state.fetch("current_phase", "").to_s)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def configured_checkers(workflow, step_id)
|
|
36
|
+
explicit = workflow.dig("governance", "step_checkers", step_id)
|
|
37
|
+
return Array(explicit) if explicit
|
|
38
|
+
case step_id
|
|
39
|
+
when "requirement-baseline" then %w[requirement-graph-check slice-independence-check acceptance-coverage-check change-impact-check]
|
|
40
|
+
when "api-contract" then %w[requirement-graph-check contract-coverage-check change-impact-check]
|
|
41
|
+
when "integration-verify", "change-plan" then %w[requirement-graph-check slice-independence-check contract-coverage-check change-impact-check]
|
|
42
|
+
when "verify" then %w[acceptance-coverage-check change-impact-check]
|
|
43
|
+
else []
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
root = Dir.pwd
|
|
48
|
+
lock = DevflowControlIO.acquire_project_lock(root)
|
|
49
|
+
begin
|
|
50
|
+
state = state_front_matter(root)
|
|
51
|
+
unless state["current_feature"] == options[:feature] && state["current_step"] == options[:step]
|
|
52
|
+
result = { "ok" => false, "reason" => "state_step_mismatch", "state_feature" => state["current_feature"], "state_step" => state["current_step"] }
|
|
53
|
+
puts JSON.pretty_generate(result)
|
|
54
|
+
exit 0
|
|
55
|
+
end
|
|
56
|
+
workflow_path, = DevflowControlIO.resolve_control_path(root, state.fetch("workflow_config"), :workflow)
|
|
57
|
+
workflow_raw = File.read(workflow_path)
|
|
58
|
+
workflow = YAML.safe_load(workflow_raw, permitted_classes: [], aliases: false)
|
|
59
|
+
step = Array(workflow["steps"]).find { |item| item["id"] == options[:step] }
|
|
60
|
+
raise "unknown workflow step" unless step
|
|
61
|
+
|
|
62
|
+
output_specs = Array(step["outputs"]).select { |item| item["required"] }
|
|
63
|
+
output_specs << step["meta_output"] if step["meta_output"].is_a?(Hash)
|
|
64
|
+
files = output_specs.map do |spec|
|
|
65
|
+
path = spec["path"]
|
|
66
|
+
next unless path
|
|
67
|
+
relative = interpolate(path, state)
|
|
68
|
+
absolute, = DevflowControlIO.resolve_control_path(root, relative, :artifact)
|
|
69
|
+
exists = File.file?(absolute)
|
|
70
|
+
{ "path" => relative, "required" => true, "exists" => exists, "bytes" => exists ? File.size(absolute) : 0,
|
|
71
|
+
"sha256" => exists ? Digest::SHA256.file(absolute).hexdigest : nil }
|
|
72
|
+
end.compact
|
|
73
|
+
|
|
74
|
+
graph, = RequirementGraph.load_graph(root, options[:feature])
|
|
75
|
+
checkers = RequirementGraph.checker_results(graph)
|
|
76
|
+
selected = configured_checkers(workflow, options[:step])
|
|
77
|
+
gate = workflow.dig("governance", "requirement_graph_gate").to_s
|
|
78
|
+
enforced = gate == "enforced"
|
|
79
|
+
checker_results = selected.map do |id|
|
|
80
|
+
issues = checkers.fetch(id, ["unknown fixed checker: #{id}"])
|
|
81
|
+
{ "id" => id, "origin" => "core_fixed_checker", "exit_code" => issues.empty? ? 0 : 1, "passed" => issues.empty?, "issues" => issues,
|
|
82
|
+
"counters" => { "issue_count" => issues.length } }
|
|
83
|
+
end
|
|
84
|
+
graph_missing = graph["revision"].zero? && !File.exist?(RequirementGraph.graph_path(root, options[:feature]).first)
|
|
85
|
+
checker_passed = !enforced || (!graph_missing && checker_results.all? { |item| item["passed"] })
|
|
86
|
+
contract_hash = nil
|
|
87
|
+
if options[:contract]
|
|
88
|
+
contract_path = File.expand_path(options[:contract], root)
|
|
89
|
+
raise "contract path escapes project" unless contract_path.start_with?("#{File.realpath(root)}#{File::SEPARATOR}") && File.file?(contract_path)
|
|
90
|
+
contract_hash = Digest::SHA256.file(contract_path).hexdigest
|
|
91
|
+
end
|
|
92
|
+
node_counts = Array(graph["nodes"]).group_by { |node| node["kind"] }.transform_values(&:length)
|
|
93
|
+
pack = {
|
|
94
|
+
"schema_version" => 2, "origin" => "core_machine", "feature" => options[:feature], "step" => options[:step],
|
|
95
|
+
"state_revision" => state["revision"], "graph_revision" => graph["revision"], "workflow_fingerprint" => Digest::SHA256.hexdigest(workflow_raw),
|
|
96
|
+
"contract_hash" => contract_hash, "files" => files, "checkers" => checker_results,
|
|
97
|
+
"counters" => { "required_file_count" => files.length, "existing_file_count" => files.count { |item| item["exists"] }, "nodes_by_kind" => node_counts,
|
|
98
|
+
"stale_node_count" => Array(graph["stale"]).length, "stale_view_count" => Hash(graph["views"]).count { |_name, view| view["needs_recheck"] } },
|
|
99
|
+
"view_fingerprints" => Hash(graph["views"]).transform_values { |view| view.select { |key, _value| %w[fingerprint graph_revision state needs_recheck stale_fingerprint].include?(key) } },
|
|
100
|
+
"complete" => files.all? { |item| item["exists"] } && checker_passed,
|
|
101
|
+
"gate_mode" => gate.empty? ? "observe" : gate
|
|
102
|
+
}.compact
|
|
103
|
+
destination = File.join(root, ".planning/evidence", options[:feature], "#{options[:step]}.machine.json")
|
|
104
|
+
DevflowControlIO.atomic_write(destination, JSON.pretty_generate(pack) + "\n")
|
|
105
|
+
result = { "ok" => true, "path" => destination.sub("#{root}/", ""), "complete" => pack["complete"], "state_revision" => pack["state_revision"], "graph_revision" => pack["graph_revision"], "checkers" => checker_results }
|
|
106
|
+
rescue StandardError => error
|
|
107
|
+
result = { "ok" => false, "reason" => "machine_evidence_failed", "message" => error.message }
|
|
108
|
+
ensure
|
|
109
|
+
DevflowControlIO.release_lock(lock)
|
|
110
|
+
end
|
|
111
|
+
puts JSON.pretty_generate(result)
|