@guanlin4924/devflow-pi 0.1.36 → 0.1.38

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 CHANGED
@@ -11,7 +11,7 @@ Devflow Pi 是一个面向项目研发交付的独立 OMP 产品入口,内置
11
11
  安装与启动:
12
12
 
13
13
  ```bash
14
- npm install -g @guanlin4924/devflow-pi@0.1.36
14
+ npm install -g @guanlin4924/devflow-pi@0.1.38
15
15
  cd /path/to/your-project
16
16
  devflow-pi init
17
17
  devflow-pi doctor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanlin4924/devflow-pi",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -1285,6 +1285,22 @@ export default function devflowCore(pi: ExtensionAPI) {
1285
1285
  return { result, snapshot: parseJsonResult(result) };
1286
1286
  }
1287
1287
 
1288
+ async function inspectWorkflowWithRegistryRecovery(cwd: string): Promise<{ result: ProcessResult; snapshot: Record<string, unknown> | null }> {
1289
+ let inspected = await inspectWorkflow(cwd);
1290
+ const errors = Array.isArray(inspected.snapshot?.state_validation && (inspected.snapshot.state_validation as Record<string, unknown>).errors)
1291
+ ? (inspected.snapshot!.state_validation as Record<string, unknown>).errors as Array<Record<string, unknown>>
1292
+ : [];
1293
+ const onlyRegistryDrift = inspected.snapshot?.reason === "state_validation_failed" &&
1294
+ errors.length > 0 && errors.every(error => error.code === "artifact_registry_revision_mismatch");
1295
+ if (!onlyRegistryDrift) return inspected;
1296
+ const synced = await runRuby(cwd, "workflow-orchestrator/scripts/sync_artifacts.rb", [
1297
+ "--write", "--no-preserve", "--json",
1298
+ ]);
1299
+ if (!synced.ok) return inspected;
1300
+ inspected = await inspectWorkflow(cwd);
1301
+ return inspected;
1302
+ }
1303
+
1288
1304
  let activeWorkflowTurn = false;
1289
1305
  let pendingProjectInitialization: ProjectInitializationRequest | undefined;
1290
1306
  let pendingRollover: { snapshot: Record<string, unknown>; preparedRevision: number; rolloverId: string } | undefined;
@@ -1362,7 +1378,7 @@ export default function devflowCore(pi: ExtensionAPI) {
1362
1378
  }
1363
1379
  }
1364
1380
  if (rolloverState && typeof rolloverState.revision === "number" && rollover?.status === "prepared" && typeof rollover.id === "string") {
1365
- const workflow = await inspectWorkflow(ctx.cwd);
1381
+ const workflow = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
1366
1382
  const expectedStateRevision = typeof rollover.expected_state_revision === "number" ? rollover.expected_state_revision : undefined;
1367
1383
  if (workflow.result.ok && workflow.snapshot && expectedStateRevision !== undefined && expectedStateRevision + 1 === rolloverState.revision) {
1368
1384
  pendingRollover = { snapshot: workflow.snapshot, preparedRevision: rolloverState.revision, rolloverId: rollover.id };
@@ -1373,8 +1389,13 @@ export default function devflowCore(pi: ExtensionAPI) {
1373
1389
  const consumed = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/session_rollover.rb", [
1374
1390
  "consume", "--write", "--json", "--expected-revision", String(rolloverState.revision), "--target-session-id", currentSessionId,
1375
1391
  ]);
1376
- const after = await inspectWorkflow(ctx.cwd);
1392
+ const after = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
1377
1393
  if (consumed.ok && after.result.ok && after.snapshot) {
1394
+ if (after.snapshot.action === "blocked") {
1395
+ setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "启动恢复校验失败" });
1396
+ ctx.ui.notify(`会话已恢复,但工作流校验失败,未启动 AI:${String(after.snapshot.reason ?? "state validation failed")}`, "error");
1397
+ return;
1398
+ }
1378
1399
  setControlState(ctx, executingState("恢复已认领的干净会话"));
1379
1400
  await triggerWorkflow(after.snapshot, "approval");
1380
1401
  } else if (!consumed.ok) {
@@ -1421,8 +1442,14 @@ export default function devflowCore(pi: ExtensionAPI) {
1421
1442
  pendingRollover = undefined;
1422
1443
  activeContract = undefined;
1423
1444
  contractAgentCalls = 0;
1445
+ const next = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
1446
+ if (!next.result.ok || !next.snapshot || next.snapshot.action === "blocked") {
1447
+ setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "启动恢复校验失败" });
1448
+ ctx.ui.notify(`会话已切换,但工作流校验失败,未启动 AI:${String(next.snapshot?.reason ?? next.result.stderr ?? next.result.stdout ?? "state validation failed")}`, "error");
1449
+ return;
1450
+ }
1424
1451
  setControlState(ctx, executingState("在干净会话中执行下一工作流步骤"));
1425
- await triggerWorkflow(pending.snapshot, "approval");
1452
+ await triggerWorkflow(next.snapshot, "approval");
1426
1453
  });
1427
1454
 
1428
1455
  pi.on("before_agent_start", async event => ({
@@ -1444,7 +1471,7 @@ export default function devflowCore(pi: ExtensionAPI) {
1444
1471
  if (controlState.intent === "requirement-draft" && controlState.phase === "planning") {
1445
1472
  const requirementDraft = event.text.trim();
1446
1473
  if (!requirementDraft) return { handled: true };
1447
- const inspected = await inspectWorkflow(ctx.cwd);
1474
+ const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
1448
1475
  if (!inspected.result.ok || !inspected.snapshot) {
1449
1476
  ctx.ui.notify(`需求分诊启动失败:${inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON"}`, "error");
1450
1477
  return { handled: true };
@@ -1468,13 +1495,37 @@ export default function devflowCore(pi: ExtensionAPI) {
1468
1495
  return { handled: true };
1469
1496
  }
1470
1497
  }
