@guanlin4924/devflow-pi 0.1.40 → 0.1.47
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 +609 -42
- package/plugins/devflow-core/skills/wireframe-proposal/SKILL.md +13 -0
- package/plugins/devflow-core/skills/wireframe-proposal/agents/openai.yaml +4 -0
- package/plugins/devflow-core/skills/wireframe-render/SKILL.md +8 -0
- package/plugins/devflow-core/skills/wireframe-review/SKILL.md +8 -0
- package/plugins/devflow-core/skills/wireframe-validate/SKILL.md +8 -0
- package/plugins/devflow-core/skills/wireframe-validate/agents/openai.yaml +4 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/SKILL.md +16 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-candidate-delta.schema.yaml +41 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-claim.schema.yaml +10 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-model-policy.yaml +51 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-model.schema.yaml +241 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-source-registry.schema.yaml +80 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/contracts/proposal.schema.yaml +44 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/contracts/questions.schema.yaml +14 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_suite.rb +13 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +143 -28
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_claim_validate.py +77 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_closure_e2e_seed.json +516 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_cross_model_check.py +76 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_impact.py +51 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_invalidation.rb +108 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_commit.rb +570 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_gate.rb +89 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_render.py +131 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_validate.py +1063 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_source_resolver.rb +365 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checker_registry.rb +2 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +3 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_contract.rb +3 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_runtime.rb +13 -4
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_recover.rb +386 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_run.rb +28 -2
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph.rb +139 -14
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph_backfill.rb +143 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/session_rollover.rb +15 -4
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/sync_artifacts.rb +45 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +10 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_commit.rb +222 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_render.py +231 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_validate.py +457 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +106 -1
- package/plugins/devflow-implementation/agents/implementation-worker.md +6 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/code_index.py +107 -37
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/core/scanner.py +20 -1
- package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +1 -1
- package/src/constants.ts +1 -1
- package/src/runtime.ts +53 -22
|
@@ -278,14 +278,37 @@ export function isRequirementDraftToolAllowed(toolName: string, input: unknown,
|
|
|
278
278
|
return normalized === "task" && taskAgents.length > 0 && taskAgents.every(agent => PLANNING_AGENTS.has(agent));
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
* can
|
|
285
|
-
|
|
286
|
-
|
|
281
|
+
type InternalDevflowControlResource = "contract" | "state" | "mainflow";
|
|
282
|
+
|
|
283
|
+
/** Resolve an xdev target only when all supplied target aliases agree.
|
|
284
|
+
* A direct/eval caller can otherwise provide `path` for a real file plus an
|
|
285
|
+
* unrelated `uri` alias and trick a permissive `some(...)` check. */
|
|
286
|
+
function unambiguousVirtualResourceTarget(input: unknown): string | undefined {
|
|
287
|
+
if (!input || typeof input !== "object") return undefined;
|
|
287
288
|
const record = input as Record<string, unknown>;
|
|
288
|
-
|
|
289
|
+
const targets = [record.path, record.uri, record.resource]
|
|
290
|
+
.filter((value): value is string => typeof value === "string" && value.length > 0);
|
|
291
|
+
if (targets.length === 0 || targets.some(value => value !== targets[0])) return undefined;
|
|
292
|
+
return targets[0];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** `xd://devflow_contract` and `xd://devflow_state` are OMP transports for
|
|
296
|
+
* the existing named Core tools, not project-file writes. Once the outer
|
|
297
|
+
* transport is admitted, OMP resolves the canonical tool, validates its
|
|
298
|
+
* schema, applies its approval tier, and emits the native tool_call before
|
|
299
|
+
* executing the single-sourced handler. */
|
|
300
|
+
export function internalDevflowControlResource(toolName: string, input: unknown): InternalDevflowControlResource | undefined {
|
|
301
|
+
if (!["read", "write"].includes(toolName.toLowerCase()) || !input || typeof input !== "object") return undefined;
|
|
302
|
+
const target = unambiguousVirtualResourceTarget(input);
|
|
303
|
+
if (target === "xd://devflow_contract") return "contract";
|
|
304
|
+
if (target === "xd://devflow_state") return "state";
|
|
305
|
+
if (target === "xd://devflow_mainflow") return "mainflow";
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Backward-compatible predicate for callers that only care about contracts. */
|
|
310
|
+
export function isInternalDevflowContractResource(toolName: string, input: unknown): boolean {
|
|
311
|
+
return internalDevflowControlResource(toolName, input) === "contract";
|
|
289
312
|
}
|
|
290
313
|
|
|
291
314
|
export type NaturalControlAction = "approve" | "reject";
|
|
@@ -301,6 +324,13 @@ export function isStateMutationTool(toolName: string): boolean {
|
|
|
301
324
|
return STATE_MUTATION_TOOLS.has(toolName.toLowerCase());
|
|
302
325
|
}
|
|
303
326
|
|
|
327
|
+
/** Generic xdev writes must not bypass the named Devflow transition guard. */
|
|
328
|
+
export function isVirtualStateTransitionResource(toolName: string, input: unknown): boolean {
|
|
329
|
+
if (toolName.toLowerCase() !== "write" || !input || typeof input !== "object") return false;
|
|
330
|
+
const record = input as Record<string, unknown>;
|
|
331
|
+
return [record.path, record.uri, record.resource].some(value => value === "xd://devflow_transition");
|
|
332
|
+
}
|
|
333
|
+
|
|
304
334
|
export function planningToolBlockReason(toolName: string): string {
|
|
305
335
|
return `Devflow ${toolName} blocked: current intent is planning. Planning is read-only; present an Evidence Pack or plan and wait for explicit approval before writing or executing.`;
|
|
306
336
|
}
|
|
@@ -580,6 +610,11 @@ function isCoreOwnedGovernancePath(filePath: string): boolean {
|
|
|
580
610
|
|| /^\.planning\/contracts\/[^/\\\x00-\x1F\x7F]+\/(?:EC-[A-Fa-f0-9]+\.json|control-events\.jsonl)$/u.test(filePath);
|
|
581
611
|
}
|
|
582
612
|
|
|
613
|
+
function planningFeatureFromPath(filePath: string): string | null {
|
|
614
|
+
const match = filePath.match(/^\.planning\/(?:requirements|features)\/([^/\\\x00-\x1F\x7F]+)(?:\/|$)/u);
|
|
615
|
+
return match?.[1] ?? null;
|
|
616
|
+
}
|
|
617
|
+
|
|
583
618
|
function resolveIntelSkillRoot(cwd: string): string | undefined {
|
|
584
619
|
if (isEmbeddedRuntime()) {
|
|
585
620
|
const packaged = path.join(resolvePluginRoot(), "devflow-intel", "skills");
|
|
@@ -749,6 +784,67 @@ function compactStep(value: unknown): Record<string, unknown> | null {
|
|
|
749
784
|
};
|
|
750
785
|
}
|
|
751
786
|
|
|
787
|
+
/**
|
|
788
|
+
* The approval prompt is session-local, while the completed step outputs are
|
|
789
|
+
* durable. After a rollover, compaction, or runtime restart the in-memory
|
|
790
|
+
* control state can be idle even though the user is still looking at a
|
|
791
|
+
* completed step. This predicate lets the runtime re-arm approval handling
|
|
792
|
+
* from the Core-owned snapshot without granting approval by itself.
|
|
793
|
+
*/
|
|
794
|
+
function isReviewableWorkflowSnapshot(snapshot: Record<string, unknown>): boolean {
|
|
795
|
+
const status = snapshot.status;
|
|
796
|
+
if (status !== "waiting_checkpoint" && status !== "not_started" && status !== "in_progress") return false;
|
|
797
|
+
const step = snapshot.current_step;
|
|
798
|
+
if (!step || typeof step !== "object") return false;
|
|
799
|
+
const stepRecord = step as Record<string, unknown>;
|
|
800
|
+
if (typeof stepRecord.id !== "string" || !stepRecord.id) return false;
|
|
801
|
+
const outputs = Array.isArray(stepRecord.outputs) ? stepRecord.outputs : [];
|
|
802
|
+
const requiredOutputs = outputs.filter(output => {
|
|
803
|
+
if (!output || typeof output !== "object") return false;
|
|
804
|
+
return (output as Record<string, unknown>).required !== false;
|
|
805
|
+
});
|
|
806
|
+
return requiredOutputs.length > 0 && requiredOutputs.every(output => {
|
|
807
|
+
if (!output || typeof output !== "object") return false;
|
|
808
|
+
const record = output as Record<string, unknown>;
|
|
809
|
+
return record.exists === true && !["failed", "blocked"].includes(String(record.artifact_status ?? ""));
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
export type WorkflowResumeDisposition = "execute" | "review" | "blocked" | "complete" | "idle";
|
|
814
|
+
|
|
815
|
+
/** Decide how an explicit workflow resume should re-enter the Runtime.
|
|
816
|
+
*
|
|
817
|
+
* Core STATE is authoritative, but output presence determines whether the
|
|
818
|
+
* current step is ready for review or still needs an agent turn. In
|
|
819
|
+
* particular, `run_next_step` does not mean that an existing current step is
|
|
820
|
+
* complete: after a process restart its required output may still be absent.
|
|
821
|
+
*/
|
|
822
|
+
export function workflowResumeDisposition(snapshot: Record<string, unknown>): WorkflowResumeDisposition {
|
|
823
|
+
const action = typeof snapshot.action === "string" ? snapshot.action : "";
|
|
824
|
+
const status = typeof snapshot.status === "string" ? snapshot.status : "";
|
|
825
|
+
if (["workflow_complete", "workflow_cancelled", "workflow_failed"].includes(action) || ["completed", "cancelled", "failed"].includes(status)) {
|
|
826
|
+
return "complete";
|
|
827
|
+
}
|
|
828
|
+
if (action === "blocked") return "blocked";
|
|
829
|
+
|
|
830
|
+
const feature = typeof snapshot.current_feature === "string" ? snapshot.current_feature : "";
|
|
831
|
+
const revision = snapshot.revision;
|
|
832
|
+
const selectedStep = compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step);
|
|
833
|
+
const stepId = typeof selectedStep?.id === "string" ? selectedStep.id : "";
|
|
834
|
+
if (!feature || !stepId || typeof revision !== "number" || !Number.isInteger(revision)) return "idle";
|
|
835
|
+
|
|
836
|
+
if (action === "await_user_confirmation") {
|
|
837
|
+
return isReviewableWorkflowSnapshot(snapshot) ? "review" : "blocked";
|
|
838
|
+
}
|
|
839
|
+
// Parallel windows must be reconstructed by the Runtime even if a stale
|
|
840
|
+
// join artifact happens to exist; branch lifecycle is the join authority.
|
|
841
|
+
if (action === "run_parallel_window" || action === "route_failure_branch") return "execute";
|
|
842
|
+
if (action === "run_next_step") {
|
|
843
|
+
return isReviewableWorkflowSnapshot(snapshot) ? "review" : "execute";
|
|
844
|
+
}
|
|
845
|
+
return "blocked";
|
|
846
|
+
}
|
|
847
|
+
|
|
752
848
|
function workflowDirective(snapshot: Record<string, unknown>, source: "start" | "approval", requirementDraft?: string): string {
|
|
753
849
|
const selectedStep = compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step);
|
|
754
850
|
const envelope = {
|
|
@@ -778,8 +874,8 @@ function workflowDirective(snapshot: Record<string, unknown>, source: "start" |
|
|
|
778
874
|
return [
|
|
779
875
|
"<devflow-runtime-directive>",
|
|
780
876
|
"Devflow 已通过启动预检。读取 skill://workflow-pipeline,并只执行当前项目的一个工作流步骤。",
|
|
781
|
-
"先调用 devflow_state(action=\"inspect\") 重新确认 revision
|
|
782
|
-
"只按 inspector 返回的当前 step 加载所需 Skill
|
|
877
|
+
"先调用 devflow_state(action=\"inspect\") 重新确认 revision,再调用 devflow_contract(action=\"status\");若没有匹配当前 feature/current_step 的 fresh contract,立即调用 devflow_contract(action=\"activate\", feature=<current_feature>, step=<current_step>),激活成功后才能执行或写入。",
|
|
878
|
+
"只按 inspector 返回的当前 step 加载所需 Skill:step.skill 是精确 ID,必须读取 skill://<step.skill>,不得自行添加 workflow- 前缀或改写名称;本回合严禁读取或写入 xd://devflow_transition,严禁调用 devflow_transition,严禁执行 Skill 中的 complete-step/confirm-checkpoint,不得推进下一步骤;状态写回和会话切换由 Runtime 在用户确认后独占处理。",
|
|
783
879
|
...(parallelInstructions.length > 0 ? parallelInstructions : [
|
|
784
880
|
"完成本步骤的真实产物后,报告证据、未决项和验证结果并停止。请用户审查后明确回复“批准执行”才可推进;回复修改意见或“拒绝执行”则不得推进。",
|
|
785
881
|
]),
|
|
@@ -846,6 +942,18 @@ function parallelBranchContext(input: unknown): { branch_id: string; attempt: nu
|
|
|
846
942
|
return { branch_id: branch, attempt: Number(attempt), branch_revision: Number(revision) };
|
|
847
943
|
}
|
|
848
944
|
|
|
945
|
+
export function taskResultHasSuccessfulDispatch(details: unknown): boolean {
|
|
946
|
+
if (!details || typeof details !== "object") return false;
|
|
947
|
+
const record = details as Record<string, unknown>;
|
|
948
|
+
if (Array.isArray(record.results) && record.results.length > 0) return true;
|
|
949
|
+
if (!Array.isArray(record.progress)) return false;
|
|
950
|
+
return record.progress.some(item => {
|
|
951
|
+
if (!item || typeof item !== "object") return false;
|
|
952
|
+
const status = String((item as Record<string, unknown>).status ?? "");
|
|
953
|
+
return status !== "failed" && status !== "aborted";
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
|
|
849
957
|
function isControlPath(filePath: string): boolean {
|
|
850
958
|
return CONTROL_PATHS.includes(filePath) || CONTROL_PREFIXES.some(prefix => filePath.startsWith(prefix));
|
|
851
959
|
}
|
|
@@ -855,7 +963,7 @@ function isDirectControlMutation(toolName: string, input: unknown, cwd: string):
|
|
|
855
963
|
return inputPaths(input, cwd).some(isControlPath);
|
|
856
964
|
}
|
|
857
965
|
|
|
858
|
-
function isShellControlMutation(toolName: string, input: unknown): boolean {
|
|
966
|
+
export function isShellControlMutation(toolName: string, input: unknown): boolean {
|
|
859
967
|
if (!["bash", "shell", "exec_command"].includes(toolName) || !input || typeof input !== "object") {
|
|
860
968
|
return false;
|
|
861
969
|
}
|
|
@@ -866,7 +974,7 @@ function isShellControlMutation(toolName: string, input: unknown): boolean {
|
|
|
866
974
|
command,
|
|
867
975
|
);
|
|
868
976
|
const namesControlPath = [...CONTROL_PATHS, ...CONTROL_PREFIXES].some(item => command.includes(item)) || /\.planning\/requirements\//.test(command);
|
|
869
|
-
const looksLikeWrite = /(?:^|[;&|]\s*)(?:rm|mv|cp|install|truncate)\b|(?:^|\s)(?:tee|sed\s+-i|perl\s+-i)\b
|
|
977
|
+
const looksLikeWrite = /(?:^|[;&|]\s*)(?:rm|mv|cp|install|truncate)\b|(?:^|\s)(?:tee|sed\s+-i|perl\s+-i)\b|(?:^|[\s;&|])(?:\d*)>{1,2}(?![>=])\s*\S/.test(
|
|
870
978
|
command,
|
|
871
979
|
);
|
|
872
980
|
return invokesWriter || (namesControlPath && looksLikeWrite);
|
|
@@ -884,8 +992,108 @@ function shellMutationPaths(command: string, cwd: string): { mutating: boolean;
|
|
|
884
992
|
return { mutating: true, paths };
|
|
885
993
|
}
|
|
886
994
|
|
|
995
|
+
type StepContractAuthority = Pick<ActiveExecutionContract, "feature" | "step">;
|
|
996
|
+
|
|
997
|
+
const CONTRACT_CONTROL_TOOLS = new Set([
|
|
998
|
+
"devflow_artifact",
|
|
999
|
+
"devflow_bootstrap",
|
|
1000
|
+
"devflow_contract",
|
|
1001
|
+
"devflow_recovery",
|
|
1002
|
+
"devflow_transition",
|
|
1003
|
+
]);
|
|
1004
|
+
|
|
1005
|
+
const CONTRACT_READ_TOOLS = new Set([
|
|
1006
|
+
"devflow_acceptance_tests",
|
|
1007
|
+
"devflow_doctor",
|
|
1008
|
+
"devflow_state",
|
|
1009
|
+
"devflow_usage_stats",
|
|
1010
|
+
"usage_stats",
|
|
1011
|
+
]);
|
|
1012
|
+
|
|
1013
|
+
function inputAction(input: unknown): string {
|
|
1014
|
+
return input && typeof input === "object" ? String((input as Record<string, unknown>).action ?? "") : "";
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function stepScopedMutationOwner(toolName: string, input: unknown): string | null | undefined {
|
|
1018
|
+
const normalized = toolName.toLowerCase();
|
|
1019
|
+
// `write xd://devflow_*` is only the OMP transport. The nested canonical
|
|
1020
|
+
// tool_call is authorized below under its real `devflow_*` name.
|
|
1021
|
+
if (internalDevflowControlResource(normalized, input)) return undefined;
|
|
1022
|
+
if (["write", "edit", "apply_patch", "notebook_edit", "task", "bash", "shell", "exec_command"].includes(normalized)) return null;
|
|
1023
|
+
if (!normalized.startsWith("devflow_") || CONTRACT_CONTROL_TOOLS.has(normalized) || CONTRACT_READ_TOOLS.has(normalized)) return undefined;
|
|
1024
|
+
const action = inputAction(input);
|
|
1025
|
+
if (normalized === "devflow_code_index") return action === "scan" ? "code-index-refresh" : undefined;
|
|
1026
|
+
if (normalized === "devflow_requirement_graph") {
|
|
1027
|
+
const writeContext = input && typeof input === "object" && (input as Record<string, unknown>).write === true;
|
|
1028
|
+
return ["propose", "confirm"].includes(action) || (action === "context" && writeContext) ? null : undefined;
|
|
1029
|
+
}
|
|
1030
|
+
if (normalized === "devflow_api_test_matrix") return action === "validate" ? undefined : "acceptance-test-design";
|
|
1031
|
+
if (normalized === "devflow_tdd_evidence") return action === "inspect" ? undefined : "execute";
|
|
1032
|
+
if (normalized === "devflow_run_acceptance_tests") return action === "run" ? "run-acceptance-tests" : undefined;
|
|
1033
|
+
if (normalized === "devflow_mainflow") return "generate-mainflow-cases";
|
|
1034
|
+
if (normalized === "devflow_wireframe" || normalized === "devflow_design_rule") return "ui-spec";
|
|
1035
|
+
if (normalized.startsWith("devflow_architecture_") || normalized === "devflow_workload_constraint") {
|
|
1036
|
+
return action === "inspect" ? undefined : "architecture-feasibility";
|
|
1037
|
+
}
|
|
1038
|
+
if (normalized === "devflow_runtime_context" || normalized === "devflow_static_runtime_context") {
|
|
1039
|
+
return action === "inspect" ? undefined : null;
|
|
1040
|
+
}
|
|
1041
|
+
if (normalized === "devflow_evidence") return ["read", "validate"].includes(action) ? undefined : null;
|
|
1042
|
+
if (normalized === "devflow_machine_evidence") {
|
|
1043
|
+
const step = input && typeof input === "object" ? (input as Record<string, unknown>).step : undefined;
|
|
1044
|
+
return typeof step === "string" && step ? step : null;
|
|
1045
|
+
}
|
|
1046
|
+
// New mutating Devflow tools must declare a step above or be explicitly
|
|
1047
|
+
// classified read/control-only; they never inherit a broad devflow_* bypass.
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function snapshotStepId(snapshot: Record<string, unknown>): string | undefined {
|
|
1052
|
+
const step = snapshot.current_step;
|
|
1053
|
+
if (typeof step === "string" && step) return step;
|
|
1054
|
+
if (step && typeof step === "object" && typeof (step as Record<string, unknown>).id === "string") {
|
|
1055
|
+
return String((step as Record<string, unknown>).id);
|
|
1056
|
+
}
|
|
1057
|
+
return undefined;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
export function contractActivationPhaseAllowed(phase: ControlPhase, activeWorkflowTurn: boolean): boolean {
|
|
1061
|
+
return ["planning", "waiting-user", "awaiting-approval"].includes(phase) || (phase === "executing" && activeWorkflowTurn);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
export function stepScopedMutationBlockReason(
|
|
1065
|
+
toolName: string,
|
|
1066
|
+
input: unknown,
|
|
1067
|
+
_cwd: string,
|
|
1068
|
+
snapshot: Record<string, unknown>,
|
|
1069
|
+
contract: StepContractAuthority | undefined,
|
|
1070
|
+
contractFresh: boolean,
|
|
1071
|
+
): string | undefined {
|
|
1072
|
+
const owner = stepScopedMutationOwner(toolName, input);
|
|
1073
|
+
if (owner === undefined) return undefined;
|
|
1074
|
+
const feature = typeof snapshot.current_feature === "string" ? snapshot.current_feature : undefined;
|
|
1075
|
+
const step = snapshotStepId(snapshot);
|
|
1076
|
+
if (!feature || !step) return "Active Devflow workflow mutation blocked: authoritative feature/current_step is unavailable.";
|
|
1077
|
+
if (snapshot.status === "waiting_checkpoint") {
|
|
1078
|
+
return `Active Devflow workflow is waiting at a checkpoint after ${step}; approve or reject it before any further mutation.`;
|
|
1079
|
+
}
|
|
1080
|
+
if (!contract) return `Workflow step ${feature}/${step} requires an active execution contract before ${toolName}.`;
|
|
1081
|
+
if (contract.feature !== feature || contract.step !== step) {
|
|
1082
|
+
return `Active contract ${contract.feature}/${contract.step} does not match authoritative workflow step ${feature}/${step}.`;
|
|
1083
|
+
}
|
|
1084
|
+
if (!contractFresh) return `Active contract for ${feature}/${step} is stale against authoritative STATE or requirement graph.`;
|
|
1085
|
+
if (owner && owner !== step) return `${toolName} belongs to workflow step ${owner}, but authoritative current_step is ${step}.`;
|
|
1086
|
+
return undefined;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
887
1089
|
export default function devflowCore(pi: ExtensionAPI) {
|
|
888
1090
|
const { z } = pi.zod;
|
|
1091
|
+
// CLI --resume means "continue this chat session" at the OMP layer. The
|
|
1092
|
+
// launcher also supplies this one-shot, process-owned signal so Devflow can
|
|
1093
|
+
// safely resume an authoritative incomplete workflow step. It is consumed
|
|
1094
|
+
// during extension construction and never becomes model-controlled state.
|
|
1095
|
+
const cliSessionResumeRequested = process.env.DEVFLOW_CLI_SESSION_RESUME === "1";
|
|
1096
|
+
delete process.env.DEVFLOW_CLI_SESSION_RESUME;
|
|
889
1097
|
let sessionId = randomUUID();
|
|
890
1098
|
let controlState: SessionControlState = initialControlState();
|
|
891
1099
|
let activeContract: ActiveExecutionContract | undefined;
|
|
@@ -1355,10 +1563,17 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1355
1563
|
let activeWorkflowTurn = false;
|
|
1356
1564
|
let parallelRunActive = false;
|
|
1357
1565
|
let parallelContinuationQueued = false;
|
|
1566
|
+
let parallelDispatchSucceededThisTurn = false;
|
|
1567
|
+
let parallelDispatchFailureThisTurn = "";
|
|
1358
1568
|
const pendingParallelLifecycle = new Map<string, string>();
|
|
1569
|
+
const pendingParallelLifecycleContexts = new Map<string, { branch_id: string; attempt: number; branch_revision: number }>();
|
|
1359
1570
|
const parallelTaskContexts = new Map<string, { branch_id: string; attempt: number; branch_revision: number }>();
|
|
1360
1571
|
let pendingProjectInitialization: ProjectInitializationRequest | undefined;
|
|
1361
1572
|
let pendingRollover: { snapshot: Record<string, unknown>; preparedRevision: number; rolloverId: string } | undefined;
|
|
1573
|
+
// A planner may request bootstrap, but only the runtime may authorize the
|
|
1574
|
+
// state mutation. Keep the authorization in memory and bind it to exactly
|
|
1575
|
+
// one tool call; never persist or expose it as a model-controlled flag.
|
|
1576
|
+
const runtimeBootstrapAuthorizations = new Set<string>();
|
|
1362
1577
|
|
|
1363
1578
|
async function enqueueControlledRolloverRequest(cwd: string, rolloverId: string, sourceSessionId: string) {
|
|
1364
1579
|
const requestPath = path.join(cwd, ".planning", "rollover-request.json");
|
|
@@ -1381,6 +1596,12 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1381
1596
|
const rollover = payload.rollover as Record<string, unknown> | undefined;
|
|
1382
1597
|
const rolloverId = typeof rollover?.id === "string" ? rollover.id : undefined;
|
|
1383
1598
|
if (!rolloverId) throw new Error("rollover prepare returned no rollover id");
|
|
1599
|
+
const synced = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/sync_artifacts.rb", [
|
|
1600
|
+
"--write", "--no-preserve", "--json",
|
|
1601
|
+
]);
|
|
1602
|
+
if (!controlPlaneSucceeded(synced)) {
|
|
1603
|
+
throw new Error(synced.stderr || synced.stdout || "artifact registry sync after rollover prepare failed");
|
|
1604
|
+
}
|
|
1384
1605
|
await enqueueControlledRolloverRequest(ctx.cwd, rolloverId, ctx.sessionManager.getSessionId());
|
|
1385
1606
|
pendingRollover = { snapshot, preparedRevision: payload.revision, rolloverId };
|
|
1386
1607
|
}
|
|
@@ -1435,16 +1656,169 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1435
1656
|
parallelRunActive = false;
|
|
1436
1657
|
parallelContinuationQueued = false;
|
|
1437
1658
|
}
|
|
1659
|
+
if (snapshot.action === "run_parallel_window") {
|
|
1660
|
+
parallelDispatchSucceededThisTurn = false;
|
|
1661
|
+
parallelDispatchFailureThisTurn = "";
|
|
1662
|
+
}
|
|
1438
1663
|
const preparedSnapshot = await prepareParallelRun(projectRoot ?? process.cwd(), snapshot);
|
|
1439
1664
|
activeWorkflowTurn = true;
|
|
1440
1665
|
await pi.sendUserMessage(workflowDirective(preparedSnapshot, source, requirementDraft));
|
|
1441
1666
|
}
|
|
1442
1667
|
|
|
1668
|
+
async function resumeInspectedWorkflow(
|
|
1669
|
+
ctx: {
|
|
1670
|
+
cwd: string;
|
|
1671
|
+
ui: {
|
|
1672
|
+
notify(message: string, level: "info" | "error"): void;
|
|
1673
|
+
setStatus(key: string, text: string | undefined): void;
|
|
1674
|
+
setWidget?(key: string, content: string[] | undefined, options?: { placement?: "aboveEditor" | "belowEditor" }): void;
|
|
1675
|
+
};
|
|
1676
|
+
},
|
|
1677
|
+
snapshot: Record<string, unknown>,
|
|
1678
|
+
): Promise<WorkflowResumeDisposition> {
|
|
1679
|
+
const disposition = workflowResumeDisposition(snapshot);
|
|
1680
|
+
const stepId = String((compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step))?.id ?? "当前步骤");
|
|
1681
|
+
if (disposition === "execute") {
|
|
1682
|
+
setControlState(ctx, executingState(`恢复并执行 ${stepId}`));
|
|
1683
|
+
ctx.ui.notify(`Devflow 已恢复到 ${stepId},当前步骤产物尚未完成,正在继续执行。`, "info");
|
|
1684
|
+
await triggerWorkflow(snapshot, "start");
|
|
1685
|
+
return disposition;
|
|
1686
|
+
}
|
|
1687
|
+
if (disposition === "review") {
|
|
1688
|
+
setControlState(ctx, waitingUserState("等待用户审查步骤产物"));
|
|
1689
|
+
ctx.ui.notify(`Devflow 已恢复到 ${stepId};请审查当前产物后明确回复“批准执行”,或直接提出修改意见。`, "info");
|
|
1690
|
+
return disposition;
|
|
1691
|
+
}
|
|
1692
|
+
if (disposition === "blocked") {
|
|
1693
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "恢复校验阻塞" });
|
|
1694
|
+
ctx.ui.notify(`Devflow 无法安全恢复 ${stepId}:${String(snapshot.reason ?? "当前状态既不可执行也不可审查")}。`, "error");
|
|
1695
|
+
return disposition;
|
|
1696
|
+
}
|
|
1697
|
+
setControlState(ctx, initialControlState());
|
|
1698
|
+
ctx.ui.notify(
|
|
1699
|
+
disposition === "complete" ? "Devflow workflow is already complete." : "当前没有可恢复的 Devflow 工作流步骤。",
|
|
1700
|
+
"info",
|
|
1701
|
+
);
|
|
1702
|
+
return disposition;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
async function confirmAndAdvanceWorkflowStep(ctx: {
|
|
1706
|
+
cwd: string;
|
|
1707
|
+
sessionManager: { getSessionId(): string };
|
|
1708
|
+
ui: { confirm(title: string, body: string): Promise<boolean>; notify(message: string, level: "info" | "error"): void; setStatus(key: string, text: string | undefined): void };
|
|
1709
|
+
}): Promise<boolean> {
|
|
1710
|
+
const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
1711
|
+
if (!inspected.result.ok || !inspected.snapshot || !isReviewableWorkflowSnapshot(inspected.snapshot)) return false;
|
|
1712
|
+
const step = compactStep(inspected.snapshot.current_step);
|
|
1713
|
+
const stepId = typeof step?.id === "string" ? step.id : undefined;
|
|
1714
|
+
const revision = typeof inspected.snapshot.revision === "number" ? inspected.snapshot.revision : undefined;
|
|
1715
|
+
if (!stepId || revision === undefined) return false;
|
|
1716
|
+
const checkpoint = inspected.snapshot.status === "waiting_checkpoint";
|
|
1717
|
+
let cp3Prerequisites: Array<{ id?: unknown; kind?: unknown; summary?: unknown }> = [];
|
|
1718
|
+
if (checkpoint && inspected.snapshot.checkpoint === "CP-3") {
|
|
1719
|
+
const feature = typeof inspected.snapshot.current_feature === "string" ? inspected.snapshot.current_feature : undefined;
|
|
1720
|
+
if (!feature) return false;
|
|
1721
|
+
const suite = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/acceptance_test_suite.rb", [
|
|
1722
|
+
"inspect", "--feature", feature, "--json",
|
|
1723
|
+
]);
|
|
1724
|
+
const suitePayload = parseJsonResult(suite);
|
|
1725
|
+
if (!suite.ok || !suitePayload || suitePayload.ok !== true) {
|
|
1726
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "CP-3 前置检查失败" });
|
|
1727
|
+
ctx.ui.notify(`无法读取 CP-3 测试前置事实:${suite.stderr || suite.stdout}`, "error");
|
|
1728
|
+
return true;
|
|
1729
|
+
}
|
|
1730
|
+
cp3Prerequisites = Array.isArray(suitePayload.unconfirmed_prerequisites)
|
|
1731
|
+
? suitePayload.unconfirmed_prerequisites as Array<{ id?: unknown; kind?: unknown; summary?: unknown }>
|
|
1732
|
+
: [];
|
|
1733
|
+
}
|
|
1734
|
+
const prerequisiteReview = cp3Prerequisites.length > 0
|
|
1735
|
+
? [
|
|
1736
|
+
"",
|
|
1737
|
+
`CP-3 检测到 ${cp3Prerequisites.length} 个尚未确认、但被候选验收测试引用的前置事实。批准后 Core 会在同一原子事务中确认它们并冻结测试集:`,
|
|
1738
|
+
...cp3Prerequisites.map(item => `- ${String(item.id ?? "?")} [${String(item.kind ?? "?")}]${item.summary ? `:${String(item.summary)}` : ""}`),
|
|
1739
|
+
].join("\n")
|
|
1740
|
+
: "";
|
|
1741
|
+
const approved = await ctx.ui.confirm(
|
|
1742
|
+
"确认推进 Devflow 工作流",
|
|
1743
|
+
`当前步骤:${stepId}\n\n步骤产物已生成并通过基础校验。确认${checkpoint ? "当前 checkpoint 并" : ""}推进到下一步骤吗?${prerequisiteReview}`,
|
|
1744
|
+
);
|
|
1745
|
+
if (!approved) {
|
|
1746
|
+
setControlState(ctx, waitingUserState("等待用户审查步骤产物并明确决定"));
|
|
1747
|
+
ctx.ui.notify("已保留当前步骤,未推进工作流。", "info");
|
|
1748
|
+
return true;
|
|
1749
|
+
}
|
|
1750
|
+
const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/advance_state.rb", [
|
|
1751
|
+
"--write", "--json", "--expected-revision", String(revision),
|
|
1752
|
+
...(checkpoint ? ["--confirm-checkpoint", "--runtime-confirmed"] : ["--complete-step", stepId]),
|
|
1753
|
+
...(cp3Prerequisites.length > 0 ? ["--confirm-test-prerequisites"] : []),
|
|
1754
|
+
]);
|
|
1755
|
+
if (!result.ok || !controlPlaneSucceeded(result)) {
|
|
1756
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "推进校验失败" });
|
|
1757
|
+
ctx.ui.notify(`未推进:${result.stderr || result.stdout}`, "error");
|
|
1758
|
+
return true;
|
|
1759
|
+
}
|
|
1760
|
+
const after = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
1761
|
+
activeContract = undefined;
|
|
1762
|
+
contractAgentCalls = 0;
|
|
1763
|
+
if (!after.result.ok || !after.snapshot || after.snapshot.action === "workflow_complete") {
|
|
1764
|
+
setControlState(ctx, initialControlState());
|
|
1765
|
+
ctx.ui.notify(after.snapshot?.action === "workflow_complete" ? "工作流已完成。" : "已推进,但读取下一步骤失败。", "info");
|
|
1766
|
+
return true;
|
|
1767
|
+
}
|
|
1768
|
+
setControlState(ctx, waitingUserState("正在准备干净会话以执行下一工作流步骤"));
|
|
1769
|
+
try {
|
|
1770
|
+
await requestControlledRollover(ctx, after.snapshot);
|
|
1771
|
+
ctx.ui.notify("当前节点的证据已固定;当前回合结束后将自动切换到下一节点。", "info");
|
|
1772
|
+
} catch (error) {
|
|
1773
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "会话轮换准备失败" });
|
|
1774
|
+
ctx.ui.notify(`未开始下一节点:会话轮换准备失败:${String(error)}`, "error");
|
|
1775
|
+
}
|
|
1776
|
+
return true;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
async function reconcileParallelLifecycle(event: Partial<SubagentLifecyclePayload>, context: { branch_id: string; attempt: number; branch_revision: number } | undefined): Promise<void> {
|
|
1780
|
+
if (!context || !projectRoot || !event.status || event.status === "started") return;
|
|
1781
|
+
const workflowInspection = await inspectWorkflow(projectRoot);
|
|
1782
|
+
const snapshot = workflowInspection.snapshot;
|
|
1783
|
+
const feature = String(snapshot?.current_feature ?? "");
|
|
1784
|
+
if (!feature) return;
|
|
1785
|
+
const inspected = parseJsonResult(await runRuby(projectRoot, "workflow-orchestrator/scripts/parallel_run.rb", ["--action", "inspect", "--feature", feature, "--json"]));
|
|
1786
|
+
const run = inspected?.run as Record<string, unknown> | undefined;
|
|
1787
|
+
if (!run) return;
|
|
1788
|
+
const stateRevision = Number(snapshot?.revision);
|
|
1789
|
+
if (!Number.isInteger(stateRevision)) return;
|
|
1790
|
+
const action = event.status === "completed" ? "complete" : "fail";
|
|
1791
|
+
const args = ["--write", "--json", "--parallel-action", action, "--parallel-branch-id", context.branch_id, "--expected-revision", String(stateRevision)];
|
|
1792
|
+
if (action === "complete") args.push("--parallel-attempt", String(context.attempt), "--parallel-branch-revision", String(context.branch_revision));
|
|
1793
|
+
const result = await runRuby(projectRoot, "workflow-orchestrator/scripts/advance_state.rb", args);
|
|
1794
|
+
if (!result.ok || !controlPlaneSucceeded(result)) {
|
|
1795
|
+
parallelDispatchFailureThisTurn = result.stderr || result.stdout || `parallel ${action} failed`;
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
if (action !== "complete") return;
|
|
1799
|
+
const refreshed = parseJsonResult(await runRuby(projectRoot, "workflow-orchestrator/scripts/parallel_run.rb", ["--action", "inspect", "--feature", feature, "--json"]));
|
|
1800
|
+
const current = refreshed?.run as Record<string, unknown> | undefined;
|
|
1801
|
+
const branches = Array.isArray(current?.branches) ? current.branches as Array<Record<string, unknown>> : [];
|
|
1802
|
+
if (branches.length > 0 && branches.every(branch => branch.status === "succeeded")) {
|
|
1803
|
+
const after = await inspectWorkflow(projectRoot);
|
|
1804
|
+
const joinRevision = Number(after.snapshot?.revision);
|
|
1805
|
+
if (Number.isInteger(joinRevision)) {
|
|
1806
|
+
await runRuby(projectRoot, "workflow-orchestrator/scripts/advance_state.rb", [
|
|
1807
|
+
"--write", "--json", "--parallel-action", "join", "--expected-revision", String(joinRevision),
|
|
1808
|
+
]);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1443
1813
|
function scheduleParallelContinuation() {
|
|
1444
1814
|
if (parallelContinuationQueued || pendingParallelLifecycle.size === 0) return;
|
|
1445
1815
|
parallelContinuationQueued = true;
|
|
1446
|
-
const events = [...pendingParallelLifecycle.entries()].map(([id, status]) =>
|
|
1816
|
+
const events = [...pendingParallelLifecycle.entries()].map(([id, status]) => {
|
|
1817
|
+
const context = pendingParallelLifecycleContexts.get(id);
|
|
1818
|
+
return context ? `${id}:${status}:${JSON.stringify(context)}` : `${id}:${status}`;
|
|
1819
|
+
});
|
|
1447
1820
|
pendingParallelLifecycle.clear();
|
|
1821
|
+
pendingParallelLifecycleContexts.clear();
|
|
1448
1822
|
pi.sendMessage({
|
|
1449
1823
|
customType: "devflow-parallel-lifecycle",
|
|
1450
1824
|
content: `并行子 Agent 生命周期事件已到达(${events.join(", ")})。请重新 inspect 整个 parallel run,逐一处理所有已完成/失败分支,必要时调用 Core 的 branch complete/fail/retry 动作,然后继续派发仍满足前置依赖的分支;不得直接 join。`,
|
|
@@ -1453,12 +1827,24 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1453
1827
|
}, { triggerTurn: true, deliverAs: "nextTurn" });
|
|
1454
1828
|
}
|
|
1455
1829
|
|
|
1456
|
-
pi.events.on(TASK_SUBAGENT_LIFECYCLE_CHANNEL, data => {
|
|
1830
|
+
pi.events.on(TASK_SUBAGENT_LIFECYCLE_CHANNEL, async data => {
|
|
1457
1831
|
if (!parallelRunActive || !data || typeof data !== "object") return;
|
|
1458
1832
|
const event = data as Partial<SubagentLifecyclePayload>;
|
|
1459
1833
|
if (typeof event.id !== "string" || !["started", "completed", "failed", "aborted"].includes(String(event.status))) return;
|
|
1460
|
-
if (event.status === "started")
|
|
1834
|
+
if (event.status === "started") {
|
|
1835
|
+
parallelDispatchSucceededThisTurn = true;
|
|
1836
|
+
if (typeof event.parentToolCallId === "string") {
|
|
1837
|
+
const context = parallelTaskContexts.get(event.parentToolCallId);
|
|
1838
|
+
if (context) pendingParallelLifecycleContexts.set(event.id, context);
|
|
1839
|
+
}
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1461
1842
|
pendingParallelLifecycle.set(event.id, String(event.status));
|
|
1843
|
+
if (typeof event.parentToolCallId === "string") {
|
|
1844
|
+
const context = parallelTaskContexts.get(event.parentToolCallId);
|
|
1845
|
+
if (context) pendingParallelLifecycleContexts.set(event.id, context);
|
|
1846
|
+
}
|
|
1847
|
+
await reconcileParallelLifecycle(event, pendingParallelLifecycleContexts.get(event.id));
|
|
1462
1848
|
scheduleParallelContinuation();
|
|
1463
1849
|
});
|
|
1464
1850
|
|
|
@@ -1517,8 +1903,11 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1517
1903
|
const consumed = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/session_rollover.rb", [
|
|
1518
1904
|
"consume", "--write", "--json", "--expected-revision", String(rolloverState.revision), "--target-session-id", currentSessionId,
|
|
1519
1905
|
]);
|
|
1906
|
+
const synced = consumed.ok
|
|
1907
|
+
? await runRuby(ctx.cwd, "workflow-orchestrator/scripts/sync_artifacts.rb", ["--write", "--no-preserve", "--json"])
|
|
1908
|
+
: undefined;
|
|
1520
1909
|
const after = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
1521
|
-
if (consumed.ok && after.result.ok && after.snapshot) {
|
|
1910
|
+
if (consumed.ok && synced && controlPlaneSucceeded(synced) && after.result.ok && after.snapshot) {
|
|
1522
1911
|
if (after.snapshot.action === "blocked") {
|
|
1523
1912
|
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "启动恢复校验失败" });
|
|
1524
1913
|
ctx.ui.notify(`会话已恢复,但工作流校验失败,未启动 AI:${String(after.snapshot.reason ?? "state validation failed")}`, "error");
|
|
@@ -1526,10 +1915,23 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1526
1915
|
}
|
|
1527
1916
|
setControlState(ctx, executingState("恢复已认领的干净会话"));
|
|
1528
1917
|
await triggerWorkflow(after.snapshot, "approval");
|
|
1529
|
-
} else if (!consumed.ok) {
|
|
1918
|
+
} else if (!consumed.ok || !synced || !controlPlaneSucceeded(synced)) {
|
|
1530
1919
|
ctx.ui.notify(`已认领的会话交接无法恢复;工作流保持冻结:${consumed.stderr || consumed.stdout}`, "error");
|
|
1531
1920
|
}
|
|
1532
1921
|
}
|
|
1922
|
+
if (cliSessionResumeRequested && controlState.phase === "idle" && !pendingRollover) {
|
|
1923
|
+
const report = await runDoctor(ctx);
|
|
1924
|
+
if (!report.ok) {
|
|
1925
|
+
ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), "error");
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
1929
|
+
if (!inspected.result.ok || !inspected.snapshot) {
|
|
1930
|
+
ctx.ui.notify(`Devflow resume failed: ${inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON"}`, "error");
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
await resumeInspectedWorkflow(ctx, inspected.snapshot);
|
|
1934
|
+
}
|
|
1533
1935
|
});
|
|
1534
1936
|
|
|
1535
1937
|
pi.on("session_before_switch", async (event) => {
|
|
@@ -1558,13 +1960,16 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1558
1960
|
const consumed = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/session_rollover.rb", [
|
|
1559
1961
|
"consume", "--write", "--json", "--expected-revision", String(claimedPayload.revision), "--target-session-id", targetSessionId,
|
|
1560
1962
|
]);
|
|
1561
|
-
|
|
1963
|
+
const synced = consumed.ok
|
|
1964
|
+
? await runRuby(ctx.cwd, "workflow-orchestrator/scripts/sync_artifacts.rb", ["--write", "--no-preserve", "--json"])
|
|
1965
|
+
: undefined;
|
|
1966
|
+
if (!consumed.ok || !synced || !controlPlaneSucceeded(synced)) {
|
|
1562
1967
|
if (parseJsonResult(consumed)?.reason === "rollover_transition_failed") {
|
|
1563
1968
|
await enqueueControlledRolloverRequest(ctx.cwd, pending.rolloverId, targetSessionId);
|
|
1564
1969
|
} else {
|
|
1565
1970
|
pendingRollover = undefined;
|
|
1566
1971
|
}
|
|
1567
|
-
ctx.ui.notify(`Devflow
|
|
1972
|
+
ctx.ui.notify(`Devflow 会话交接消费或 registry 同步失败;工作流保持冻结:${consumed.stderr || consumed.stdout || synced?.stderr || synced?.stdout}`, "error");
|
|
1568
1973
|
return;
|
|
1569
1974
|
}
|
|
1570
1975
|
pendingRollover = undefined;
|
|
@@ -1593,6 +1998,16 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1593
1998
|
activeWorkflowTurn = false;
|
|
1594
1999
|
if (parallelRunActive) {
|
|
1595
2000
|
parallelContinuationQueued = false;
|
|
2001
|
+
if (!parallelDispatchSucceededThisTurn && pendingParallelLifecycle.size === 0) {
|
|
2002
|
+
parallelRunActive = false;
|
|
2003
|
+
parallelTaskContexts.clear();
|
|
2004
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "并行子 Agent 未启动" });
|
|
2005
|
+
ctx.ui.notify(
|
|
2006
|
+
`并行窗口未启动任何子 Agent,已停止等待不存在的生命周期事件;STATE 未推进。${parallelDispatchFailureThisTurn ? `\n原因:${parallelDispatchFailureThisTurn}` : ""}\n请检查打包 Agent 注册;若当前环境不支持并行,调度器必须按 serial_fallback_order 串行执行。`,
|
|
2007
|
+
"error",
|
|
2008
|
+
);
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
1596
2011
|
setControlState(ctx, executingState("并行窗口运行中,等待子 Agent 生命周期事件"));
|
|
1597
2012
|
// A second detached task may finish while the continuation is running.
|
|
1598
2013
|
// Its event is retained and gets a follow-up continuation instead of
|
|
@@ -1602,7 +2017,13 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1602
2017
|
}
|
|
1603
2018
|
if (controlState.phase === "executing") {
|
|
1604
2019
|
setControlState(ctx, waitingUserState("等待用户审查步骤产物并明确决定"));
|
|
1605
|
-
|
|
2020
|
+
// UI confirmation may legitimately wait for the user longer than the
|
|
2021
|
+
// host's event-handler deadline. Do not await it from agent_end or the
|
|
2022
|
+
// host reports a timeout and the approval result is lost.
|
|
2023
|
+
void confirmAndAdvanceWorkflowStep(ctx).catch(error => {
|
|
2024
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "推进确认处理失败" });
|
|
2025
|
+
ctx.ui.notify(`推进确认处理失败:${String(error)}`, "error");
|
|
2026
|
+
});
|
|
1606
2027
|
}
|
|
1607
2028
|
});
|
|
1608
2029
|
|
|
@@ -1660,7 +2081,22 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1660
2081
|
}
|
|
1661
2082
|
return { handled: true };
|
|
1662
2083
|
}
|
|
1663
|
-
|
|
2084
|
+
// The in-memory prompt state is intentionally not authoritative. A fresh
|
|
2085
|
+
// session starts idle, so recover the approval arm from the durable Core
|
|
2086
|
+
// snapshot before handing an explicit approval phrase to the model. This
|
|
2087
|
+
// still requires the user's phrase and never auto-advances the workflow.
|
|
2088
|
+
let recoveredApprovalState = false;
|
|
2089
|
+
if (
|
|
2090
|
+
controlAction &&
|
|
2091
|
+
controlState.phase === "idle"
|
|
2092
|
+
) {
|
|
2093
|
+
const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
2094
|
+
if (inspected.result.ok && inspected.snapshot && isReviewableWorkflowSnapshot(inspected.snapshot)) {
|
|
2095
|
+
setControlState(ctx, waitingUserState("恢复当前步骤审批状态"));
|
|
2096
|
+
recoveredApprovalState = true;
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
if (recoveredApprovalState || controlState.phase === "awaiting-approval" || controlState.phase === "waiting-user") {
|
|
1664
2100
|
if (controlAction === "reject") {
|
|
1665
2101
|
ctx.ui.notify("已保留当前步骤和产物。请直接说明需要修改的内容;未执行任何推进。", "info");
|
|
1666
2102
|
return { handled: true };
|
|
@@ -1679,6 +2115,17 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1679
2115
|
ctx.ui.notify("当前状态缺少可确认的 revision 或步骤,拒绝推进。", "error");
|
|
1680
2116
|
return { handled: true };
|
|
1681
2117
|
}
|
|
2118
|
+
if (checkpoint && stepId === "wireframe-review" && typeof inspected.snapshot.current_feature === "string") {
|
|
2119
|
+
const approval = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/wireframe_commit.rb", [
|
|
2120
|
+
"--action", "approve", "--feature", inspected.snapshot.current_feature,
|
|
2121
|
+
"--expected-revision", String(revision), "--all-screens", "--runtime-confirmed", "--write", "--json",
|
|
2122
|
+
]);
|
|
2123
|
+
if (!controlPlaneSucceeded(approval)) {
|
|
2124
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "原型确认写入失败" });
|
|
2125
|
+
ctx.ui.notify(`原型确认未写入:${approval.stderr || approval.stdout}`, "error");
|
|
2126
|
+
return { handled: true };
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
1682
2129
|
const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/advance_state.rb", [
|
|
1683
2130
|
"--write",
|
|
1684
2131
|
"--json",
|
|
@@ -1760,11 +2207,34 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1760
2207
|
|
|
1761
2208
|
pi.on("tool_call", async (event, ctx) => {
|
|
1762
2209
|
const toolName = String(event.toolName).toLowerCase();
|
|
2210
|
+
const toolInput = event.input && typeof event.input === "object" ? event.input as Record<string, unknown> : undefined;
|
|
2211
|
+
const toolCallId = String(event.toolCallId ?? "");
|
|
2212
|
+
const internalControlResource = internalDevflowControlResource(toolName, event.input);
|
|
2213
|
+
if (isVirtualStateTransitionResource(toolName, event.input)) {
|
|
2214
|
+
return {
|
|
2215
|
+
block: true,
|
|
2216
|
+
reason: "禁止通过 write xd://devflow_transition 推进工作流;步骤产物完成后由 Runtime 弹出用户确认并执行状态迁移。",
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
let bootstrapAuthorized = toolName === "devflow_bootstrap" && runtimeBootstrapAuthorizations.has(toolCallId);
|
|
2220
|
+
if (toolName === "devflow_bootstrap" && !bootstrapAuthorized) {
|
|
2221
|
+
const feature = typeof toolInput?.feature === "string" ? toolInput.feature.trim() : "";
|
|
2222
|
+
const phase = typeof toolInput?.phase === "string" && toolInput.phase.trim() ? toolInput.phase.trim() : "默认需求阶段";
|
|
2223
|
+
if (!feature) return { block: true, reason: "bootstrap requires a feature name" };
|
|
2224
|
+
const confirmed = await ctx.ui.confirm(
|
|
2225
|
+
"确认启动新的 Devflow Feature",
|
|
2226
|
+
`Feature:${feature}\n阶段:${phase}\n\n确认后由 Core 原子初始化并切换当前 Feature;不会由 AI 直接修改 STATE.md。若当前仍有未完成 Feature,Core 会拒绝切换并保持原状态。继续吗?`,
|
|
2227
|
+
);
|
|
2228
|
+
if (!confirmed) return { block: true, reason: "User declined runtime bootstrap authorization." };
|
|
2229
|
+
if (!toolCallId) return { block: true, reason: "bootstrap runtime authorization is missing a tool-call binding" };
|
|
2230
|
+
runtimeBootstrapAuthorizations.add(toolCallId);
|
|
2231
|
+
bootstrapAuthorized = true;
|
|
2232
|
+
}
|
|
1763
2233
|
if (parallelRunActive && toolName === "task") {
|
|
1764
2234
|
const branchContext = parallelBranchContext(event.input);
|
|
1765
2235
|
if (branchContext) parallelTaskContexts.set(event.toolCallId, branchContext);
|
|
1766
2236
|
}
|
|
1767
|
-
if (activeContract && controlState.phase === "executing" && !toolName.startsWith("devflow_")) {
|
|
2237
|
+
if (activeContract && controlState.phase === "executing" && !toolName.startsWith("devflow_") && !internalControlResource) {
|
|
1768
2238
|
if (!activeContract.allowedTools.includes(toolName)) {
|
|
1769
2239
|
return { block: true, reason: `Active contract ${activeContract.contractId} does not allow tool: ${toolName}` };
|
|
1770
2240
|
}
|
|
@@ -1811,15 +2281,15 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1811
2281
|
} else if (
|
|
1812
2282
|
controlState.phase === "planning" &&
|
|
1813
2283
|
toolName !== "devflow_contract" &&
|
|
1814
|
-
!
|
|
2284
|
+
!internalControlResource &&
|
|
1815
2285
|
!(toolName === "devflow_evidence" && String((event.input as Record<string, unknown> | undefined)?.action) !== "create") &&
|
|
2286
|
+
!bootstrapAuthorized &&
|
|
1816
2287
|
!(controlState.intent === "requirement-draft"
|
|
1817
2288
|
? isRequirementDraftToolAllowed(toolName, event.input)
|
|
1818
2289
|
: isPlanningToolAllowed(toolName))
|
|
1819
2290
|
) {
|
|
1820
2291
|
return { block: true, reason: planningToolBlockReason(toolName) };
|
|
1821
2292
|
}
|
|
1822
|
-
const toolInput = event.input && typeof event.input === "object" ? event.input as Record<string, unknown> : undefined;
|
|
1823
2293
|
// Registry reconciliation is intentionally the one write operation an
|
|
1824
2294
|
// agent may request while a step is blocked: it changes only derived
|
|
1825
2295
|
// artifact metadata and never advances STATE, checkpoints, or ownership.
|
|
@@ -1829,7 +2299,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1829
2299
|
toolName === "devflow_transition" &&
|
|
1830
2300
|
toolInput?.action === "complete-step" &&
|
|
1831
2301
|
toolInput?.confirm === true;
|
|
1832
|
-
if (isStateMutationTool(toolName) && !isRegistryReconciliation && !isUserConfirmedStepTransition) {
|
|
2302
|
+
if (isStateMutationTool(toolName) && !isRegistryReconciliation && !isUserConfirmedStepTransition && !bootstrapAuthorized) {
|
|
1833
2303
|
return {
|
|
1834
2304
|
block: true,
|
|
1835
2305
|
reason:
|
|
@@ -1848,6 +2318,18 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1848
2318
|
};
|
|
1849
2319
|
}
|
|
1850
2320
|
}
|
|
2321
|
+
if (activeWorkflowTurn && !parallelRunActive && stepScopedMutationOwner(toolName, event.input) !== undefined) {
|
|
2322
|
+
const inspected = await inspectWorkflow(ctx.cwd);
|
|
2323
|
+
if (!inspected.result.ok || !inspected.snapshot) {
|
|
2324
|
+
return {
|
|
2325
|
+
block: true,
|
|
2326
|
+
reason: `Active Devflow workflow mutation blocked because authoritative state could not be inspected: ${inspected.result.stderr || inspected.result.stdout}`,
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
const fresh = activeContract ? await contractIsFresh(ctx.cwd, activeContract) : false;
|
|
2330
|
+
const reason = stepScopedMutationBlockReason(toolName, event.input, ctx.cwd, inspected.snapshot, activeContract, fresh);
|
|
2331
|
+
if (reason) return { block: true, reason };
|
|
2332
|
+
}
|
|
1851
2333
|
if (
|
|
1852
2334
|
!toolName.startsWith("devflow_") &&
|
|
1853
2335
|
["write", "edit", "apply_patch", "notebook_edit", "bash", "shell", "exec_command"].includes(toolName) &&
|
|
@@ -1858,10 +2340,30 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1858
2340
|
reason: "规范需求图谱、派生视图、机器 Evidence 与合同审计只能由 Core writer 写入;Skill 只能提交候选 delta 或 agent_claim。",
|
|
1859
2341
|
};
|
|
1860
2342
|
}
|
|
2343
|
+
if (
|
|
2344
|
+
!toolName.startsWith("devflow_") &&
|
|
2345
|
+
["write", "edit", "apply_patch", "notebook_edit"].includes(toolName)
|
|
2346
|
+
) {
|
|
2347
|
+
const scopedFeatures = inputPaths(event.input, ctx.cwd)
|
|
2348
|
+
.map(planningFeatureFromPath)
|
|
2349
|
+
.filter((value): value is string => value !== null);
|
|
2350
|
+
if (scopedFeatures.length > 0) {
|
|
2351
|
+
const state = parseJsonResult(await runRuby(ctx.cwd, "workflow-orchestrator/scripts/inspect_state.rb", ["--json"]));
|
|
2352
|
+
const activeFeature = typeof state?.current_feature === "string" ? state.current_feature : undefined;
|
|
2353
|
+
const mismatched = scopedFeatures.find(feature => activeFeature && feature !== activeFeature);
|
|
2354
|
+
if (mismatched) {
|
|
2355
|
+
return {
|
|
2356
|
+
block: true,
|
|
2357
|
+
reason: `feature is not active: requested=${mismatched}, current=${activeFeature}; bootstrap the feature before writing feature-scoped artifacts`,
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
1861
2362
|
if (toolName.startsWith("devflow_")) return;
|
|
1862
2363
|
if (
|
|
1863
2364
|
activeContract &&
|
|
1864
2365
|
controlState.phase === "executing" &&
|
|
2366
|
+
!internalControlResource &&
|
|
1865
2367
|
["write", "edit", "apply_patch", "notebook_edit"].includes(toolName)
|
|
1866
2368
|
) {
|
|
1867
2369
|
const paths = inputPaths(event.input, ctx.cwd);
|
|
@@ -1923,15 +2425,31 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1923
2425
|
});
|
|
1924
2426
|
|
|
1925
2427
|
pi.on("tool_result", async (event, ctx) => {
|
|
2428
|
+
const taskContext = parallelTaskContexts.get(event.toolCallId);
|
|
2429
|
+
if (parallelRunActive && event.toolName === "task") {
|
|
2430
|
+
if (taskResultHasSuccessfulDispatch(event.details)) {
|
|
2431
|
+
parallelDispatchSucceededThisTurn = true;
|
|
2432
|
+
} else {
|
|
2433
|
+
parallelDispatchFailureThisTurn = event.content
|
|
2434
|
+
.filter(part => part.type === "text")
|
|
2435
|
+
.map(part => part.text)
|
|
2436
|
+
.join("\n")
|
|
2437
|
+
.slice(0, 1000);
|
|
2438
|
+
}
|
|
2439
|
+
// Detached task lifecycle events arrive after tool_result and carry the
|
|
2440
|
+
// parent tool-call id. Keep the correlation until that terminal event.
|
|
2441
|
+
if (taskContext && !parallelRunActive) parallelTaskContexts.delete(event.toolCallId);
|
|
2442
|
+
}
|
|
1926
2443
|
if (event.isError) return;
|
|
1927
2444
|
if (activeWorkflowTurn && activeContract) {
|
|
1928
|
-
const
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
2445
|
+
const taskWasDispatched = event.toolName !== "task" || taskResultHasSuccessfulDispatch(event.details);
|
|
2446
|
+
if (taskWasDispatched) {
|
|
2447
|
+
await appendEngineEvidence(ctx.cwd, "engine_dispatched", {
|
|
2448
|
+
engine: event.toolName,
|
|
2449
|
+
ai_capable: event.toolName === "task" || event.toolName.toLowerCase().includes("ai") || event.toolName.toLowerCase().includes("midscene"),
|
|
2450
|
+
...(taskContext ?? {}),
|
|
2451
|
+
});
|
|
2452
|
+
}
|
|
1935
2453
|
}
|
|
1936
2454
|
await runHook(ctx.cwd, "post_tool.py", {
|
|
1937
2455
|
session_id: sessionId,
|
|
@@ -1986,6 +2504,26 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1986
2504
|
},
|
|
1987
2505
|
});
|
|
1988
2506
|
|
|
2507
|
+
pi.registerTool({
|
|
2508
|
+
name: "devflow_wireframe",
|
|
2509
|
+
label: "Devflow Wireframe",
|
|
2510
|
+
description: "Build Core-owned wireframe metadata from the current canonical proposal. Approval is only available through the user-confirmed checkpoint runtime path.",
|
|
2511
|
+
parameters: z.object({
|
|
2512
|
+
action: z.literal("meta"),
|
|
2513
|
+
feature: z.string().min(1),
|
|
2514
|
+
expectedRevision: z.number().int().min(0),
|
|
2515
|
+
}),
|
|
2516
|
+
approval: "write",
|
|
2517
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
2518
|
+
return renderResult(
|
|
2519
|
+
await runRuby(ctx.cwd, "workflow-orchestrator/scripts/wireframe_commit.rb", [
|
|
2520
|
+
"--action", "meta", "--feature", params.feature, "--expected-revision", String(params.expectedRevision), "--write", "--json",
|
|
2521
|
+
]),
|
|
2522
|
+
{ action: params.action, feature: params.feature },
|
|
2523
|
+
);
|
|
2524
|
+
},
|
|
2525
|
+
});
|
|
2526
|
+
|
|
1989
2527
|
pi.registerTool({
|
|
1990
2528
|
|
|
1991
2529
|
name: "devflow_requirement_graph",
|
|
@@ -2014,6 +2552,19 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2014
2552
|
}),
|
|
2015
2553
|
approval: (args) => args.action === "propose" || args.action === "confirm" ? "write" : "read",
|
|
2016
2554
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
2555
|
+
const graphWrite = params.action === "propose" || params.action === "confirm" || (params.action === "context" && params.write === true);
|
|
2556
|
+
if (graphWrite) {
|
|
2557
|
+
const state = parseJsonResult(await runRuby(ctx.cwd, "workflow-orchestrator/scripts/inspect_state.rb", ["--json"]));
|
|
2558
|
+
const currentFeature = typeof state?.current_feature === "string" ? state.current_feature : undefined;
|
|
2559
|
+
if (currentFeature && currentFeature !== params.feature) {
|
|
2560
|
+
return renderResult({
|
|
2561
|
+
ok: false,
|
|
2562
|
+
exitCode: 1,
|
|
2563
|
+
stdout: "",
|
|
2564
|
+
stderr: `feature is not active: requested=${params.feature}, current=${currentFeature}; bootstrap the feature before writing its requirement graph`,
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2017
2568
|
const args = [params.action, "--feature", params.feature, "--json"];
|
|
2018
2569
|
if (params.action === "context") {
|
|
2019
2570
|
if (!params.sliceId) return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "context requires sliceId" });
|
|
@@ -2513,8 +3064,8 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2513
3064
|
const contract = contractFromRecord(raw as Record<string, unknown>);
|
|
2514
3065
|
if (!contract) return renderResult({ ok: false, exitCode: 1, stdout: "", stderr: "Core returned an invalid execution contract." });
|
|
2515
3066
|
if (input.action === "inspect") return { content: [{ type: "text" as const, text: JSON.stringify(contract, null, 2) }], details: { contract } };
|
|
2516
|
-
if (!
|
|
2517
|
-
return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "Contracts may only be activated from
|
|
3067
|
+
if (!contractActivationPhaseAllowed(controlState.phase, activeWorkflowTurn)) {
|
|
3068
|
+
return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "Contracts may only be activated from planning/review, or for the exact current step during a runtime-triggered workflow turn." });
|
|
2518
3069
|
}
|
|
2519
3070
|
const confirmed = await ctx.ui.confirm(
|
|
2520
3071
|
"激活 Devflow 执行合同",
|
|
@@ -2603,6 +3154,9 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2603
3154
|
}),
|
|
2604
3155
|
approval: "write",
|
|
2605
3156
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
3157
|
+
// Consume the one-shot runtime authorization before running Core's
|
|
3158
|
+
// atomic bootstrap sequence. A retry must obtain a fresh user approval.
|
|
3159
|
+
runtimeBootstrapAuthorizations.delete(String(_id));
|
|
2606
3160
|
const phase = params.phase?.trim() || defaultPhaseId(params.feature);
|
|
2607
3161
|
if (!validPhaseId(phase)) {
|
|
2608
3162
|
return {
|
|
@@ -2645,10 +3199,28 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2645
3199
|
"--json",
|
|
2646
3200
|
]);
|
|
2647
3201
|
if (!controlPlaneSucceeded(artifacts)) return renderResult(artifacts, { stage: "artifacts" });
|
|
3202
|
+
const inspected = await inspectWorkflow(ctx.cwd);
|
|
3203
|
+
if (!inspected.result.ok || !inspected.snapshot) {
|
|
3204
|
+
return renderResult(
|
|
3205
|
+
{ ok: false, exitCode: 1, stdout: "", stderr: "bootstrap completed but the resulting workflow state could not be inspected" },
|
|
3206
|
+
{ stage: "inspect-after-bootstrap" },
|
|
3207
|
+
);
|
|
3208
|
+
}
|
|
3209
|
+
try {
|
|
3210
|
+
setControlState(ctx, waitingUserState("准备进入需求入口分诊"));
|
|
3211
|
+
await requestControlledRollover(ctx, inspected.snapshot);
|
|
3212
|
+
} catch (error) {
|
|
3213
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "会话轮换准备失败" });
|
|
3214
|
+
return renderResult(
|
|
3215
|
+
{ ok: false, exitCode: 1, stdout: "", stderr: `bootstrap succeeded but session handoff could not be prepared: ${String(error)}` },
|
|
3216
|
+
{ stage: "rollover" },
|
|
3217
|
+
);
|
|
3218
|
+
}
|
|
2648
3219
|
return renderResult(artifacts, {
|
|
2649
3220
|
stage: "complete",
|
|
2650
3221
|
workflow: workflow.stdout,
|
|
2651
3222
|
state: state.stdout,
|
|
3223
|
+
rollover: "prepared",
|
|
2652
3224
|
});
|
|
2653
3225
|
},
|
|
2654
3226
|
});
|
|
@@ -2963,10 +3535,6 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2963
3535
|
);
|
|
2964
3536
|
return;
|
|
2965
3537
|
}
|
|
2966
|
-
if (inspected.snapshot.action === "workflow_complete") {
|
|
2967
|
-
ctx.ui.notify("Devflow workflow is already complete.", "info");
|
|
2968
|
-
return;
|
|
2969
|
-
}
|
|
2970
3538
|
// Re-arm a durable rollover left by an interrupted runtime. An
|
|
2971
3539
|
// explicit resume is the user's authorization for a fresh session to
|
|
2972
3540
|
// adopt the same project's integrity-checked prepared handoff.
|
|
@@ -2986,12 +3554,11 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2986
3554
|
rolloverId: rollover.id,
|
|
2987
3555
|
};
|
|
2988
3556
|
await enqueueControlledRolloverRequest(ctx.cwd, rollover.id, currentSessionId);
|
|
3557
|
+
setControlState(ctx, waitingUserState("正在恢复受控会话交接"));
|
|
3558
|
+
ctx.ui.notify("Devflow 已恢复待完成的受控会话交接;当前回合结束后将继续切换。", "info");
|
|
3559
|
+
return;
|
|
2989
3560
|
}
|
|
2990
|
-
|
|
2991
|
-
ctx.ui.notify(
|
|
2992
|
-
`Devflow 已恢复到 ${String((compactStep(inspected.snapshot.next_step) ?? compactStep(inspected.snapshot.current_step))?.id ?? "当前步骤")};请审查当前产物后明确回复“批准执行”,或直接提出修改意见。`,
|
|
2993
|
-
"info",
|
|
2994
|
-
);
|
|
3561
|
+
await resumeInspectedWorkflow(ctx, inspected.snapshot);
|
|
2995
3562
|
return;
|
|
2996
3563
|
}
|
|
2997
3564
|
if (trimmed.startsWith("start ")) {
|