@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 +1 -1
- package/package.json +1 -1
- package/plugins/devflow-core/extensions/devflow-core.ts +62 -11
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/session_rollover.rb +16 -2
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +1 -1
- package/src/constants.ts +1 -1
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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