1498
+ // A step transition can succeed before the runtime manages to prepare the
1499
+ // fresh-session handoff. Keep approval useful in this recovery state, but
1500
+ // retry only the handoff; never repeat the already-persisted transition.
1501
+ if (controlState.phase === "blocked" && controlState.label === "会话轮换准备失败" && controlAction === "approve") {
1502
+ const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
1503
+ if (!inspected.result.ok || !inspected.snapshot) {
1504
+ ctx.ui.notify(`会话交接重试失败:${inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON"}`, "error");
1505
+ return { handled: true };
1506
+ }
1507
+ if (inspected.snapshot.action === "workflow_complete") {
1508
+ setControlState(ctx, initialControlState());
1509
+ ctx.ui.notify("工作流已完成。", "info");
1510
+ return { handled: true };
1511
+ }
1512
+ setControlState(ctx, waitingUserState("正在重试干净会话交接"));
1513
+ try {
1514
+ await requestControlledRollover(ctx, inspected.snapshot);
1515
+ ctx.ui.notify("已重新登记会话交接;当前回合结束后将自动切换到下一节点。", "info");
1516
+ } catch (error) {
1517
+ setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "会话轮换准备失败" });
1518
+ ctx.ui.notify(`会话交接仍未完成:${String(error)}`, "error");
1519
+ }
1520
+ return { handled: true };
1521
+ }
1471
1522
  if (controlState.phase === "awaiting-approval" || controlState.phase === "waiting-user") {
1472
1523
  if (controlAction === "reject") {
1473
1524
  ctx.ui.notify("已保留当前步骤和产物。请直接说明需要修改的内容;未执行任何推进。", "info");
1474
1525
  return { handled: true };
1475
1526
  }
1476
1527
  if (controlAction === "approve") {
1477
- const inspected = await inspectWorkflow(ctx.cwd);
1528
+ const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
1478
1529
  if (!inspected.result.ok || !inspected.snapshot) {
1479
1530
  ctx.ui.notify(`审批处理失败:${inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON"}`, "error");
1480
1531
  return { handled: true };
@@ -1499,7 +1550,7 @@ export default function devflowCore(pi: ExtensionAPI) {
1499
1550
  ctx.ui.notify(`未推进:${result.stderr || result.stdout}`, "error");
1500
1551
  return { handled: true };
1501
1552
  }
1502
- const after = await inspectWorkflow(ctx.cwd);
1553
+ const after = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
1503
1554
  activeContract = undefined;
1504
1555
  contractAgentCalls = 0;
1505
1556
  if (!after.result.ok || !after.snapshot || after.snapshot.action === "workflow_complete") {
@@ -2489,7 +2540,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2489
2540
  }
2490
2541
  args.push("--checkpoint-decision", params.decision);
2491
2542
  } else if (params.action === "cancel-feature") {
2492
- const inspected = await inspectWorkflow(ctx.cwd);
2543
+ const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
2493
2544
  const snapshot = inspected.snapshot;
2494
2545
  const feature = typeof snapshot?.current_feature === "string" ? snapshot.current_feature : "当前需求";
2495
2546
  const revision = typeof snapshot?.revision === "number" ? snapshot.revision : undefined;
@@ -2617,7 +2668,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2617
2668
  return;
2618
2669
  }
2619
2670
  if (trimmed === "explain-state") {
2620
- const inspected = await inspectWorkflow(ctx.cwd);
2671
+ const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
2621
2672
  ctx.ui.notify(
2622
2673
  inspected.result.ok ? explainWorkflowSnapshot(inspected.snapshot) : `状态读取失败:${inspected.result.stderr || inspected.result.stdout}`,
2623
2674
  inspected.result.ok ? "info" : "error",
@@ -2626,7 +2677,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2626
2677
  return;
2627
2678
  }
2628
2679
  if (trimmed === "recover") {
2629
- const inspected = await inspectWorkflow(ctx.cwd);
2680
+ const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
2630
2681
  const plan = inspected.snapshot?.recovery_plan as Record<string, unknown> | undefined;
2631
2682
  const revision = plan?.expected_revision;
2632
2683
  if (!inspected.result.ok || !plan || plan.classification !== "safe_repairable" || typeof revision !== "number") {
@@ -2665,7 +2716,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2665
2716
  return;
2666
2717
  }
2667
2718
  if (trimmed === "abandon" || trimmed.startsWith("abandon ") || trimmed === "cancel" || trimmed.startsWith("cancel ")) {
2668
- const inspected = await inspectWorkflow(ctx.cwd);
2719
+ const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
2669
2720
  const snapshot = inspected.snapshot;
2670
2721
  const revision = typeof snapshot?.revision === "number" ? snapshot.revision : undefined;
2671
2722
  const feature = typeof snapshot?.current_feature === "string" ? snapshot.current_feature : undefined;
@@ -2734,7 +2785,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2734
2785
  ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), "error");
2735
2786
  return;
2736
2787
  }
2737
- const inspected = await inspectWorkflow(ctx.cwd);
2788
+ const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
2738
2789
  if (!inspected.result.ok || !inspected.snapshot) {
2739
2790
  ctx.ui.notify(
2740
2791
  `Devflow resume failed: ${inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON"}`,
@@ -98,8 +98,22 @@ result = catch(:transition_done) do
98
98
  result = fail_result("handoff_integrity_failure", "rollover" => rollover)
99
99
  throw :transition_done, result
100
100
  end
101
- result = fail_result("rollover_in_progress", "rollover" => rollover)
102
- throw :transition_done, result
101
+ if expected_state_revision.is_a?(Integer) && expected_state_revision < revision - 1
102
+ # The state has moved beyond the revision captured by this prepared
103
+ # handoff. It is an orphan from an interrupted/retried runtime turn,
104
+ # not an active lock. Verify its immutable evidence, preserve the
105
+ # handoff file on disk, and replace the STATE pointer with a handoff
106
+ # for the current revision.
107
+ existing_path, = DevflowControlIO.resolve_control_path(root, rollover["handoff_path"], :handoff)
108
+ unless File.exist?(existing_path) && Digest::SHA256.hexdigest(File.binread(existing_path)) == rollover["handoff_sha256"]
109
+ result = fail_result("handoff_integrity_failure", "rollover" => rollover)
110
+ throw :transition_done, result
111
+ end
112
+ rollover = nil
113
+ else
114
+ result = fail_result("rollover_in_progress", "rollover" => rollover)
115
+ throw :transition_done, result
116
+ end
103
117
  elsif rollover && rollover["status"] == "claimed"
104
118
  result = fail_result("rollover_in_progress", "rollover" => rollover)
105
119
  throw :transition_done, result
@@ -132,7 +132,7 @@ module WorkflowStateValidator
132
132
  return
133
133
  end
134
134
  status = rollover["status"]
135
- unless %w[prepared claimed consumed failed].include?(status)
135
+ unless %w[prepared claimed consumed superseded failed].include?(status)
136
136
  issues << issue("error", "invalid_rollover_status", "STATE.rollover.status is invalid", status: status)
137
137
  return
138
138
  end
package/src/constants.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PRODUCT_NAME = "Devflow Pi";
2
- export const PRODUCT_VERSION = "0.1.36";
2
+ export const PRODUCT_VERSION = "0.1.38";
3
3
  export const PROJECT_SCHEMA_VERSION = 1;
4
4
  export const OMP_RUNTIME_VERSION = "17.2.7";
5
5