@guanlin4924/devflow-pi 0.1.15 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanlin4924/devflow-pi",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -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, rename, writeFile } from "node:fs/promises";
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 persistContract(cwd: string, contract: ActiveExecutionContract): Promise<string> {
690
- assertRuntimeProjectBoundary(cwd);
691
- const feature = normalizeContractSegment(contract.feature, "feature");
692
- const contractId = normalizeContractSegment(contract.contractId, "contractId");
693
- const root = path.join(cwd, ".planning", "contracts", feature);
694
- const destination = path.join(root, `${contractId}.json`);
695
- await mkdir(root, { recursive: true });
696
- const payload = JSON.stringify({ schemaVersion: 1, activatedAt: new Date().toISOString(), ...contract }, null, 2) + "\n";
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
- return { block: true, reason: `Active contract ${activeContract.contractId} only allows agents: ${activeContract.allowedAgents.join(", ")}` };
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
- if (record.schemaVersion !== 1) errors.push("schemaVersion must equal 1");
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: 1, taskId, createdAt: new Date().toISOString(), ...params.pack };
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: "Show or activate a user-confirmed, path-bounded execution contract. Only an active contract may authorize low-cost implementation agents.",
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 (controlState.phase !== "planning") {
1315
- return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "Contracts may only be activated from the planning phase." });
1316
- }
1317
- if (!input.contractId || !input.feature || !input.step || input.allowedPaths.length === 0 || input.allowedAgents.length === 0 || input.acceptance.length === 0) {
1318
- return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "contractId, feature, step, allowedPaths, allowedAgents, and acceptance are required." });
1319
- }
1320
- let allowedPaths: string[];
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: input.contractId,
1332
- feature: input.feature,
1333
- step: input.step,
1334
- allowedPaths: [...new Set(allowedPaths)],
1335
- allowedAgents: [...new Set(input.allowedAgents)],
1336
- acceptance: [...new Set(input.acceptance)],
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
- `验收:${contract.acceptance.join(";")}`,
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 destination = await persistContract(ctx.cwd, contract);
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");
@@ -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" => 1,
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)