@guanlin4924/devflow-pi 0.1.106 → 0.1.107
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 +184 -91
- package/plugins/devflow-core/skills/workflow-dynamic/SKILL.md +41 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/SKILL.md +3 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +6 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dynamic_plan.rb +504 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +6 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/sync_artifacts.rb +4 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +9 -0
- package/plugins/devflow-core/skills/workflow-pipeline/SKILL.md +3 -0
- package/plugins/devflow-implementation/skills/workflow-bug-fix/SKILL.md +3 -0
- package/plugins/devflow-implementation/skills/workflow-execute-phase/SKILL.md +3 -0
- package/plugins/devflow-quality/agents/code-reviewer.md +2 -0
- package/plugins/devflow-quality/skills/code-review/SKILL.md +3 -0
- package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/SKILL.md +3 -0
- package/plugins/devflow-requirements/skills/workflow-requirement-triage/SKILL.md +3 -0
- package/src/constants.ts +1 -1
- package/src/dynamic-workflow.ts +31 -0
- package/templates/omp/AGENTS.md +4 -3
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -19,6 +19,7 @@ import { generateProjectDocuments } from "../../../src/project-documents.ts";
|
|
|
19
19
|
import { preparedRolloverRevision } from "../../../src/session-rollover.ts";
|
|
20
20
|
import { describeStartupIssue, renderStartupBriefing } from "../../../src/startup-briefing.ts";
|
|
21
21
|
import { PRODUCT_VERSION } from "../../../src/constants.ts";
|
|
22
|
+
import { dynamicWorkflowDirective, renderDynamicWorkflow } from "../../../src/dynamic-workflow.ts";
|
|
22
23
|
import {
|
|
23
24
|
FIX_LABELS,
|
|
24
25
|
buildHelpBundle,
|
|
@@ -819,6 +820,7 @@ const REQUIRED_EXTENSION_TOOLS = [
|
|
|
819
820
|
];
|
|
820
821
|
|
|
821
822
|
const REQUIRED_SKILLS = [
|
|
823
|
+
"workflow-dynamic",
|
|
822
824
|
"code-index-query",
|
|
823
825
|
"code-review",
|
|
824
826
|
"requirement-feasibility-review",
|
|
@@ -876,6 +878,7 @@ const REQUIRED_MODEL_ROLES = ["@smol", "@task", "@slow"];
|
|
|
876
878
|
const REQUIRED_AGENT_NAMES = new Set(Object.keys(REQUIRED_AGENTS));
|
|
877
879
|
|
|
878
880
|
const REQUIRED_SCRIPTS = [
|
|
881
|
+
"workflow-orchestrator/scripts/dynamic_plan.rb",
|
|
879
882
|
"workflow-orchestrator/scripts/init_workflow.rb",
|
|
880
883
|
"workflow-orchestrator/scripts/init_state.rb",
|
|
881
884
|
"workflow-orchestrator/scripts/phase_ownership.rb",
|
|
@@ -1535,6 +1538,7 @@ export function isShellControlMutation(toolName: string, input: unknown): boolea
|
|
|
1535
1538
|
}
|
|
1536
1539
|
const command = String((input as Record<string, unknown>).command ?? (input as Record<string, unknown>).cmd ?? "");
|
|
1537
1540
|
if (!command) return false;
|
|
1541
|
+
if (/\bdynamic_plan\.rb\b/.test(command)) return true;
|
|
1538
1542
|
const invokesWriter =
|
|
1539
1543
|
/workflow-orchestrator\/scripts\/(?:advance_state|init_state|init_workflow|sync_artifacts|requirement_graph|machine_evidence|execution_contract)\.rb[^\n]*--write/.test(
|
|
1540
1544
|
command,
|
|
@@ -1997,6 +2001,10 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1997
2001
|
const reviewBindingKey = randomBytes(32).toString("hex");
|
|
1998
2002
|
const pendingReviewTasks = new Map<string, { feature: string; contractId: string }>();
|
|
1999
2003
|
const completedReviewTasks = new Map<string, { feature: string; contractId: string; report: Record<string, unknown> }>();
|
|
2004
|
+
const dynamicReviewBindings = new Map<string, { taskId: string; attempt: string; revision: number }>();
|
|
2005
|
+
const dynamicReviewResults = new Map<string, { taskId: string; attempt: string; revision: number; report: Record<string, unknown> }>();
|
|
2006
|
+
let dynamicTurnRevision: number | undefined;
|
|
2007
|
+
let dynamicTurnProgress: string | undefined;
|
|
2000
2008
|
let latestReviewTaskCallId: string | undefined;
|
|
2001
2009
|
let projectRoot: string | undefined;
|
|
2002
2010
|
|
|
@@ -2109,6 +2117,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2109
2117
|
const content = lines.map((line, index) => {
|
|
2110
2118
|
if (index === 0) return theme.bold(theme.fg("accent", line));
|
|
2111
2119
|
if (line.startsWith("──")) return theme.fg("accent", line);
|
|
2120
|
+
if (line.startsWith("—")) return theme.fg("dim", line);
|
|
2112
2121
|
return theme.fg("muted", line);
|
|
2113
2122
|
}).join("\n");
|
|
2114
2123
|
panel.addChild(new Text(content, 0, 0));
|
|
@@ -2660,6 +2669,22 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2660
2669
|
return { result, snapshot: parseJsonResult(result) };
|
|
2661
2670
|
}
|
|
2662
2671
|
|
|
2672
|
+
async function runDynamicPlan(cwd: string, request: Record<string, unknown>): Promise<ProcessResult> {
|
|
2673
|
+
assertRuntimeProjectBoundary(cwd);
|
|
2674
|
+
return runProcess(["ruby", path.join(resolveKitRoot(cwd), "workflow-orchestrator/scripts/dynamic_plan.rb")], cwd, {}, JSON.stringify(request));
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
async function startDynamicTurn(ctx: { ui: { notify(message: string, level: "info" | "error"): void } }, snapshot: Record<string, unknown>) {
|
|
2678
|
+
ctx.ui.notify(renderDynamicWorkflow(snapshot), "info");
|
|
2679
|
+
if (["ready", "cancelled"].includes(String(snapshot.delivery))) return;
|
|
2680
|
+
dynamicTurnRevision = Number(snapshot.revision);
|
|
2681
|
+
dynamicTurnProgress = String(snapshot.progress_key);
|
|
2682
|
+
activeContract = undefined;
|
|
2683
|
+
activeWorkflowTurn = true;
|
|
2684
|
+
controlState = executingState("执行中文工作流");
|
|
2685
|
+
await pi.sendUserMessage(dynamicWorkflowDirective(snapshot));
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2663
2688
|
async function inspectWorkflowWithRegistryRecovery(cwd: string): Promise<{ result: ProcessResult; snapshot: Record<string, unknown> | null }> {
|
|
2664
2689
|
let inspected = await inspectWorkflow(cwd);
|
|
2665
2690
|
const errors = Array.isArray(inspected.snapshot?.state_validation && (inspected.snapshot.state_validation as Record<string, unknown>).errors)
|
|
@@ -2819,6 +2844,10 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2819
2844
|
snapshot: Record<string, unknown>,
|
|
2820
2845
|
options: { doctorAlreadyPassed: boolean },
|
|
2821
2846
|
): Promise<WorkflowResumeDisposition> {
|
|
2847
|
+
if (snapshot.mode === "dynamic") {
|
|
2848
|
+
await startDynamicTurn(ctx, snapshot);
|
|
2849
|
+
return snapshot.delivery === "ready" ? "complete" : "execute";
|
|
2850
|
+
}
|
|
2822
2851
|
const disposition = workflowResumeDisposition(snapshot);
|
|
2823
2852
|
const stepId = String(selectedWorkflowStep(snapshot)?.id ?? "当前步骤");
|
|
2824
2853
|
if (disposition === "execute") {
|
|
@@ -3000,6 +3029,11 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3000
3029
|
ctx.ui.notify("当前步骤正在执行,等这一轮结束后再说「下一步」。", "info");
|
|
3001
3030
|
return;
|
|
3002
3031
|
}
|
|
3032
|
+
const dynamic = parseJsonResult(await runDynamicPlan(ctx.cwd, { action: "inspect" }));
|
|
3033
|
+
if (dynamic?.mode === "dynamic") {
|
|
3034
|
+
await startDynamicTurn(ctx, dynamic);
|
|
3035
|
+
return;
|
|
3036
|
+
}
|
|
3003
3037
|
if (mode === "next") {
|
|
3004
3038
|
const report = await runDoctor(ctx);
|
|
3005
3039
|
if (!report.ok) {
|
|
@@ -3704,6 +3738,25 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3704
3738
|
|
|
3705
3739
|
pi.on("agent_end", async (event, ctx) => {
|
|
3706
3740
|
if (!activeWorkflowTurn || event.willContinue) return;
|
|
3741
|
+
if (dynamicTurnRevision !== undefined) {
|
|
3742
|
+
const before = dynamicTurnRevision;
|
|
3743
|
+
dynamicTurnRevision = undefined;
|
|
3744
|
+
activeWorkflowTurn = false;
|
|
3745
|
+
const snapshot = parseJsonResult(await runDynamicPlan(ctx.cwd, { action: "inspect" }));
|
|
3746
|
+
if (snapshot?.mode === "dynamic") {
|
|
3747
|
+
ctx.ui.notify(renderDynamicWorkflow(snapshot), "info");
|
|
3748
|
+
if (["run_task", "plan_work", "reconcile_plan"].includes(String(snapshot.action)) && Number(snapshot.revision) > before && snapshot.progress_key !== dynamicTurnProgress) {
|
|
3749
|
+
dynamicTurnRevision = Number(snapshot.revision);
|
|
3750
|
+
dynamicTurnProgress = String(snapshot.progress_key);
|
|
3751
|
+
activeWorkflowTurn = true;
|
|
3752
|
+
setControlState(ctx, executingState("继续执行中文工作流"));
|
|
3753
|
+
pi.sendMessage({ customType: "devflow-dynamic-continue", content: dynamicWorkflowDirective(snapshot), display: false }, { triggerTurn: true, deliverAs: "nextTurn" });
|
|
3754
|
+
} else {
|
|
3755
|
+
setControlState(ctx, snapshot.delivery === "ready" ? initialControlState() : waitingUserState("保留当前结果;等待决定、外部条件或执行恢复"));
|
|
3756
|
+
}
|
|
3757
|
+
return;
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3707
3760
|
const assistant = [...event.messages].reverse().find(message => (
|
|
3708
3761
|
message && typeof message === "object" && (message as { role?: unknown }).role === "assistant"
|
|
3709
3762
|
));
|
|
@@ -3750,6 +3803,17 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3750
3803
|
|
|
3751
3804
|
pi.on("input", async (event, ctx) => {
|
|
3752
3805
|
if (event.source !== "interactive") return;
|
|
3806
|
+
if (event.text.trim() && !event.text.trim().startsWith("/") && !classifyNextStepPhrase(event.text)) {
|
|
3807
|
+
const dynamic = parseJsonResult(await runDynamicPlan(ctx.cwd, { action: "inspect" }));
|
|
3808
|
+
if (dynamic?.mode === "dynamic") {
|
|
3809
|
+
dynamicTurnRevision = Number(dynamic.revision);
|
|
3810
|
+
dynamicTurnProgress = String(dynamic.progress_key);
|
|
3811
|
+
activeWorkflowTurn = true;
|
|
3812
|
+
activeContract = undefined;
|
|
3813
|
+
setControlState(ctx, executingState("处理需求调整"));
|
|
3814
|
+
return { text: `${dynamicWorkflowDirective(dynamic)}\n用户补充:${event.text}`, handled: false };
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3753
3817
|
const controlAction = classifyNaturalControlAction(event.text);
|
|
3754
3818
|
const stepNote = event.text.trim();
|
|
3755
3819
|
if (controlState.intent === "workflow-execute" && !controlAction && stepNote && !stepNote.startsWith("/") && !classifyNextStepPhrase(stepNote)) {
|
|
@@ -3943,6 +4007,18 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3943
4007
|
});
|
|
3944
4008
|
|
|
3945
4009
|
pi.on("tool_call", async (event, ctx) => {
|
|
4010
|
+
if (dynamicTurnRevision !== undefined) {
|
|
4011
|
+
const name = String(event.toolName).toLowerCase();
|
|
4012
|
+
if (isDirectControlMutation(name, event.input, ctx.cwd) || isShellControlMutation(name, event.input) || name === "devflow_transition") {
|
|
4013
|
+
return { block: true, reason: "动态计划由 Core 独占写入;请使用 devflow_plan,不能手工写入通过状态。" };
|
|
4014
|
+
}
|
|
4015
|
+
if (name === "task" && requestedTaskAgents(event.input).names.includes("devflow-code-reviewer")) {
|
|
4016
|
+
const snapshot = parseJsonResult(await runDynamicPlan(ctx.cwd, { action: "inspect" }));
|
|
4017
|
+
const task = snapshot?.current_step as Record<string, unknown> | undefined;
|
|
4018
|
+
if (task?.kind === "review" && task.status === "running") dynamicReviewBindings.set(event.toolCallId, { taskId: String(task.id), attempt: String(task.attempt), revision: Number(snapshot?.revision) });
|
|
4019
|
+
}
|
|
4020
|
+
return;
|
|
4021
|
+
}
|
|
3946
4022
|
const decision = await (async () => {
|
|
3947
4023
|
const toolName = String(event.toolName).toLowerCase();
|
|
3948
4024
|
const toolInput = event.input && typeof event.input === "object" ? event.input as Record<string, unknown> : undefined;
|
|
@@ -4108,6 +4184,18 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
4108
4184
|
});
|
|
4109
4185
|
|
|
4110
4186
|
pi.on("tool_result", async (event, ctx) => {
|
|
4187
|
+
const dynamicBinding = dynamicReviewBindings.get(event.toolCallId);
|
|
4188
|
+
if (event.toolName === "task" && dynamicBinding) {
|
|
4189
|
+
dynamicReviewBindings.delete(event.toolCallId);
|
|
4190
|
+
const results = (event.details as { results?: Array<Record<string, unknown>> } | undefined)?.results;
|
|
4191
|
+
const child = results?.length === 1 ? results[0] : undefined;
|
|
4192
|
+
if (!event.isError && child?.agent === "devflow-code-reviewer" && child.exitCode === 0 && child.truncated === false && typeof child.output === "string") {
|
|
4193
|
+
try {
|
|
4194
|
+
const report = JSON.parse(child.output);
|
|
4195
|
+
if (typeof report.summary === "string" && Array.isArray(report.findings)) dynamicReviewResults.set(event.toolCallId, { ...dynamicBinding, report });
|
|
4196
|
+
} catch { /* Invalid reviewer output never becomes a pass. */ }
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4111
4199
|
if (event.toolName === "task") {
|
|
4112
4200
|
const binding = pendingReviewTasks.get(event.toolCallId);
|
|
4113
4201
|
if (binding) {
|
|
@@ -5018,6 +5106,59 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
5018
5106
|
},
|
|
5019
5107
|
});
|
|
5020
5108
|
|
|
5109
|
+
pi.registerTool({
|
|
5110
|
+
name: "devflow_plan",
|
|
5111
|
+
label: "Devflow 中文工作流",
|
|
5112
|
+
description: "Inspect or execute the Core-owned dynamic plan. Never accepts caller-written pass/approval results. Use revise for task proposals, claim before work, report for activity results, verify for actual executable checks, review_result for a captured independent review, and decision only for genuine user choices. Task fields: id,title,kind(discover/implement/verify/review),requirements(R-ids),dependencies,inputs,outputs,checks([{argv:string[],timeout?:number}]),cancelled?,reason?.",
|
|
5113
|
+
parameters: z.object({
|
|
5114
|
+
action: z.enum(["inspect", "revise", "claim", "report", "verify", "review_result", "recover", "wait", "resume", "decision"]),
|
|
5115
|
+
expected_revision: z.number().int().optional(),
|
|
5116
|
+
task_id: z.string().optional(),
|
|
5117
|
+
attempt: z.string().optional(),
|
|
5118
|
+
tasks: z.array(z.object({
|
|
5119
|
+
id: z.string(), title: z.string(), kind: z.enum(["discover", "implement", "repair", "verify", "review"]),
|
|
5120
|
+
requirements: z.array(z.string()), dependencies: z.array(z.string()), inputs: z.array(z.string()), outputs: z.array(z.string()),
|
|
5121
|
+
checks: z.array(z.object({ argv: z.array(z.string()).min(1), timeout: z.number().int().optional() })),
|
|
5122
|
+
cancelled: z.boolean().optional(), reason: z.string().optional(),
|
|
5123
|
+
})).optional(),
|
|
5124
|
+
reason: z.string().optional(), summary: z.string().optional(), task_call_id: z.string().optional(),
|
|
5125
|
+
question: z.string().optional(), options: z.array(z.string()).optional(),
|
|
5126
|
+
task_ids: z.array(z.string()).optional(),
|
|
5127
|
+
scope_decision_id: z.string().optional(), remove_requirements: z.array(z.string()).optional(), approval_option: z.string().optional(),
|
|
5128
|
+
}),
|
|
5129
|
+
approval: "write",
|
|
5130
|
+
async execute(_id, rawParams, _signal, _onUpdate, ctx) {
|
|
5131
|
+
const params = rawParams as { action: string; task_call_id?: string; task_id?: string; attempt?: string; expected_revision?: number; question?: string };
|
|
5132
|
+
const request: Record<string, unknown> = { ...params };
|
|
5133
|
+
if (params.action === "review_result") {
|
|
5134
|
+
const captured = params.task_call_id ? dynamicReviewResults.get(params.task_call_id) : undefined;
|
|
5135
|
+
if (!captured || captured.taskId !== params.task_id || captured.attempt !== params.attempt || captured.revision !== params.expected_revision) {
|
|
5136
|
+
return renderResult({ ok: false, exitCode: 1, stdout: "", stderr: "No current independent reviewer result is bound to this task attempt. Dispatch the read-only reviewer first." });
|
|
5137
|
+
}
|
|
5138
|
+
request.runtime_review = true;
|
|
5139
|
+
request.report = captured.report;
|
|
5140
|
+
}
|
|
5141
|
+
let result = await runDynamicPlan(ctx.cwd, request);
|
|
5142
|
+
let snapshot = parseJsonResult(result);
|
|
5143
|
+
if (snapshot?.ok === true && params.action === "review_result" && params.task_call_id) dynamicReviewResults.delete(params.task_call_id);
|
|
5144
|
+
if (snapshot?.ok === true && params.action === "decision") {
|
|
5145
|
+
const decisions = snapshot.decisions as Array<Record<string, unknown>>;
|
|
5146
|
+
const pending = decisions.find(item => item.status === "pending" && item.question === params.question);
|
|
5147
|
+
if (pending) {
|
|
5148
|
+
const removals = pending.remove_requirements as string[] | undefined;
|
|
5149
|
+
const question = String(pending.question) + (removals?.length ? `\n拟移除验收要求:${removals.join("、")};仅选择「${pending.approval_option}」会授权移除。` : "");
|
|
5150
|
+
const choice = await ctx.ui.select(question, pending.options as string[]);
|
|
5151
|
+
if (choice) {
|
|
5152
|
+
result = await runDynamicPlan(ctx.cwd, { action: "decide", expected_revision: snapshot.revision, decision_id: pending.id, choice, runtime_decision: true });
|
|
5153
|
+
snapshot = parseJsonResult(result);
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
5156
|
+
}
|
|
5157
|
+
if (snapshot?.mode === "dynamic") ctx.ui.setWidget?.("devflow-plan", projectControlWidget(renderDynamicWorkflow(snapshot).split("\n")));
|
|
5158
|
+
return renderResult(result);
|
|
5159
|
+
},
|
|
5160
|
+
});
|
|
5161
|
+
|
|
5021
5162
|
pi.registerTool({
|
|
5022
5163
|
name: "devflow_bootstrap",
|
|
5023
5164
|
label: "Devflow Bootstrap",
|
|
@@ -5025,11 +5166,24 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
5025
5166
|
parameters: z.object({
|
|
5026
5167
|
feature: z.string().min(1),
|
|
5027
5168
|
phase: z.string().min(1).optional(),
|
|
5028
|
-
flow: z.enum(["feature-development", "acceptance-remediation", "regression-acceptance"]).default("
|
|
5169
|
+
flow: z.enum(["dynamic", "feature-development", "acceptance-remediation", "regression-acceptance"]).default("dynamic"),
|
|
5029
5170
|
force: z.boolean().default(false),
|
|
5030
5171
|
}),
|
|
5031
5172
|
approval: "write",
|
|
5032
|
-
async execute(_id,
|
|
5173
|
+
async execute(_id, rawParams, _signal, _onUpdate, ctx) {
|
|
5174
|
+
const params = rawParams as { flow: string; feature: string; phase?: string; force: boolean };
|
|
5175
|
+
if (params.flow === "dynamic") {
|
|
5176
|
+
const result = await runDynamicPlan(ctx.cwd, { action: "initialize", feature: params.feature, phase: params.phase });
|
|
5177
|
+
const snapshot = parseJsonResult(result);
|
|
5178
|
+
if (snapshot?.mode === "dynamic") {
|
|
5179
|
+
dynamicTurnRevision = Number(snapshot.revision);
|
|
5180
|
+
dynamicTurnProgress = String(snapshot.progress_key);
|
|
5181
|
+
activeWorkflowTurn = true;
|
|
5182
|
+
activeContract = undefined;
|
|
5183
|
+
setControlState(ctx, executingState("梳理需求并生成中文步骤"));
|
|
5184
|
+
}
|
|
5185
|
+
return renderResult(result);
|
|
5186
|
+
}
|
|
5033
5187
|
// Consume the one-shot runtime authorization before running Core's
|
|
5034
5188
|
// atomic bootstrap sequence. A retry must obtain a fresh user approval.
|
|
5035
5189
|
runtimeBootstrapAuthorizations.delete(String(_id));
|
|
@@ -5398,6 +5552,15 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
5398
5552
|
|
|
5399
5553
|
const devflowCommand = async (args: string, ctx: DevflowCommandContext): Promise<void> => {
|
|
5400
5554
|
const trimmed = args.trim();
|
|
5555
|
+
if (["", "status", "control", "explain-state"].includes(trimmed)) {
|
|
5556
|
+
const dynamic = parseJsonResult(await runDynamicPlan(ctx.cwd, { action: "inspect" }));
|
|
5557
|
+
if (dynamic?.mode === "dynamic") {
|
|
5558
|
+
const lines = renderDynamicWorkflow(dynamic, trimmed === "control").split("\n");
|
|
5559
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
5560
|
+
ctx.ui.setWidget?.("devflow-plan", projectControlWidget(lines));
|
|
5561
|
+
return;
|
|
5562
|
+
}
|
|
5563
|
+
}
|
|
5401
5564
|
if (trimmed === "" || trimmed === "status") {
|
|
5402
5565
|
await showWorkflowOverview(ctx);
|
|
5403
5566
|
return;
|
|
@@ -5470,6 +5633,17 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
5470
5633
|
if (trimmed === "abandon" || trimmed.startsWith("abandon ") || trimmed === "cancel" || trimmed.startsWith("cancel ")) {
|
|
5471
5634
|
const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
5472
5635
|
const snapshot = inspected.snapshot;
|
|
5636
|
+
if (snapshot?.mode === "dynamic") {
|
|
5637
|
+
const result = await runDynamicPlan(ctx.cwd, { action: "cancel", expected_revision: snapshot.revision, runtime_cancel: true, reason: trimmed.replace(/^\S+\s*/, "") || "用户主动终止当前需求" });
|
|
5638
|
+
const cancelled = parseJsonResult(result);
|
|
5639
|
+
if (cancelled?.ok) {
|
|
5640
|
+
dynamicTurnRevision = undefined;
|
|
5641
|
+
activeWorkflowTurn = false;
|
|
5642
|
+
setControlState(ctx, initialControlState());
|
|
5643
|
+
ctx.ui.notify("已取消需求,保留已有工作与失败证据。", "info");
|
|
5644
|
+
} else ctx.ui.notify(String(cancelled?.reason ?? result.stderr), "error");
|
|
5645
|
+
return;
|
|
5646
|
+
}
|
|
5473
5647
|
const revision = typeof snapshot?.revision === "number" ? snapshot.revision : undefined;
|
|
5474
5648
|
const feature = typeof snapshot?.current_feature === "string" ? snapshot.current_feature : undefined;
|
|
5475
5649
|
const status = typeof snapshot?.status === "string" ? snapshot.status : undefined;
|
|
@@ -5573,100 +5747,19 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
5573
5747
|
}
|
|
5574
5748
|
if (trimmed.startsWith("start ")) {
|
|
5575
5749
|
const [feature, requestedPhase] = trimmed.slice(6).split("|", 2).map(value => value.trim());
|
|
5576
|
-
if (!feature) {
|
|
5577
|
-
ctx.ui.notify("
|
|
5578
|
-
return;
|
|
5579
|
-
}
|
|
5580
|
-
const phase = requestedPhase || defaultPhaseId(feature);
|
|
5581
|
-
if (!validPhaseId(phase)) {
|
|
5582
|
-
ctx.ui.notify("Phase ID 必须是 1-64 位字母、数字、下划线或连字符,且不得包含路径分隔符。", "error");
|
|
5750
|
+
if (!feature || (requestedPhase && !validPhaseId(requestedPhase))) {
|
|
5751
|
+
ctx.ui.notify("用法:/devflow start <需求名>|<可选阶段标识>", "error");
|
|
5583
5752
|
return;
|
|
5584
5753
|
}
|
|
5585
5754
|
const report = await runDoctor(ctx);
|
|
5586
|
-
if (!report.ok)
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
const existingFeature = existing.snapshot?.current_feature;
|
|
5592
|
-
const previousWorkflowComplete = ["workflow_complete", "workflow_cancelled"].includes(existing.snapshot?.action as string);
|
|
5593
|
-
if (
|
|
5594
|
-
!previousWorkflowComplete &&
|
|
5595
|
-
typeof existingFeature === "string" &&
|
|
5596
|
-
existingFeature !== feature
|
|
5597
|
-
) {
|
|
5598
|
-
ctx.ui.notify(
|
|
5599
|
-
`当前仍有未完成需求:${existingFeature}。请在对话中继续处理当前步骤,完成后再创建新需求:${feature}。`,
|
|
5600
|
-
"error",
|
|
5601
|
-
);
|
|
5602
|
-
return;
|
|
5603
|
-
}
|
|
5604
|
-
// This dry-run happens before init_workflow.rb, so a rejected feature
|
|
5605
|
-
// can never overwrite generated workflow files belonging to an active
|
|
5606
|
-
// owner or a legacy phase directory that has no trustworthy owner.
|
|
5607
|
-
const ownership = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/phase_ownership.rb", [
|
|
5608
|
-
"claim",
|
|
5609
|
-
"--feature",
|
|
5610
|
-
feature,
|
|
5611
|
-
"--phase",
|
|
5612
|
-
phase,
|
|
5613
|
-
"--flow",
|
|
5614
|
-
"feature-development",
|
|
5615
|
-
"--json",
|
|
5616
|
-
]);
|
|
5617
|
-
if (!controlPlaneSucceeded(ownership)) {
|
|
5618
|
-
ctx.ui.notify((ownership.stderr || ownership.stdout).slice(0, 6000), "error");
|
|
5755
|
+
if (!report.ok) ctx.ui.notify(formatDoctorReport(report).slice(0, 6000), "info");
|
|
5756
|
+
const result = await runDynamicPlan(ctx.cwd, { action: "initialize", feature, phase: requestedPhase || undefined });
|
|
5757
|
+
const snapshot = parseJsonResult(result);
|
|
5758
|
+
if (!snapshot?.ok) {
|
|
5759
|
+
ctx.ui.notify(String(snapshot?.reason ?? result.stderr), "error");
|
|
5619
5760
|
return;
|
|
5620
5761
|
}
|
|
5621
|
-
|
|
5622
|
-
"--write",
|
|
5623
|
-
"--json",
|
|
5624
|
-
]);
|
|
5625
|
-
if (!controlPlaneSucceeded(workflow)) {
|
|
5626
|
-
ctx.ui.notify(workflow.stderr || workflow.stdout, "error");
|
|
5627
|
-
return;
|
|
5628
|
-
}
|
|
5629
|
-
const state = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/init_state.rb", [
|
|
5630
|
-
"--feature",
|
|
5631
|
-
feature,
|
|
5632
|
-
"--phase",
|
|
5633
|
-
phase,
|
|
5634
|
-
"--flow",
|
|
5635
|
-
"feature-development",
|
|
5636
|
-
"--write",
|
|
5637
|
-
"--json",
|
|
5638
|
-
...(previousWorkflowComplete ? ["--force"] : []),
|
|
5639
|
-
]);
|
|
5640
|
-
if (!controlPlaneSucceeded(state)) {
|
|
5641
|
-
ctx.ui.notify((state.stderr || state.stdout).slice(0, 6000), "error");
|
|
5642
|
-
return;
|
|
5643
|
-
}
|
|
5644
|
-
const artifacts = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/sync_artifacts.rb", [
|
|
5645
|
-
"--write",
|
|
5646
|
-
"--json",
|
|
5647
|
-
]);
|
|
5648
|
-
if (!controlPlaneSucceeded(artifacts)) {
|
|
5649
|
-
ctx.ui.notify((artifacts.stderr || artifacts.stdout).slice(0, 6000), "error");
|
|
5650
|
-
return;
|
|
5651
|
-
}
|
|
5652
|
-
const inspected = await inspectWorkflow(ctx.cwd);
|
|
5653
|
-
if (!inspected.result.ok || !inspected.snapshot) {
|
|
5654
|
-
ctx.ui.notify(
|
|
5655
|
-
`Devflow initialized but inspector failed: ${inspected.result.stderr || inspected.result.stdout || "invalid JSON"}`,
|
|
5656
|
-
"error",
|
|
5657
|
-
);
|
|
5658
|
-
return;
|
|
5659
|
-
}
|
|
5660
|
-
setControlState(ctx, planningState("requirement-draft"));
|
|
5661
|
-
await pi.sendUserMessage(
|
|
5662
|
-
[
|
|
5663
|
-
"<devflow-requirement-draft-directive>",
|
|
5664
|
-
`Devflow 已创建 Feature ${feature}。当前为安全的需求梳理阶段:允许澄清和只读证据收集,不得写入项目或迁移状态。`,
|
|
5665
|
-
"请先基于功能名说明你需要的最少业务背景,并邀请用户直接补充目标、使用者、约束和验收结果。收到用户的下一段需求描述后,运行时会将其原文注入 requirement-triage 并启动该步骤。",
|
|
5666
|
-
"不要让用户输入命令;也不要把普通需求文本当成批准或 checkpoint 确认。",
|
|
5667
|
-
"</devflow-requirement-draft-directive>",
|
|
5668
|
-
].join("\n"),
|
|
5669
|
-
);
|
|
5762
|
+
await startDynamicTurn(ctx, snapshot);
|
|
5670
5763
|
return;
|
|
5671
5764
|
}
|
|
5672
5765
|
ctx.ui.notify(
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: workflow-dynamic
|
|
3
|
+
description: Execute a Core-owned dynamic requirement plan, maintain one readable requirement document, route technical findings to repair skills, and ask only genuine business decisions. Use when the inspector reports mode dynamic; existing fixed workflows keep their own contracts.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Dynamic requirement delivery
|
|
7
|
+
|
|
8
|
+
Use `devflow_plan inspect` as the scheduling authority. Present task titles and progress in the user's language. Do not edit STATE, synthesize test results, or use legacy complete-step/checkpoint tools.
|
|
9
|
+
|
|
10
|
+
## Intent and planning
|
|
11
|
+
|
|
12
|
+
Read the single document returned by Core and inspect relevant source. Update that document for scope, approach, and acceptance requirements. Each acceptance clause has a stable heading such as `### R-1: Do not send another batch after the deadline`. Preserve IDs across wording changes. Cosmetic whitespace does not change the obligation. Removing an obligation requires a genuine scope decision; never delete a failed obligation to finish.
|
|
13
|
+
|
|
14
|
+
Propose tasks with `revise`, including its inspected `expected_revision` and a plain-language change reason. Fields are `id`, `title`, `kind`, `requirements`, `dependencies`, `inputs`, `outputs`, `checks`. Allowed kinds: discover, implement, verify, review. Each check declares an executable `argv` array and optional timeout seconds. Inputs must include the relevant production code, tests, and configuration: a test file alone is insufficient. Prefer directories where additional source files affect behavior. Every acceptance clause needs a verification task. Code changes need an independent review task that depends on the implementation and verification tasks.
|
|
15
|
+
|
|
16
|
+
Generate only applicable work. A backend-only feature has no UI, wireframe, browser-test, or frontend-document task. If applicability is unclear, investigate it before proposing that branch. Do not generate no-op documents. Tasks may produce source, tests, migrations, schemas or genuinely needed design assets; their existence must have a consumer.
|
|
17
|
+
|
|
18
|
+
## Execution
|
|
19
|
+
|
|
20
|
+
1. Inspect and claim the task selected by Core. Use the returned task ID and attempt for all subsequent results.
|
|
21
|
+
2. Read its exact Skill ID. In dynamic mode use that skill's technical procedures, but the dynamic task supplies scope, inputs, outputs and acceptance. Legacy stage lists, mandatory Markdown/meta pairs and checkpoint instructions do not apply.
|
|
22
|
+
3. For discovery/implementation/repair, do the work and call `report` with a factual summary. This records activity completion, not an acceptance pass.
|
|
23
|
+
4. For verification, call `verify`. Core executes the declared commands and captures exit status and output. Do not substitute a shell transcript or self-reported pass. Use tests that assert the actual expected behavior, including production paths and meaningful failure scenarios.
|
|
24
|
+
5. For review, dispatch a read-only `devflow-code-reviewer` task with the requirement document, task, source paths and recorded verification evidence. Ask for strict JSON `{"summary":"...","findings":[...]}`; each finding identifies the violated requirement, evidence, affected scope and suggested recheck. Submit its task-call ID using `review_result`; Core accepts only the captured child result, not your rewritten report.
|
|
25
|
+
6. Continue ready tasks. Failed checks automatically create a bounded repair task. Read the finding and invoke its repair skill, then allow Core to rerun the original verification. Do not reset repair budgets by renaming tasks.
|
|
26
|
+
|
|
27
|
+
## Changes, waiting, and recovery
|
|
28
|
+
|
|
29
|
+
Keep stable task IDs. To cancel unnecessary work, retain the task with `cancelled: true` and a reason and rewire its consumers in the same proposal. Preserve verification coverage. A failed required behavior is a repair, not an irrelevant task.
|
|
30
|
+
|
|
31
|
+
After a requirement edit, inspect and revise only affected work. Old evidence remains history and cannot certify changed inputs. Never weaken checks merely to obtain a pass.
|
|
32
|
+
|
|
33
|
+
Use `decision` only for business alternatives, scope reduction, residual-risk choices, or actions outside existing authorization. Supply one clear question and two or three options with consequences; place the recommendation first. Core displays and records the actual user choice. Missing metadata, stale indexes, missing tests and repairable code defects are technical work, not user decisions. A cancelled choice stays pending; it is not approval. Continue independent tasks if possible.
|
|
34
|
+
|
|
35
|
+
For removed acceptance clauses, include their IDs in `remove_requirements` and identify the exact `approval_option` in the decision. Only that actual user selection authorizes removing those clauses; bind the subsequent revision with `scope_decision_id`. A generic confirmation or a choice to retain scope is not permission to remove acceptance requirements.
|
|
36
|
+
|
|
37
|
+
For an interrupted running/checking task, inspect actual effects first. Use `recover` with the findings only after establishing the previous operation is no longer running and retry is appropriate. Do not rerun uncertain external writes. Repair exhaustion remains failed: explain what was attempted and actionable next choices without claiming delivery complete.
|
|
38
|
+
|
|
39
|
+
If an external dependency is unavailable, use `wait` with the task attempt and concrete reason. After read-only evidence establishes readiness, use `resume`; this creates eligibility for a new attempt. Do not classify missing generated documents as external dependencies.
|
|
40
|
+
|
|
41
|
+
Summarize the readable task board and remaining decisions. Detailed machine evidence stays in Core; do not generate another report document for each invocation.
|
|
@@ -3,6 +3,9 @@ name: workflow-orchestrator
|
|
|
3
3
|
description: Use when the user asks to continue, resume, advance, inspect, or route a .devflow/planning-based workflow; when the OMP development agent must decide the next workflow skill from project state; or when a checkpoint must be handled before proceeding.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
Dynamic plan mode: when Core reports `mode: dynamic`, use `workflow-dynamic` as the execution protocol. The selected task supplies scope, dependencies, required outputs and verification. Use this skill's relevant technical guidance, but do not create legacy stage documents/meta pairs, require fixed checkpoints, or advance legacy STATE. Return activity/evidence to `devflow_plan`; only Core records verification.
|
|
7
|
+
|
|
8
|
+
|
|
6
9
|
## 默认软门禁策略
|
|
7
10
|
|
|
8
11
|
工作流硬门禁已移除。下文及各步骤中的产物、索引、合同、审查、质量、恢复和 CP 审批条件均为诊断建议:保留失败证据和缺项,允许继续执行。用户说“下一步”即可推进,无需额外批准弹窗;不得把继续执行记录成人工批准,也不得把失败、未执行的测试改写为通过。真实读写/执行失败、无法解析的状态和并发版本冲突须如实报告。硬门禁及其恢复开关已移除。
|
|
@@ -15,6 +15,12 @@ require_relative "validate_state"
|
|
|
15
15
|
require_relative "phase_ownership"
|
|
16
16
|
require_relative "requirement_graph"
|
|
17
17
|
require_relative "gate_runtime"
|
|
18
|
+
require_relative "dynamic_plan"
|
|
19
|
+
|
|
20
|
+
if DynamicPlan.read_state(Dir.pwd)&.dig("current_flow") == "dynamic"
|
|
21
|
+
puts JSON.generate({ "ok" => false, "reason" => "dynamic_plan_requires_evidence", "message" => "Use devflow_plan; legacy complete-step cannot complete dynamic tasks." })
|
|
22
|
+
exit 1
|
|
23
|
+
end
|
|
18
24
|
|
|
19
25
|
options = {
|
|
20
26
|
state: nil,
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "yaml"
|
|
5
|
+
require "digest"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
require "open3"
|
|
8
|
+
require "timeout"
|
|
9
|
+
require_relative "control_io"
|
|
10
|
+
|
|
11
|
+
# STATE is the only mutable authority. Documents own intent, never verdicts.
|
|
12
|
+
module DynamicPlan
|
|
13
|
+
SKILLS = {
|
|
14
|
+
"discover" => "workflow-requirement-triage",
|
|
15
|
+
"implement" => "workflow-execute-phase",
|
|
16
|
+
"repair" => "workflow-bug-fix",
|
|
17
|
+
"verify" => "workflow-run-acceptance-tests",
|
|
18
|
+
"review" => "code-review"
|
|
19
|
+
}.freeze
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def digest(value)
|
|
23
|
+
Digest::SHA256.hexdigest(JSON.generate(value))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def normalize(text)
|
|
27
|
+
text.to_s.split.join(" ")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def path(root, relative)
|
|
31
|
+
raise ArgumentError, "project-relative path required" unless relative.is_a?(String) && !relative.empty? && !relative.start_with?("/") && !relative.split(/[\\\/]/).include?("..")
|
|
32
|
+
absolute = File.expand_path(relative, root)
|
|
33
|
+
cursor = absolute
|
|
34
|
+
cursor = File.dirname(cursor) until File.exist?(cursor) || File.symlink?(cursor)
|
|
35
|
+
real = File.realpath(cursor)
|
|
36
|
+
raise ArgumentError, "path escapes project" unless real == File.realpath(root) || real.start_with?(File.realpath(root) + "/")
|
|
37
|
+
absolute
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def clauses(root, plan)
|
|
41
|
+
text = File.read(path(root, plan.fetch("document")))
|
|
42
|
+
result = {}
|
|
43
|
+
text.scan(/^### (R-[A-Za-z0-9_-]+)[::]([^\n]*\n(?:(?!^### |^## ).*\n?)*)/).each do |id, body|
|
|
44
|
+
raise ArgumentError, "duplicate requirement #{id}" if result.key?(id)
|
|
45
|
+
result[id] = normalize(body)
|
|
46
|
+
end
|
|
47
|
+
result
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def file_pin(root, relative)
|
|
51
|
+
absolute = path(root, relative)
|
|
52
|
+
return "missing" unless File.exist?(absolute)
|
|
53
|
+
return Digest::SHA256.file(absolute).hexdigest if File.file?(absolute)
|
|
54
|
+
entries = Dir.glob(File.join(absolute, "**", "*"), File::FNM_DOTMATCH).sort.filter_map do |entry|
|
|
55
|
+
next if entry.sub(absolute + "/", "").split("/").any? { |part| %w[.git node_modules .devflow __pycache__].include?(part) }
|
|
56
|
+
next unless File.file?(entry)
|
|
57
|
+
safe = path(root, entry.sub(File.expand_path(root) + "/", ""))
|
|
58
|
+
[entry.sub(absolute + "/", ""), Digest::SHA256.file(safe).hexdigest]
|
|
59
|
+
end
|
|
60
|
+
digest(entries)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def pins(root, plan, task)
|
|
64
|
+
requirements = clauses(root, plan)
|
|
65
|
+
{
|
|
66
|
+
"requirements" => task.fetch("requirements").to_h { |id| [id, requirements[id]] },
|
|
67
|
+
"inputs" => task.fetch("inputs").to_h { |file| [file, file_pin(root, file)] },
|
|
68
|
+
"spec" => digest(task.slice("kind", "requirements", "inputs", "outputs", "checks"))
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def validate!(root, plan)
|
|
73
|
+
tasks = plan.fetch("tasks")
|
|
74
|
+
raise ArgumentError, "tasks must be an array" unless tasks.is_a?(Array)
|
|
75
|
+
ids = tasks.map { |task| task.fetch("id") }
|
|
76
|
+
raise ArgumentError, "duplicate task ids" unless ids.uniq == ids
|
|
77
|
+
tasks.each do |task|
|
|
78
|
+
raise ArgumentError, "invalid task id" unless task["id"].match?(/\A[a-zA-Z0-9][a-zA-Z0-9_-]{0,79}\z/)
|
|
79
|
+
raise ArgumentError, "unknown capability" unless SKILLS.key?(task["kind"])
|
|
80
|
+
raise ArgumentError, "task title required" if task["title"].to_s.strip.empty?
|
|
81
|
+
%w[dependencies requirements inputs outputs checks].each { |key| raise ArgumentError, "#{key} must be an array" unless task[key].is_a?(Array) }
|
|
82
|
+
raise ArgumentError, "unknown dependency" unless (task["dependencies"] - ids).empty?
|
|
83
|
+
(task["inputs"] + task["outputs"]).each { |file| path(root, file) }
|
|
84
|
+
raise ArgumentError, "verification needs inputs and executable checks" if task["kind"] == "verify" && (task["checks"].empty? || task["inputs"].empty?)
|
|
85
|
+
raise ArgumentError, "review needs source inputs" if task["kind"] == "review" && task["inputs"].empty?
|
|
86
|
+
task["checks"].each do |check|
|
|
87
|
+
argv = check["argv"]
|
|
88
|
+
raise ArgumentError, "checks require nonempty argv arrays" unless argv.is_a?(Array) && argv.any? && argv.all? { |arg| arg.is_a?(String) && !arg.include?("\0") }
|
|
89
|
+
end
|
|
90
|
+
next unless task["status"] == "cancelled"
|
|
91
|
+
raise ArgumentError, "cancellation reason required" if task["reason"].to_s.strip.empty?
|
|
92
|
+
raise ArgumentError, "cancelled task still has consumers" if tasks.any? { |other| other["status"] != "cancelled" && other["dependencies"].include?(task["id"]) }
|
|
93
|
+
end
|
|
94
|
+
visited, visiting = [], []
|
|
95
|
+
visit = lambda do |id|
|
|
96
|
+
raise ArgumentError, "cyclic task dependencies" if visiting.include?(id)
|
|
97
|
+
return if visited.include?(id)
|
|
98
|
+
visiting << id
|
|
99
|
+
tasks.find { |task| task["id"] == id }["dependencies"].each { |dependency| visit.call(dependency) }
|
|
100
|
+
visiting.delete(id)
|
|
101
|
+
visited << id
|
|
102
|
+
end
|
|
103
|
+
ids.each { |id| visit.call(id) }
|
|
104
|
+
requirements = clauses(root, plan)
|
|
105
|
+
active = tasks.reject { |task| task["status"] == "cancelled" }
|
|
106
|
+
active.each do |task|
|
|
107
|
+
raise ArgumentError, "unknown requirement" unless (task["requirements"] - requirements.keys).empty?
|
|
108
|
+
end
|
|
109
|
+
unless tasks.empty? || plan["cancelled"]
|
|
110
|
+
raise ArgumentError, "requirement clauses missing from main document" if requirements.empty?
|
|
111
|
+
requirements.each_key do |id|
|
|
112
|
+
raise ArgumentError, "#{id} has no verification task" unless active.any? { |task| task["kind"] == "verify" && task["requirements"].include?(id) }
|
|
113
|
+
end
|
|
114
|
+
if active.any? { |task| %w[implement repair].include?(task["kind"]) }
|
|
115
|
+
raise ArgumentError, "code changes require independent review" unless active.any? { |task| task["kind"] == "review" }
|
|
116
|
+
end
|
|
117
|
+
ancestors = lambda do |task|
|
|
118
|
+
task["dependencies"].flat_map do |id|
|
|
119
|
+
[id, *ancestors.call(tasks.find { |other| other["id"] == id })]
|
|
120
|
+
end.uniq
|
|
121
|
+
end
|
|
122
|
+
active.select { |task| task["kind"] == "verify" }.each do |task|
|
|
123
|
+
producers = active.select { |other| other["kind"] == "implement" && (other["requirements"] & task["requirements"]).any? }
|
|
124
|
+
raise ArgumentError, "verification must depend on its implementation" unless (producers.map { |item| item["id"] } - ancestors.call(task)).empty?
|
|
125
|
+
end
|
|
126
|
+
if active.any? { |task| %w[implement repair].include?(task["kind"]) }
|
|
127
|
+
changed_requirements = active.select { |task| %w[implement repair].include?(task["kind"]) }.flat_map { |task| task["requirements"] }.uniq
|
|
128
|
+
changed_requirements.each do |id|
|
|
129
|
+
verifiers = active.select { |task| task["kind"] == "verify" && task["requirements"].include?(id) }
|
|
130
|
+
covered = active.any? do |task|
|
|
131
|
+
task["kind"] == "review" && task["requirements"].include?(id) && (verifiers.map { |item| item["id"] } - ancestors.call(task)).empty?
|
|
132
|
+
end
|
|
133
|
+
raise ArgumentError, "#{id} needs review after verification" unless covered
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
true
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def refresh!(root, plan)
|
|
141
|
+
plan["tasks"].each do |task|
|
|
142
|
+
next unless task["status"] == "completed" && task["evidence"]
|
|
143
|
+
current_pins = pins(root, plan, task)
|
|
144
|
+
observed_pins = task["evidence"]["pins"]
|
|
145
|
+
unchanged = if %w[implement repair].include?(task["kind"])
|
|
146
|
+
observed_pins.slice("requirements", "spec") == current_pins.slice("requirements", "spec")
|
|
147
|
+
else
|
|
148
|
+
observed_pins == current_pins
|
|
149
|
+
end
|
|
150
|
+
next if unchanged
|
|
151
|
+
task["status"] = "pending"
|
|
152
|
+
task["verification"] = "stale"
|
|
153
|
+
end
|
|
154
|
+
loop do
|
|
155
|
+
changed = false
|
|
156
|
+
plan["tasks"].each do |task|
|
|
157
|
+
next unless task["status"] == "completed"
|
|
158
|
+
next if task["dependencies"].all? { |id| plan["tasks"].find { |other| other["id"] == id }&.dig("status") == "completed" }
|
|
159
|
+
task["status"], task["verification"], changed = "pending", "stale", true
|
|
160
|
+
end
|
|
161
|
+
break unless changed
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def snapshot(root, state)
|
|
166
|
+
plan = state.fetch("plan")
|
|
167
|
+
refresh!(root, plan)
|
|
168
|
+
validation_error = begin
|
|
169
|
+
validate!(root, plan)
|
|
170
|
+
nil
|
|
171
|
+
rescue ArgumentError, KeyError => error
|
|
172
|
+
error.message
|
|
173
|
+
end
|
|
174
|
+
tasks = plan["tasks"]
|
|
175
|
+
ready = tasks.find do |task|
|
|
176
|
+
task["status"] == "pending" && task["dependencies"].all? { |id| tasks.find { |other| other["id"] == id }&.dig("status") == "completed" }
|
|
177
|
+
end
|
|
178
|
+
running = tasks.find { |task| %w[running checking].include?(task["status"]) }
|
|
179
|
+
complete = tasks.any? && tasks.all? { |task| %w[completed cancelled].include?(task["status"]) } && plan.fetch("decisions", []).none? { |item| item["status"] == "pending" }
|
|
180
|
+
action = if plan["cancelled"] then "workflow_cancelled"
|
|
181
|
+
elsif validation_error then "reconcile_plan"
|
|
182
|
+
elsif tasks.empty? then "plan_work"
|
|
183
|
+
elsif running then "resume_task"
|
|
184
|
+
elsif ready then "run_task"
|
|
185
|
+
elsif complete then "workflow_complete"
|
|
186
|
+
else "waiting" end
|
|
187
|
+
selected = running || ready
|
|
188
|
+
{
|
|
189
|
+
"ok" => true, "mode" => "dynamic", "action" => action, "revision" => state["revision"],
|
|
190
|
+
"current_flow" => "dynamic", "current_feature" => state["current_feature"], "current_phase" => state["current_phase"],
|
|
191
|
+
"status" => plan["cancelled"] ? "cancelled" : (complete && !validation_error ? "completed" : "in_progress"),
|
|
192
|
+
"document" => plan["document"], "plan_revision" => plan["revision"], "reason" => validation_error,
|
|
193
|
+
"progress_key" => digest(tasks.map { |task| task.slice("id", "status", "verification", "attempt").merge("results" => task.fetch("history", []).length) }),
|
|
194
|
+
"delivery" => plan["cancelled"] ? "cancelled" : (complete && !validation_error ? "ready" : "incomplete"),
|
|
195
|
+
"current_step" => selected && selected.merge("label" => selected["title"], "skill" => SKILLS[selected["kind"]]),
|
|
196
|
+
"workflow_steps" => tasks.map { |task| task.slice("id", "status", "verification", "reason").merge("label" => task["title"]) },
|
|
197
|
+
"tasks" => tasks, "decisions" => plan.fetch("decisions", [])
|
|
198
|
+
}
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def read_state(root)
|
|
202
|
+
file, = DevflowControlIO.resolve_control_path(root, ".devflow/planning/STATE.md", :state)
|
|
203
|
+
return nil unless File.file?(file)
|
|
204
|
+
YAML.safe_load(File.read(file)[/\A---\n(.*?)\n---\n/m, 1], permitted_classes: [], aliases: false)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def save(root, state)
|
|
208
|
+
state["revision"] += 1
|
|
209
|
+
view = snapshot(root, state)
|
|
210
|
+
state["status"] = view["status"]
|
|
211
|
+
state["current_step"] = view.dig("current_step", "id")
|
|
212
|
+
state["completed_steps"] = state["plan"]["tasks"].select { |task| task["status"] == "completed" }.map { |task| task["id"] }
|
|
213
|
+
state["pending_steps"] = state["plan"]["tasks"].reject { |task| %w[completed cancelled].include?(task["status"]) }.map { |task| task["id"] }
|
|
214
|
+
file, = DevflowControlIO.resolve_control_path(root, ".devflow/planning/STATE.md", :state)
|
|
215
|
+
# JSON round-trip removes shared object identities; safe YAML readers must
|
|
216
|
+
# never need aliases to read a runtime state snapshot.
|
|
217
|
+
DevflowControlIO.atomic_write(file, JSON.parse(JSON.generate(state)).to_yaml + "---\n\n# Runtime state\n\nManaged by Core. Edit the requirement document to change intent.\n")
|
|
218
|
+
snapshot(root, state)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def transact(root, expected)
|
|
222
|
+
lock = DevflowControlIO.acquire_project_lock(root)
|
|
223
|
+
state = read_state(root)
|
|
224
|
+
raise ArgumentError, "dynamic plan not initialized" unless state && state["current_flow"] == "dynamic"
|
|
225
|
+
raise ArgumentError, "REVISION_CONFLICT: inspect again" unless expected.is_a?(Integer) && expected == state["revision"]
|
|
226
|
+
before = digest(state)
|
|
227
|
+
refresh!(root, state["plan"])
|
|
228
|
+
yield state
|
|
229
|
+
return snapshot(root, state) if digest(state) == before
|
|
230
|
+
save(root, state)
|
|
231
|
+
ensure
|
|
232
|
+
DevflowControlIO.release_lock(lock)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def run_check(root, check)
|
|
236
|
+
output, status, timed_out = +"", nil, false
|
|
237
|
+
seconds = [[Integer(check.fetch("timeout", 120)), 1].max, 600].min
|
|
238
|
+
Open3.popen2e(*check.fetch("argv"), chdir: root, pgroup: true) do |stdin, stream, wait|
|
|
239
|
+
stdin.close
|
|
240
|
+
reader = Thread.new do
|
|
241
|
+
loop do
|
|
242
|
+
chunk = stream.readpartial(4096)
|
|
243
|
+
output << chunk if output.bytesize < 32_768
|
|
244
|
+
end
|
|
245
|
+
rescue EOFError, IOError
|
|
246
|
+
nil
|
|
247
|
+
end
|
|
248
|
+
begin
|
|
249
|
+
Timeout.timeout(seconds) { status = wait.value.exitstatus }
|
|
250
|
+
rescue Timeout::Error
|
|
251
|
+
timed_out = true
|
|
252
|
+
Process.kill("KILL", -wait.pid) rescue nil
|
|
253
|
+
wait.value
|
|
254
|
+
ensure
|
|
255
|
+
# Drain the exited child's buffered output before closing the pipe.
|
|
256
|
+
# A descendant retaining the pipe cannot keep recovery waiting forever.
|
|
257
|
+
reader.join(1)
|
|
258
|
+
stream.close unless stream.closed?
|
|
259
|
+
reader.join
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
{ "argv" => check["argv"], "exit_code" => status, "timeout" => timed_out, "output" => output.encode("UTF-8", invalid: :replace, undef: :replace), "passed" => status == 0 && !timed_out }
|
|
263
|
+
rescue SystemCallError => error
|
|
264
|
+
{ "argv" => check["argv"], "passed" => false, "output" => error.message }
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def failure!(plan, task, evidence)
|
|
268
|
+
task["status"], task["verification"] = "failed", "fail"
|
|
269
|
+
fingerprint = digest(evidence["checks"]&.map { |check| check.slice("argv", "exit_code", "output") } || evidence["review"])
|
|
270
|
+
previous = task.fetch("history", [])[0...-1].last
|
|
271
|
+
attempts = task.fetch("repair_attempts", 0)
|
|
272
|
+
if attempts >= 2 || (previous && previous["finding"] == fingerprint && previous["pins"] == evidence["pins"])
|
|
273
|
+
task["reason"] = "自动修复已停止:达到次数上限或没有进展;保留失败结果。"
|
|
274
|
+
return
|
|
275
|
+
end
|
|
276
|
+
evidence["finding"] = fingerprint
|
|
277
|
+
task["repair_attempts"] = attempts + 1
|
|
278
|
+
repair_id = "repair-#{digest(task['id'])[0, 12]}-#{attempts + 1}"
|
|
279
|
+
task["status"] = "pending"
|
|
280
|
+
task["dependencies"] << repair_id
|
|
281
|
+
plan["tasks"] << {
|
|
282
|
+
"id" => repair_id, "title" => "修复:#{task['title']}", "kind" => "repair", "requirements" => task["requirements"].dup,
|
|
283
|
+
"inputs" => task["inputs"].dup, "outputs" => [], "checks" => [], "dependencies" => [], "status" => "pending",
|
|
284
|
+
"verification" => "not_run", "finding" => evidence, "repairs" => task["id"], "history" => []
|
|
285
|
+
}
|
|
286
|
+
unless plan["tasks"].any? { |item| item["kind"] == "review" && item["status"] != "cancelled" && (task["requirements"] - item["requirements"]).empty? }
|
|
287
|
+
plan["tasks"] << {
|
|
288
|
+
"id" => "review-#{digest(task['id'])[0, 12]}-#{attempts + 1}", "title" => "独立审查修复结果", "kind" => "review", "requirements" => task["requirements"].dup,
|
|
289
|
+
"inputs" => task["inputs"].dup, "outputs" => [], "checks" => [], "dependencies" => plan["tasks"].select { |item| item["kind"] == "verify" && item["status"] != "cancelled" && (item["requirements"] & task["requirements"]).any? }.map { |item| item["id"] },
|
|
290
|
+
"status" => "pending", "verification" => "not_run", "history" => []
|
|
291
|
+
}
|
|
292
|
+
end
|
|
293
|
+
plan["revision"] += 1
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def dispatch(root, request)
|
|
297
|
+
action = request.fetch("action")
|
|
298
|
+
if action == "inspect"
|
|
299
|
+
state = read_state(root)
|
|
300
|
+
return { "ok" => true, "mode" => "legacy" } unless state && state["current_flow"] == "dynamic"
|
|
301
|
+
return snapshot(root, state)
|
|
302
|
+
end
|
|
303
|
+
if action == "initialize"
|
|
304
|
+
lock = DevflowControlIO.acquire_project_lock(root)
|
|
305
|
+
begin
|
|
306
|
+
old = read_state(root)
|
|
307
|
+
raise ArgumentError, "active workflow exists; continue it or explicitly migrate" if old && !%w[completed cancelled].include?(old["status"])
|
|
308
|
+
feature = request.fetch("feature").to_s.strip
|
|
309
|
+
raise ArgumentError, "feature required" if feature.empty?
|
|
310
|
+
if old
|
|
311
|
+
archive = path(root, ".devflow/runs/plans/#{Digest::SHA256.hexdigest(old['current_phase'].to_s)[0, 16]}-#{SecureRandom.uuid}.json")
|
|
312
|
+
DevflowControlIO.atomic_write(archive, JSON.pretty_generate(old))
|
|
313
|
+
end
|
|
314
|
+
phase = request["phase"] || "feature-#{Digest::SHA256.hexdigest(feature)[0, 12]}-#{SecureRandom.hex(3)}"
|
|
315
|
+
raise ArgumentError, "invalid phase identifier" unless phase.is_a?(String) && phase.match?(/\A[A-Za-z0-9][A-Za-z0-9_-]{0,63}\z/)
|
|
316
|
+
document = ".devflow/planning/features/#{phase}/需求.md"
|
|
317
|
+
absolute = path(root, document)
|
|
318
|
+
raise ArgumentError, "requirement document already exists; choose a new phase or resume" if File.exist?(absolute)
|
|
319
|
+
DevflowControlIO.atomic_write(absolute, "# #{feature}\n\n## 目标与范围\n\n#{request.fetch('description', feature)}\n\n## 验收要求\n\n在需求核实后,将可验证要求写成 `### R-1: 要求描述`,每条保留稳定编号。\n\n## 方案与决定\n\n执行进度由 Core 展示;此处不手工记录通过状态。\n")
|
|
320
|
+
state = { "state_schema_version" => 3, "revision" => old ? old["revision"] : -1, "current_flow" => "dynamic", "current_feature" => feature, "current_phase" => phase,
|
|
321
|
+
"plan" => { "version" => 1, "revision" => 0, "document" => document, "tasks" => [], "decisions" => [], "changes" => [] } }
|
|
322
|
+
return save(root, state)
|
|
323
|
+
ensure
|
|
324
|
+
DevflowControlIO.release_lock(lock)
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
if action == "verify"
|
|
328
|
+
claim = transact(root, request["expected_revision"]) do |state|
|
|
329
|
+
task = state["plan"]["tasks"].find { |item| item["id"] == request["task_id"] }
|
|
330
|
+
raise ArgumentError, "running verification attempt required" unless task && task["kind"] == "verify" && task["status"] == "running" && task["attempt"] == request["attempt"]
|
|
331
|
+
task["status"] = "checking"
|
|
332
|
+
task["runner_pid"] = Process.pid
|
|
333
|
+
task["check_pins"] = pins(root, state["plan"], task)
|
|
334
|
+
end
|
|
335
|
+
task = claim["tasks"].find { |item| item["id"] == request["task_id"] }
|
|
336
|
+
checks = task["checks"].map { |check| run_check(root, check) }
|
|
337
|
+
return transact(root, claim["revision"]) do |state|
|
|
338
|
+
current = state["plan"]["tasks"].find { |item| item["id"] == task["id"] }
|
|
339
|
+
raise ArgumentError, "verification inputs changed during execution" unless current["attempt"] == task["attempt"] && task["check_pins"] == pins(root, state["plan"], current)
|
|
340
|
+
evidence = { "attempt" => task["attempt"], "pins" => task["check_pins"], "checks" => checks, "at" => Time.now.utc.iso8601 }
|
|
341
|
+
current["history"] << evidence
|
|
342
|
+
current["evidence"] = evidence
|
|
343
|
+
if checks.all? { |check| check["passed"] }
|
|
344
|
+
current["status"], current["verification"] = "completed", "pass"
|
|
345
|
+
else
|
|
346
|
+
failure!(state["plan"], current, evidence)
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
transact(root, request["expected_revision"]) do |state|
|
|
351
|
+
plan = state["plan"]
|
|
352
|
+
raise ArgumentError, "workflow was cancelled; create a new requirement" if plan["cancelled"]
|
|
353
|
+
case action
|
|
354
|
+
when "cancel"
|
|
355
|
+
raise ArgumentError, "cancellation must come from user runtime action" unless request["runtime_cancel"] == true
|
|
356
|
+
plan["cancelled"] = true
|
|
357
|
+
plan["cancel_reason"] = request.fetch("reason")
|
|
358
|
+
plan["tasks"].each do |task|
|
|
359
|
+
next if task["status"] == "completed"
|
|
360
|
+
task["status"], task["reason"] = "cancelled", plan["cancel_reason"]
|
|
361
|
+
task.delete("attempt")
|
|
362
|
+
end
|
|
363
|
+
when "revise"
|
|
364
|
+
old_tasks = plan["tasks"].to_h { |task| [task["id"], task] }
|
|
365
|
+
new_requirements = clauses(root, plan)
|
|
366
|
+
scope_decision = plan["decisions"].find { |item| item["id"] == request["scope_decision_id"] }
|
|
367
|
+
permitted_removals = if scope_decision && scope_decision["status"] == "answered" && scope_decision["choice"] == scope_decision["approval_option"] && scope_decision["requirements_pin"] == digest(plan.fetch("requirements", {}))
|
|
368
|
+
scope_decision.fetch("remove_requirements", [])
|
|
369
|
+
else [] end
|
|
370
|
+
proposal = request.fetch("tasks")
|
|
371
|
+
raise ArgumentError, "keep removed tasks as cancelled with reasons" unless (old_tasks.keys - proposal.map { |task| task["id"] }).empty?
|
|
372
|
+
new_tasks = proposal.map do |item|
|
|
373
|
+
spec = item.slice("id", "title", "kind", "requirements", "inputs", "outputs", "checks", "dependencies")
|
|
374
|
+
%w[requirements inputs outputs checks dependencies].each { |key| spec[key] ||= [] }
|
|
375
|
+
old = old_tasks[item["id"]]
|
|
376
|
+
candidate = (old || { "status" => "pending", "verification" => "not_run", "history" => [] }).merge(spec)
|
|
377
|
+
changed = old && spec.any? { |key, value| key != "title" && old[key] != value }
|
|
378
|
+
if changed
|
|
379
|
+
candidate["status"], candidate["verification"] = "pending", "stale"
|
|
380
|
+
candidate.delete("attempt")
|
|
381
|
+
end
|
|
382
|
+
if item["cancelled"] == true
|
|
383
|
+
if old && old["verification"] == "fail"
|
|
384
|
+
withdrawn = (old["requirements"] - permitted_removals).empty? && (old["requirements"] & new_requirements.keys).empty?
|
|
385
|
+
raise ArgumentError, "cannot cancel a failing obligation without its explicit scope withdrawal" unless withdrawn
|
|
386
|
+
end
|
|
387
|
+
candidate["status"], candidate["reason"] = "cancelled", item["reason"]
|
|
388
|
+
candidate.delete("attempt")
|
|
389
|
+
end
|
|
390
|
+
candidate
|
|
391
|
+
end
|
|
392
|
+
plan["tasks"] = new_tasks
|
|
393
|
+
validate!(root, plan)
|
|
394
|
+
old_requirements = plan.fetch("requirements", {})
|
|
395
|
+
removed = old_requirements.keys - new_requirements.keys
|
|
396
|
+
raise ArgumentError, "removing acceptance requirements needs an explicit scope decision for those clauses" unless (removed - permitted_removals).empty?
|
|
397
|
+
plan["requirements"] = new_requirements
|
|
398
|
+
plan["revision"] += 1
|
|
399
|
+
plan["changes"] << { "revision" => plan["revision"], "reason" => request.fetch("reason"), "at" => Time.now.utc.iso8601 }
|
|
400
|
+
when "claim"
|
|
401
|
+
view = snapshot(root, state)
|
|
402
|
+
raise ArgumentError, "no runnable task" unless view["action"] == "run_task"
|
|
403
|
+
task = plan["tasks"].find { |item| item["id"] == view.dig("current_step", "id") }
|
|
404
|
+
task["status"], task["attempt"] = "running", SecureRandom.uuid
|
|
405
|
+
task["started_at"] = Time.now.utc.iso8601
|
|
406
|
+
task["dispatch_pins"] = pins(root, plan, task)
|
|
407
|
+
when "report", "review_result"
|
|
408
|
+
task = plan["tasks"].find { |item| item["id"] == request["task_id"] }
|
|
409
|
+
raise ArgumentError, "stale or missing task attempt" unless task && task["status"] == "running" && task["attempt"] == request["attempt"]
|
|
410
|
+
raise ArgumentError, "verification must run through Core" if task["kind"] == "verify"
|
|
411
|
+
if task["kind"] == "review"
|
|
412
|
+
raise ArgumentError, "independent reviewer result required" unless action == "review_result" && request["runtime_review"] == true
|
|
413
|
+
raise ArgumentError, "review inputs changed" unless task["dispatch_pins"] == pins(root, plan, task)
|
|
414
|
+
report = request.fetch("report")
|
|
415
|
+
raise ArgumentError, "invalid reviewer report" unless report["findings"].is_a?(Array) && report["summary"].is_a?(String)
|
|
416
|
+
evidence = { "pins" => pins(root, plan, task), "review" => report, "task_call_id" => request.fetch("task_call_id"), "attempt" => task["attempt"] }
|
|
417
|
+
task["history"] << evidence
|
|
418
|
+
task["evidence"] = evidence
|
|
419
|
+
if report["findings"].empty?
|
|
420
|
+
task["status"], task["verification"] = "completed", "pass"
|
|
421
|
+
else
|
|
422
|
+
failure!(plan, task, evidence)
|
|
423
|
+
end
|
|
424
|
+
else
|
|
425
|
+
raise ArgumentError, "requirements changed during execution" unless task["dispatch_pins"]["requirements"] == pins(root, plan, task)["requirements"]
|
|
426
|
+
raise ArgumentError, "result summary required" if request["summary"].to_s.strip.empty?
|
|
427
|
+
raise ArgumentError, "expected output missing" unless task["outputs"].all? { |file| File.exist?(path(root, file)) }
|
|
428
|
+
# Implementation is an activity result, never an acceptance pass.
|
|
429
|
+
evidence = { "pins" => pins(root, plan, task), "summary" => request["summary"], "attempt" => task["attempt"] }
|
|
430
|
+
task["evidence"] = evidence
|
|
431
|
+
task["history"] << evidence
|
|
432
|
+
task["status"], task["verification"] = "completed", "not_run"
|
|
433
|
+
end
|
|
434
|
+
when "recover"
|
|
435
|
+
task = plan["tasks"].find { |item| item["id"] == request["task_id"] }
|
|
436
|
+
raise ArgumentError, "only an interrupted attempt may recover" unless task && %w[running checking].include?(task["status"])
|
|
437
|
+
if task["status"] == "checking" && task["runner_pid"]
|
|
438
|
+
alive = begin
|
|
439
|
+
Process.kill(0, task["runner_pid"])
|
|
440
|
+
true
|
|
441
|
+
rescue Errno::ESRCH
|
|
442
|
+
false
|
|
443
|
+
end
|
|
444
|
+
raise ArgumentError, "verification process is still running" if alive
|
|
445
|
+
end
|
|
446
|
+
raise ArgumentError, "inspect actual effects and provide a recovery reason" if request["reason"].to_s.strip.empty?
|
|
447
|
+
task["history"] << { "attempt" => task["attempt"], "interrupted" => true, "reason" => request["reason"] }
|
|
448
|
+
task["status"] = "pending"
|
|
449
|
+
task.delete("attempt")
|
|
450
|
+
when "wait"
|
|
451
|
+
task = plan["tasks"].find { |item| item["id"] == request["task_id"] }
|
|
452
|
+
raise ArgumentError, "running attempt required" unless task && task["status"] == "running" && task["attempt"] == request["attempt"]
|
|
453
|
+
raise ArgumentError, "external dependency reason required" if request["reason"].to_s.strip.empty?
|
|
454
|
+
task["status"], task["reason"] = "waiting_external", request["reason"]
|
|
455
|
+
when "resume"
|
|
456
|
+
task = plan["tasks"].find { |item| item["id"] == request["task_id"] }
|
|
457
|
+
raise ArgumentError, "external waiting task required" unless task && task["status"] == "waiting_external"
|
|
458
|
+
raise ArgumentError, "evidence that external dependency is ready required" if request["reason"].to_s.strip.empty?
|
|
459
|
+
task["history"] << { "resumed" => true, "reason" => request["reason"] }
|
|
460
|
+
task["status"] = "pending"
|
|
461
|
+
task.delete("attempt")
|
|
462
|
+
when "decision"
|
|
463
|
+
options = request.fetch("options")
|
|
464
|
+
raise ArgumentError, "provide two or three concrete options" unless options.is_a?(Array) && (2..3).include?(options.length) && options.all? { |item| item.is_a?(String) && !item.strip.empty? }
|
|
465
|
+
question = request.fetch("question")
|
|
466
|
+
removals = request.fetch("remove_requirements", [])
|
|
467
|
+
raise ArgumentError, "scope removal must name existing requirement IDs" unless removals.is_a?(Array) && (removals - plan.fetch("requirements", {}).keys).empty?
|
|
468
|
+
raise ArgumentError, "scope removal needs an explicit approving option" if removals.any? && !options.include?(request["approval_option"])
|
|
469
|
+
binding = file_pin(root, plan["document"])
|
|
470
|
+
unless plan["decisions"].any? { |item| item["question"] == question && item["document_pin"] == binding }
|
|
471
|
+
plan["decisions"].select { |item| item["question"] == question && item["status"] == "pending" }.each { |item| item["status"] = "superseded" }
|
|
472
|
+
affected = request["task_ids"] || [snapshot(root, state).dig("current_step", "id")].compact
|
|
473
|
+
raise ArgumentError, "unknown affected decision task" unless (affected - plan["tasks"].map { |task| task["id"] }).empty?
|
|
474
|
+
blocked = plan["tasks"].select { |task| affected.include?(task["id"]) && !%w[completed cancelled].include?(task["status"]) }
|
|
475
|
+
prior = blocked.to_h { |task| [task["id"], task["status"] == "waiting_decision" ? "pending" : task["status"]] }
|
|
476
|
+
blocked.each { |task| task["status"] = "waiting_decision" }
|
|
477
|
+
plan["decisions"] << { "id" => SecureRandom.uuid, "question" => question, "options" => options, "status" => "pending", "document_pin" => binding, "requirements_pin" => digest(plan.fetch("requirements", {})), "blocked_tasks" => prior, "remove_requirements" => removals, "approval_option" => request["approval_option"] }
|
|
478
|
+
end
|
|
479
|
+
when "decide"
|
|
480
|
+
raise ArgumentError, "user response must come from runtime UI" unless request["runtime_decision"] == true
|
|
481
|
+
decision = plan["decisions"].find { |item| item["id"] == request["decision_id"] }
|
|
482
|
+
raise ArgumentError, "decision expired" unless decision && decision["status"] == "pending" && decision["document_pin"] == file_pin(root, plan["document"])
|
|
483
|
+
raise ArgumentError, "invalid option" unless decision["options"].include?(request["choice"])
|
|
484
|
+
decision["status"], decision["choice"] = "answered", request["choice"]
|
|
485
|
+
decision.fetch("blocked_tasks", {}).each do |id, prior|
|
|
486
|
+
task = plan["tasks"].find { |item| item["id"] == id }
|
|
487
|
+
still_blocked = plan["decisions"].any? { |item| item["status"] == "pending" && item.fetch("blocked_tasks", {}).key?(id) }
|
|
488
|
+
task["status"] = prior if task && task["status"] == "waiting_decision" && !still_blocked
|
|
489
|
+
end
|
|
490
|
+
else
|
|
491
|
+
raise ArgumentError, "unknown dynamic plan action"
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
if __FILE__ == $PROGRAM_NAME
|
|
498
|
+
begin
|
|
499
|
+
puts JSON.generate(DynamicPlan.dispatch(Dir.pwd, JSON.parse(STDIN.read)))
|
|
500
|
+
rescue StandardError => error
|
|
501
|
+
puts JSON.generate({ "ok" => false, "reason" => error.message })
|
|
502
|
+
exit 1
|
|
503
|
+
end
|
|
504
|
+
end
|
|
@@ -7,6 +7,7 @@ require "yaml"
|
|
|
7
7
|
require_relative "validate_state"
|
|
8
8
|
require_relative "checkpoint_meta"
|
|
9
9
|
require_relative "gate_policy"
|
|
10
|
+
require_relative "dynamic_plan"
|
|
10
11
|
|
|
11
12
|
options = { json: false, state: nil }
|
|
12
13
|
OptionParser.new do |opts|
|
|
@@ -16,6 +17,11 @@ OptionParser.new do |opts|
|
|
|
16
17
|
end.parse!
|
|
17
18
|
|
|
18
19
|
root = Dir.pwd
|
|
20
|
+
dynamic_state = DynamicPlan.read_state(root)
|
|
21
|
+
if dynamic_state && dynamic_state["current_flow"] == "dynamic"
|
|
22
|
+
puts JSON.pretty_generate(DynamicPlan.snapshot(root, dynamic_state))
|
|
23
|
+
exit 0
|
|
24
|
+
end
|
|
19
25
|
state_path = options[:state] ? File.expand_path(options[:state], root) : File.join(root, ".devflow/planning", "STATE.md")
|
|
20
26
|
|
|
21
27
|
def fail_result(reason, details = {})
|
|
@@ -69,6 +69,10 @@ project_lock = DevflowControlIO.acquire_project_lock(root) if options[:write]
|
|
|
69
69
|
begin
|
|
70
70
|
state_path, = DevflowControlIO.resolve_control_path(root, options[:state], :state)
|
|
71
71
|
state = load_state(state_path)
|
|
72
|
+
if state["current_flow"] == "dynamic"
|
|
73
|
+
puts JSON.generate({ "ok" => true, "written" => false, "mode" => "dynamic", "reason" => "task evidence is maintained in STATE; no per-skill artifact registry required" })
|
|
74
|
+
exit 0
|
|
75
|
+
end
|
|
72
76
|
ownership = PhaseOwnership.validate_state(root: root, state: state)
|
|
73
77
|
unless ownership["ok"]
|
|
74
78
|
puts JSON.pretty_generate(ownership.merge("written" => false))
|
|
@@ -60,6 +60,15 @@ module WorkflowStateValidator
|
|
|
60
60
|
issues = []
|
|
61
61
|
|
|
62
62
|
state = state_override || load_state(state_path, issues)
|
|
63
|
+
if state && state["current_flow"] == "dynamic"
|
|
64
|
+
require_relative "dynamic_plan"
|
|
65
|
+
begin
|
|
66
|
+
DynamicPlan.validate!(root, state.fetch("plan"))
|
|
67
|
+
rescue ArgumentError, KeyError => error
|
|
68
|
+
issues << issue("error", "invalid_dynamic_plan", error.message)
|
|
69
|
+
end
|
|
70
|
+
return result(issues.empty?, state_path, nil, nil, issues)
|
|
71
|
+
end
|
|
63
72
|
return result(false, state_path, nil, nil, issues) unless state
|
|
64
73
|
|
|
65
74
|
REQUIRED_STATE_FIELDS.each do |field|
|
|
@@ -3,6 +3,9 @@ name: workflow-pipeline
|
|
|
3
3
|
description: Use when running or resuming the full .devflow/planning delivery pipeline from requirements through implementation, frozen acceptance tests, and Devflow release closeout.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
Dynamic plan mode: when Core reports `mode: dynamic`, use `workflow-dynamic` as the execution protocol. The selected task supplies scope, dependencies, required outputs and verification. Use this skill's relevant technical guidance, but do not create legacy stage documents/meta pairs, require fixed checkpoints, or advance legacy STATE. Return activity/evidence to `devflow_plan`; only Core records verification.
|
|
7
|
+
|
|
8
|
+
|
|
6
9
|
## 默认软门禁策略
|
|
7
10
|
|
|
8
11
|
工作流硬门禁已移除。下文及各步骤中的产物、索引、合同、审查、质量、恢复和 CP 审批条件均为诊断建议:保留失败证据和缺项,允许继续执行。用户说“下一步”即可推进,无需额外批准弹窗;不得把继续执行记录成人工批准,也不得把失败、未执行的测试改写为通过。真实读写/执行失败、无法解析的状态和并发版本冲突须如实报告。硬门禁及其恢复开关已移除。
|
|
@@ -3,6 +3,9 @@ name: workflow-bug-fix
|
|
|
3
3
|
description: "Use when a bug must be traced through workflow artifacts, root-caused, fixed, verified, and captured as a lesson when needed."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
Dynamic plan mode: when Core reports `mode: dynamic`, use `workflow-dynamic` as the execution protocol. The selected task supplies scope, dependencies, required outputs and verification. Use this skill's relevant technical guidance, but do not create legacy stage documents/meta pairs, require fixed checkpoints, or advance legacy STATE. Return activity/evidence to `devflow_plan`; only Core records verification.
|
|
7
|
+
|
|
8
|
+
|
|
6
9
|
门禁策略:默认软门禁,门禁行为以本段为准。本文及引用材料中的前置检查、审查、质量、完成条件和 CP 确认用于诊断;未满足时记录缺项、风险与后续处理,继续可执行的工作,不自动阻断阶段推进。保留真实的 failed/blocked/未审批状态,不把继续执行写成通过或人工批准。硬门禁及其恢复开关已移除。
|
|
7
10
|
|
|
8
11
|
## Legacy Command Metadata
|
|
@@ -3,6 +3,9 @@ name: workflow-execute-phase
|
|
|
3
3
|
description: "Use when a planned .devflow/planning phase needs implementation, dependency-aware task execution, progress tracking, and verification evidence."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
Dynamic plan mode: when Core reports `mode: dynamic`, use `workflow-dynamic` as the execution protocol. The selected task supplies scope, dependencies, required outputs and verification. Use this skill's relevant technical guidance, but do not create legacy stage documents/meta pairs, require fixed checkpoints, or advance legacy STATE. Return activity/evidence to `devflow_plan`; only Core records verification.
|
|
7
|
+
|
|
8
|
+
|
|
6
9
|
门禁策略:默认软门禁,门禁行为以本段为准。本文及引用材料中的前置检查、审查、质量、完成条件和 CP 确认用于诊断;未满足时记录缺项、风险与后续处理,继续可执行的工作,不自动阻断阶段推进。保留真实的 failed/blocked/未审批状态,不把继续执行写成通过或人工批准。硬门禁及其恢复开关已移除。
|
|
7
10
|
|
|
8
11
|
## Legacy Command Metadata
|
|
@@ -13,6 +13,8 @@ autoloadSkills:
|
|
|
13
13
|
- code-review
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
+
For a Core dynamic-plan review (task kind `review`, mode `dynamic`), read the requirement main document, production source, tests and Core verification evidence directly. Use only applicable review surfaces. Return strict JSON `{ "summary": "evidence inspected and conclusion", "findings": [] }`; findings must include requirement ID, file/line evidence, triggering scenario, impact and recheck. Missing technical evidence is a finding to collect or repair, not a request for the user to manufacture metadata. An empty findings array requires evidence supporting every linked acceptance requirement. The legacy frozen-test-case and REVIEW.md contract below applies only to legacy fixed workflows. Remain read-only.
|
|
17
|
+
|
|
16
18
|
Review the actual diff and its affected call paths. Prefer concrete defects over style commentary. Treat the parent agent's task summary as scope guidance, never as acceptance authority: verify each expected behavior against confirmed Core requirements/contracts or an explicit human decision, then independently read the relevant source and test evidence at fixed revisions. For each finding provide severity, authoritative expectation reference, source file and location, triggering scenario, impact, evidence status, and a minimal fix direction. Verify whether tests cover the failure. If an essential business fact is absent or conflicting, stop and return the exact question requiring human confirmation; do not infer a clean result.
|
|
17
19
|
|
|
18
20
|
Do not edit code, fix findings, or mutate workflow state. If no actionable finding remains, say so and list the validation evidence inspected.
|
|
@@ -3,6 +3,9 @@ name: code-review
|
|
|
3
3
|
description: "Use when code needs review for project conventions, spec compliance, behavioral risks, missing tests, change-list completion criteria verification, or post-refactor validation."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
Dynamic plan mode: when Core reports `mode: dynamic`, use `workflow-dynamic` as the execution protocol. The selected task supplies scope, dependencies, required outputs and verification. Use this skill's relevant technical guidance, but do not create legacy stage documents/meta pairs, require fixed checkpoints, or advance legacy STATE. Return activity/evidence to `devflow_plan`; only Core records verification.
|
|
7
|
+
|
|
8
|
+
|
|
6
9
|
门禁策略:默认软门禁,门禁行为以本段为准。本文及引用材料中的前置检查、审查、质量、完成条件和 CP 确认用于诊断;未满足时记录缺项、风险与后续处理,继续可执行的工作,不自动阻断阶段推进。保留真实的 failed/blocked/未审批状态,不把继续执行写成通过或人工批准。硬门禁及其恢复开关已移除。
|
|
7
10
|
|
|
8
11
|
# 代码规范审查技能
|
|
@@ -3,6 +3,9 @@ name: workflow-run-acceptance-tests
|
|
|
3
3
|
description: "Execute the frozen TC acceptance suite after code review. Use the requirement baseline and frozen TC nodes for test intent; use current code only to bind execution adapters."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
Dynamic plan mode: when Core reports `mode: dynamic`, use `workflow-dynamic` as the execution protocol. The selected task supplies scope, dependencies, required outputs and verification. Use this skill's relevant technical guidance, but do not create legacy stage documents/meta pairs, require fixed checkpoints, or advance legacy STATE. Return activity/evidence to `devflow_plan`; only Core records verification.
|
|
7
|
+
|
|
8
|
+
|
|
6
9
|
门禁策略:默认软门禁,门禁行为以本段为准。本文及引用材料中的前置检查、审查、质量、完成条件和 CP 确认用于诊断;未满足时记录缺项、风险与后续处理,继续可执行的工作,不自动阻断阶段推进。保留真实的 failed/blocked/未审批状态,不把继续执行写成通过或人工批准。硬门禁及其恢复开关已移除。
|
|
7
10
|
|
|
8
11
|
# 冻结验收测试执行
|
|
@@ -3,6 +3,9 @@ name: workflow-requirement-triage
|
|
|
3
3
|
description: "Use as the lightweight first step for a new request, bug report, PRD, or feature idea. Classify the request, check shallow duplicate and prior out-of-scope/rejected decisions, then route a viable request into workflow-wayfinder; do not perform deep requirement validation or route directly to baseline."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
Dynamic plan mode: when Core reports `mode: dynamic`, use `workflow-dynamic` as the execution protocol. The selected task supplies scope, dependencies, required outputs and verification. Use this skill's relevant technical guidance, but do not create legacy stage documents/meta pairs, require fixed checkpoints, or advance legacy STATE. Return activity/evidence to `devflow_plan`; only Core records verification.
|
|
7
|
+
|
|
8
|
+
|
|
6
9
|
门禁策略:默认软门禁,门禁行为以本段为准。本文及引用材料中的前置检查、审查、质量、完成条件和 CP 确认用于诊断;未满足时记录缺项、风险与后续处理,继续可执行的工作,不自动阻断阶段推进。保留真实的 failed/blocked/未审批状态,不把继续执行写成通过或人工批准。硬门禁及其恢复开关已移除。
|
|
7
10
|
|
|
8
11
|
# Workflow: Requirement Triage(需求入口分诊)
|
package/src/constants.ts
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Presentation only. The revisioned Core plan owns every execution fact. */
|
|
2
|
+
export function renderDynamicWorkflow(snapshot: Record<string, unknown>, showHistory = false): string {
|
|
3
|
+
const tasks = (snapshot.workflow_steps ?? []) as Array<Record<string, unknown>>;
|
|
4
|
+
const marks: Record<string, string> = { completed: "✓", cancelled: "—", pending: "○", running: "●", checking: "●", failed: "✗" };
|
|
5
|
+
const labels: Record<string, string> = { cancelled: "已取消", waiting_decision: "待确认", waiting_external: "等待外部条件", failed: "未通过", checking: "验证中" };
|
|
6
|
+
const visible = !showHistory && tasks.length > 8 ? tasks.filter(task => !["completed", "cancelled"].includes(String(task.status))) : tasks;
|
|
7
|
+
const lines = visible.map(task => `${marks[String(task.status)] ?? "○"} ${task.label}${labels[String(task.status)] ? `(${labels[String(task.status)]})` : ""}${task.verification === "stale" ? "(需要重新验证)" : ""}${task.reason ? `:${task.reason}` : ""}`);
|
|
8
|
+
if (visible.length < tasks.length) lines.push(`已折叠 ${tasks.length - visible.length} 项历史记录,可在 /devflow control 查看全部步骤。`);
|
|
9
|
+
const decisions = (snapshot.decisions ?? []) as Array<Record<string, unknown>>;
|
|
10
|
+
for (const decision of decisions.filter(item => item.status === "pending")) lines.push(`待确认:${decision.question}`);
|
|
11
|
+
return [String(snapshot.current_feature ?? "需求工作流"), `需求说明:${snapshot.document}`, ...lines,
|
|
12
|
+
snapshot.delivery === "ready" ? "验收证据齐备,可以交付。" : snapshot.delivery === "cancelled" ? "需求已取消,历史结果已保留。" : snapshot.reason ? `需要调整计划:${snapshot.reason}` : "",
|
|
13
|
+
].filter(Boolean).join("\n");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function dynamicWorkflowDirective(snapshot: Record<string, unknown>): string {
|
|
17
|
+
return [
|
|
18
|
+
"<devflow-dynamic-workflow>",
|
|
19
|
+
"读取 skill://workflow-dynamic。当前是动态计划模式,按需求选择工作,不套用固定阶段和逐步审批。",
|
|
20
|
+
"Core STATE 是计划与结果的唯一写入入口。通过 devflow_plan inspect/revise/claim/report/verify 管理任务。",
|
|
21
|
+
"先读需求主文档和下列真实状态。没有计划时,核实项目后在主文档验收要求下维护稳定的 ### R-1: 描述 条目,提交中文任务计划。",
|
|
22
|
+
"已有计划时,执行 Core 选中的任务,读取它的精确 skill。缺项先补证据,技术失败自动修复。完成后继续可执行任务,不逐步询问用户。",
|
|
23
|
+
"只有真实业务取舍或未获授权的操作才调用 devflow_plan decision,给出 2–3 个选项,首项为推荐并说明影响。",
|
|
24
|
+
"已有 running/checking 任务先检查实际产物与执行状态;不得盲目重复外部操作。确认旧执行已结束才 recover。",
|
|
25
|
+
"不生成无关前端任务、空白方案或每技能一份报告。修改需求主文档;源码、测试、API 契约等必要产物照常维护。",
|
|
26
|
+
"验证必须用 verify 运行声明的命令;review 必须派发 devflow-code-reviewer 独立只读 task,返回 JSON {summary:string,findings:array},再用 review_result 提交捕获的原始结果。",
|
|
27
|
+
"Agent 回合结束不等于任务完成;缺失证据、失败、取消与通过必须保持区别。",
|
|
28
|
+
JSON.stringify(snapshot),
|
|
29
|
+
"</devflow-dynamic-workflow>",
|
|
30
|
+
].join("\n");
|
|
31
|
+
}
|
package/templates/omp/AGENTS.md
CHANGED
|
@@ -4,14 +4,15 @@ This repository uses an OMP-native, project-scoped software delivery runtime.
|
|
|
4
4
|
|
|
5
5
|
## Gate policy
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
New requirements use dynamic plans: continue dependency-ready work and automatically repair technical findings. Delivery requires current verification and applicable independent review evidence. Preserve failed, stale, waiting and cancelled results; none means passed. Business choices use concise runtime option cards. Actual operation errors, unreadable control state and revision conflicts reject the affected operation. Existing fixed workflows retain their advisory checkpoint policy; do not apply their fixed stage/document requirements to dynamic tasks.
|
|
8
8
|
|
|
9
9
|
## Control plane
|
|
10
10
|
|
|
11
11
|
- Start only through `/devflow start <feature>|<phase>` after checking `/devflow doctor` and reporting its diagnostics; resume with `/devflow resume`.
|
|
12
12
|
- Inspect workflow state with `devflow_state`; do not infer state from chat history.
|
|
13
13
|
- Initialize delivery with `devflow_bootstrap`.
|
|
14
|
-
-
|
|
14
|
+
- When the inspector reports `mode: dynamic`, load `workflow-dynamic` and use `devflow_plan` for task proposals, execution results and verification. The requirement document owns intent; Core owns progress and observed results. Do not generate irrelevant UI tasks or per-skill Markdown/meta pairs.
|
|
15
|
+
- For existing fixed workflows, mutate checkpoints and steps only with `devflow_transition`.
|
|
15
16
|
- To abandon an active feature, use the runtime-confirmed `devflow_transition`
|
|
16
17
|
`cancel-feature` operation or `/devflow abandon`; never edit control files.
|
|
17
18
|
- Never directly edit `.devflow/planning/STATE.md`, `.devflow/planning/.next-intent`, `.devflow/planning/workflows/`, or `.devflow/planning/artifacts/index.yaml`.
|
|
@@ -39,4 +40,4 @@ An index accelerates discovery; it never overrides source code, executable tests
|
|
|
39
40
|
- A model assertion is not validation evidence.
|
|
40
41
|
- Record exact commands and results for tests, builds, migrations, and acceptance checks.
|
|
41
42
|
- When context reconciliation is requested, follow `skill://workflow-context-sync`, validate the required level, then acknowledge through `devflow_context_sync`.
|
|
42
|
-
-
|
|
43
|
+
- Legacy soft checkpoints continue with `continued_with_warning`. Dynamic plans continue from actual task results and never treat an Agent turn ending as successful completion. Never synthesize human approval.
|