@guanlin4924/devflow-pi 0.1.98 → 0.1.100
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 +635 -263
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +6 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_claim_validate.py +3 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_cross_model_check.py +3 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_traceability.py +31 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_v1.py +6 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/environment_profile.py +11 -2
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_workflow.rb +3 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_run.rb +14 -5
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/review_preflight.py +8 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/session_rollover.rb +12 -5
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/startup_preflight.py +14 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +6 -2
- package/plugins/devflow-intel/extensions/devflow-intel.ts +25 -3
- package/plugins/devflow-quality/runtime/playwright/explore-host.mjs +55 -28
- package/plugins/devflow-quality/runtime/playwright/runner.spec.mjs +41 -14
- package/plugins/devflow-quality/runtime/playwright/runtime-manifest.json +2 -2
- package/src/constants.ts +1 -1
- package/src/mcp/core.ts +23 -24
- package/src/mcp/server.ts +11 -12
- package/src/project-context.ts +9 -1
- package/src/project-initializer.ts +4 -4
- package/src/session-rollover.ts +10 -0
- package/src/workflow-diagnosis.ts +423 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
3
3
|
import { appendFile, mkdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
4
|
+
import { homedir, tmpdir } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
@@ -16,7 +16,21 @@ import { loadProjectContext } from "../../../src/project-context.ts";
|
|
|
16
16
|
import { runProjectInitialization } from "../../../src/project-initializer.ts";
|
|
17
17
|
import { projectControlDatabasePath } from "../../../src/paths.ts";
|
|
18
18
|
import { generateProjectDocuments } from "../../../src/project-documents.ts";
|
|
19
|
+
import { preparedRolloverRevision } from "../../../src/session-rollover.ts";
|
|
19
20
|
import { describeStartupIssue, renderStartupBriefing } from "../../../src/startup-briefing.ts";
|
|
21
|
+
import { PRODUCT_VERSION } from "../../../src/constants.ts";
|
|
22
|
+
import {
|
|
23
|
+
FIX_LABELS,
|
|
24
|
+
buildHelpBundle,
|
|
25
|
+
classifyNextStepPhrase,
|
|
26
|
+
describeControlFailure,
|
|
27
|
+
diagnoseWorkflow,
|
|
28
|
+
renderDiagnosis,
|
|
29
|
+
summarizeToolFailure,
|
|
30
|
+
type FixAction,
|
|
31
|
+
type ToolFailureRecord,
|
|
32
|
+
type WorkflowDiagnosis,
|
|
33
|
+
} from "../../../src/workflow-diagnosis.ts";
|
|
20
34
|
import { exportInitializationSnapshot, importLegacyInitialization, switchInitializationAuthority } from "../../../src/project-migration.ts";
|
|
21
35
|
import { migrateLegacyProjectLayout } from "../../../src/project-layout.ts";
|
|
22
36
|
import {
|
|
@@ -37,6 +51,15 @@ import {
|
|
|
37
51
|
|
|
38
52
|
export type DevflowIntent = "idle" | "explain-state" | "project-init" | "requirement-draft" | "workflow-execute";
|
|
39
53
|
|
|
54
|
+
/** Used only after the runtime receives an explicit user checkpoint approval. */
|
|
55
|
+
export function wireframeApprovalArgs(feature: string, revision: number): string[] {
|
|
56
|
+
return [
|
|
57
|
+
"--action", "approve", "--feature", feature,
|
|
58
|
+
"--expected-revision", String(revision), "--approved-by", "user",
|
|
59
|
+
"--all-screens", "--runtime-confirmed", "--write", "--json",
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
40
63
|
export type ControlPhase = "idle" | "planning" | "awaiting-approval" | "waiting-user" | "executing" | "blocked";
|
|
41
64
|
|
|
42
65
|
export type SessionControlState = {
|
|
@@ -581,6 +604,10 @@ export function isVirtualStateTransitionResource(toolName: string, input: unknow
|
|
|
581
604
|
return [record.path, record.uri, record.resource].some(value => value === "xd://devflow_transition");
|
|
582
605
|
}
|
|
583
606
|
|
|
607
|
+
export const OUTSIDE_STEP_WRITE_REASON =
|
|
608
|
+
"当前不在受控的工作流步骤中:在这里写入 .devflow/planning/requirements 或 features 下的需求产物不会被工作流采纳,已拒绝。" +
|
|
609
|
+
"不要改用其他方式写入这些文件。请停止写入,把已有结论直接告诉用户,并请用户说「下一步」(还没有需求时先创建需求),由 Devflow 启动对应步骤后再写。";
|
|
610
|
+
|
|
584
611
|
export function planningToolBlockReason(toolName: string): string {
|
|
585
612
|
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.`;
|
|
586
613
|
}
|
|
@@ -756,6 +783,9 @@ type DoctorContext = {
|
|
|
756
783
|
};
|
|
757
784
|
};
|
|
758
785
|
|
|
786
|
+
type DevflowCommandHandler = Parameters<ExtensionAPI["registerCommand"]>[1]["handler"];
|
|
787
|
+
type DevflowCommandContext = Parameters<DevflowCommandHandler>[1];
|
|
788
|
+
|
|
759
789
|
const CONTROL_PATHS = [
|
|
760
790
|
".devflow/planning/STATE.md",
|
|
761
791
|
".devflow/planning/.next-intent",
|
|
@@ -1131,8 +1161,31 @@ async function runProcess(
|
|
|
1131
1161
|
};
|
|
1132
1162
|
}
|
|
1133
1163
|
|
|
1164
|
+
/** Raw failures kept in memory for the user's help bundle; never sent to the model. */
|
|
1165
|
+
const recentToolFailures: ToolFailureRecord[] = [];
|
|
1166
|
+
|
|
1167
|
+
function recordToolFailure(tool: string, result: ProcessResult, summary: string): void {
|
|
1168
|
+
recentToolFailures.push({
|
|
1169
|
+
at: new Date().toISOString(),
|
|
1170
|
+
tool,
|
|
1171
|
+
exitCode: result.exitCode,
|
|
1172
|
+
summary,
|
|
1173
|
+
raw: (result.stderr || result.stdout).slice(0, 4000),
|
|
1174
|
+
});
|
|
1175
|
+
if (recentToolFailures.length > 20) recentToolFailures.splice(0, recentToolFailures.length - 20);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1134
1178
|
function renderResult(result: ProcessResult, details: Record<string, unknown> = {}) {
|
|
1135
|
-
|
|
1179
|
+
// JSON output passes through unchanged; plain-text failures and empty output
|
|
1180
|
+
// become a Chinese summary instead of a raw stack trace.
|
|
1181
|
+
const summary = summarizeToolFailure(result);
|
|
1182
|
+
if (summary && !result.ok) recordToolFailure(String(details.action ?? "devflow"), result, summary);
|
|
1183
|
+
const raw = (result.stderr || result.stdout).trim();
|
|
1184
|
+
// Stack traces collapse to the summary; other plain-text failures may list
|
|
1185
|
+
// several problems, so the model still sees them in full.
|
|
1186
|
+
const keepRaw = summary !== null && !result.ok && raw !== "" && !summary.includes(raw)
|
|
1187
|
+
&& !/:\d+:in [`']|Traceback \(most recent call last\)/u.test(raw);
|
|
1188
|
+
const text = summary === null ? (result.stdout || result.stderr) : keepRaw ? `${summary}\n\n原始信息:\n${raw.slice(0, 2000)}` : summary;
|
|
1136
1189
|
return {
|
|
1137
1190
|
content: [{ type: "text" as const, text }],
|
|
1138
1191
|
details: { ...details, exitCode: result.exitCode, stderr: result.stderr || undefined },
|
|
@@ -1261,7 +1314,7 @@ function selectedWorkflowStep(snapshot: Record<string, unknown>): Record<string,
|
|
|
1261
1314
|
* completed step. This predicate lets the runtime re-arm approval handling
|
|
1262
1315
|
* from the Core-owned snapshot without granting approval by itself.
|
|
1263
1316
|
*/
|
|
1264
|
-
function isReviewableWorkflowSnapshot(snapshot: Record<string, unknown
|
|
1317
|
+
function isReviewableWorkflowSnapshot(snapshot: Record<string, unknown>, options: { strictRegistry?: boolean } = {}): boolean {
|
|
1265
1318
|
const status = snapshot.status;
|
|
1266
1319
|
if (status !== "waiting_checkpoint" && status !== "not_started" && status !== "in_progress") return false;
|
|
1267
1320
|
const step = snapshot.current_step;
|
|
@@ -1284,9 +1337,15 @@ function isReviewableWorkflowSnapshot(snapshot: Record<string, unknown>): boolea
|
|
|
1284
1337
|
// Core has already validated and registered the artifact. Treat an
|
|
1285
1338
|
// explicit false as invalid, but use exists + artifact_status as the
|
|
1286
1339
|
// durable fallback when the field is absent.
|
|
1340
|
+
// sync_artifacts.rb keeps every output of an unfinished non-checkpoint
|
|
1341
|
+
// step at `planned`, so `planned` must not hide an output the step has
|
|
1342
|
+
// already written. advance_state.rb re-validates outputs on transition.
|
|
1343
|
+
// Resume disposition stays strict so an existing file never lets a step
|
|
1344
|
+
// start without contract activation.
|
|
1345
|
+
const rejected = options.strictRegistry ? ["failed", "blocked", "planned"] : ["failed", "blocked"];
|
|
1287
1346
|
return record.exists === true
|
|
1288
1347
|
&& record.content_valid !== false
|
|
1289
|
-
&& !
|
|
1348
|
+
&& !rejected.includes(String(record.artifact_status ?? ""));
|
|
1290
1349
|
});
|
|
1291
1350
|
}
|
|
1292
1351
|
|
|
@@ -1314,13 +1373,13 @@ export function workflowResumeDisposition(snapshot: Record<string, unknown>): Wo
|
|
|
1314
1373
|
if (!feature || !stepId || typeof revision !== "number" || !Number.isInteger(revision)) return "idle";
|
|
1315
1374
|
|
|
1316
1375
|
if (action === "await_user_confirmation") {
|
|
1317
|
-
return isReviewableWorkflowSnapshot(snapshot) ? "review" : "blocked";
|
|
1376
|
+
return isReviewableWorkflowSnapshot(snapshot, { strictRegistry: true }) ? "review" : "blocked";
|
|
1318
1377
|
}
|
|
1319
1378
|
// Parallel windows must be reconstructed by the Runtime even if a stale
|
|
1320
1379
|
// join artifact happens to exist; branch lifecycle is the join authority.
|
|
1321
1380
|
if (action === "run_parallel_window" || action === "route_failure_branch") return "execute";
|
|
1322
1381
|
if (action === "run_next_step") {
|
|
1323
|
-
return isReviewableWorkflowSnapshot(snapshot) ? "review" : "execute";
|
|
1382
|
+
return isReviewableWorkflowSnapshot(snapshot, { strictRegistry: true }) ? "review" : "execute";
|
|
1324
1383
|
}
|
|
1325
1384
|
return "blocked";
|
|
1326
1385
|
}
|
|
@@ -2613,7 +2672,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2613
2672
|
const details = issues.length > 0
|
|
2614
2673
|
? issues.map(issue => `- ${describeStartupIssue(issue && typeof issue === "object" ? issue as Record<string, unknown> : {})}`).join("\n")
|
|
2615
2674
|
: "- 开工预检输出无法解析";
|
|
2616
|
-
ctx.ui.notify(`开工预检未通过:\n${details}
|
|
2675
|
+
ctx.ui.notify(`开工预检未通过:\n${details}\n可以说「下一步」,由 Devflow 诊断并给出处理办法。`, "error");
|
|
2617
2676
|
return { ok: false };
|
|
2618
2677
|
}
|
|
2619
2678
|
const facts = payload.facts && typeof payload.facts === "object" ? payload.facts as Record<string, unknown> : {};
|
|
@@ -2675,6 +2734,9 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2675
2734
|
}
|
|
2676
2735
|
|
|
2677
2736
|
let activeWorkflowTurn = false;
|
|
2737
|
+
// Set once the /devflow command is defined; lets the input handler and the
|
|
2738
|
+
// next-step entry reuse existing subcommands (start, abandon).
|
|
2739
|
+
let devflowCommandRef: ((args: string, ctx: DevflowCommandContext) => Promise<void>) | undefined;
|
|
2678
2740
|
let parallelRunActive = false;
|
|
2679
2741
|
let parallelContinuationQueued = false;
|
|
2680
2742
|
let parallelDispatchSucceededThisTurn = false;
|
|
@@ -2880,6 +2942,17 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2880
2942
|
ctx.ui.notify("已保留当前步骤,未推进工作流。", "info");
|
|
2881
2943
|
return true;
|
|
2882
2944
|
}
|
|
2945
|
+
if (checkpoint && stepId === "wireframe-review") {
|
|
2946
|
+
const feature = inspected.snapshot.current_feature;
|
|
2947
|
+
if (typeof feature !== "string") return false;
|
|
2948
|
+
const approval = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/wireframe_commit.rb",
|
|
2949
|
+
wireframeApprovalArgs(feature, revision));
|
|
2950
|
+
if (!controlPlaneSucceeded(approval)) {
|
|
2951
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "原型确认写入失败" });
|
|
2952
|
+
ctx.ui.notify(`原型确认未写入:${approval.stderr || approval.stdout}`, "error");
|
|
2953
|
+
return true;
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2883
2956
|
const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/advance_state.rb", [
|
|
2884
2957
|
"--write", "--json", "--expected-revision", String(revision),
|
|
2885
2958
|
...(checkpoint ? ["--confirm-checkpoint", "--runtime-confirmed"] : ["--complete-step", stepId]),
|
|
@@ -2887,7 +2960,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2887
2960
|
]);
|
|
2888
2961
|
if (!result.ok || !controlPlaneSucceeded(result)) {
|
|
2889
2962
|
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "推进校验失败" });
|
|
2890
|
-
ctx.ui.notify(`未推进:${result
|
|
2963
|
+
ctx.ui.notify(`未推进:${describeControlFailure(result)}\n可以说「下一步」重新诊断,或选择重新执行这一步来修复产物。`, "error");
|
|
2891
2964
|
return true;
|
|
2892
2965
|
}
|
|
2893
2966
|
const after = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
@@ -2909,6 +2982,257 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2909
2982
|
return true;
|
|
2910
2983
|
}
|
|
2911
2984
|
|
|
2985
|
+
async function diagnoseCurrentWorkflow(cwd: string, options: { skipWorkflowUpgrade?: boolean } = {}): Promise<{
|
|
2986
|
+
snapshot: Record<string, unknown> | null;
|
|
2987
|
+
inspectError?: string;
|
|
2988
|
+
rolloverState: Record<string, unknown> | null;
|
|
2989
|
+
diagnosis: WorkflowDiagnosis;
|
|
2990
|
+
}> {
|
|
2991
|
+
const inspected = await inspectWorkflowWithRegistryRecovery(cwd);
|
|
2992
|
+
// A missing STATE.md is reported as structured JSON, not always with exit 0.
|
|
2993
|
+
const snapshot = inspected.snapshot ?? parseJsonOutput(inspected.result);
|
|
2994
|
+
const inspectError = snapshot ? undefined : (inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON");
|
|
2995
|
+
const rolloverState = snapshot?.current_feature
|
|
2996
|
+
? parseJsonResult(await runRuby(cwd, "workflow-orchestrator/scripts/session_rollover.rb", ["inspect", "--json"]))
|
|
2997
|
+
: null;
|
|
2998
|
+
const rollover = rolloverState?.rollover as Record<string, unknown> | undefined;
|
|
2999
|
+
let workflowUpgrade = { outdated: false, blocked: false, paths: [] as string[] };
|
|
3000
|
+
if (typeof snapshot?.current_feature === "string" && !options.skipWorkflowUpgrade) {
|
|
3001
|
+
try {
|
|
3002
|
+
const upgradeRun = await runRuby(
|
|
3003
|
+
cwd,
|
|
3004
|
+
"workflow-orchestrator/scripts/init_workflow.rb",
|
|
3005
|
+
["--force", "--json"],
|
|
3006
|
+
);
|
|
3007
|
+
if (!upgradeRun.ok) throw new Error("workflow upgrade dry-run failed");
|
|
3008
|
+
const upgradeResult = parseJsonResult(upgradeRun);
|
|
3009
|
+
if (!upgradeResult) throw new Error("workflow upgrade dry-run returned invalid JSON");
|
|
3010
|
+
// init_workflow.rb returns { changes: [{ path, exists,
|
|
3011
|
+
// same_as_template, action, ... }] }; a dry-run drift has action
|
|
3012
|
+
// "would_skip_existing", so drift is derived from the boolean fields.
|
|
3013
|
+
const changes = Array.isArray(upgradeResult?.changes) ? upgradeResult.changes : [];
|
|
3014
|
+
const records = changes.filter((change): change is Record<string, unknown> =>
|
|
3015
|
+
Boolean(change && typeof change === "object" && !Array.isArray(change))
|
|
3016
|
+
);
|
|
3017
|
+
const paths = records.flatMap(change =>
|
|
3018
|
+
change.exists === true && change.same_as_template !== true && typeof change.path === "string"
|
|
3019
|
+
? [change.path]
|
|
3020
|
+
: []
|
|
3021
|
+
);
|
|
3022
|
+
workflowUpgrade = {
|
|
3023
|
+
outdated: paths.length > 0,
|
|
3024
|
+
blocked: records.some(change =>
|
|
3025
|
+
change.action === "blocked_state_mismatch" || change.action === "would_overwrite_blocked"
|
|
3026
|
+
),
|
|
3027
|
+
paths,
|
|
3028
|
+
};
|
|
3029
|
+
} catch {
|
|
3030
|
+
workflowUpgrade = { outdated: false, blocked: false, paths: [] };
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
const diagnosis = diagnoseWorkflow({
|
|
3034
|
+
inspect: { ok: Boolean(snapshot), error: inspectError },
|
|
3035
|
+
snapshot,
|
|
3036
|
+
disposition: snapshot ? workflowResumeDisposition(snapshot) : "idle",
|
|
3037
|
+
outputsReady: snapshot ? isReviewableWorkflowSnapshot(snapshot) : false,
|
|
3038
|
+
rolloverPrepared: Boolean(rolloverState && typeof rolloverState.revision === "number"
|
|
3039
|
+
&& rollover?.status === "prepared" && typeof rollover.id === "string"),
|
|
3040
|
+
workflowUpgrade,
|
|
3041
|
+
});
|
|
3042
|
+
return { snapshot, inspectError, rolloverState, diagnosis };
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
async function writeHelpBundle(
|
|
3046
|
+
ctx: DevflowCommandContext,
|
|
3047
|
+
current: Awaited<ReturnType<typeof diagnoseCurrentWorkflow>>,
|
|
3048
|
+
): Promise<string> {
|
|
3049
|
+
const report = await runDoctor(ctx).catch(() => undefined);
|
|
3050
|
+
const createdAt = new Date().toISOString();
|
|
3051
|
+
const bundle = buildHelpBundle({
|
|
3052
|
+
version: PRODUCT_VERSION,
|
|
3053
|
+
createdAt,
|
|
3054
|
+
diagnosis: current.diagnosis,
|
|
3055
|
+
snapshot: current.snapshot,
|
|
3056
|
+
inspectError: current.inspectError,
|
|
3057
|
+
doctor: report?.checks,
|
|
3058
|
+
rollover: current.rolloverState,
|
|
3059
|
+
failures: recentToolFailures,
|
|
3060
|
+
root: ctx.cwd,
|
|
3061
|
+
home: homedir(),
|
|
3062
|
+
});
|
|
3063
|
+
// .devflow/runs is the git-ignored runtime output root.
|
|
3064
|
+
const relative = `.devflow/runs/diagnostics/help-${createdAt.replace(/[:.]/g, "-")}.json`;
|
|
3065
|
+
await mkdir(path.dirname(path.join(ctx.cwd, relative)), { recursive: true });
|
|
3066
|
+
await writeFile(path.join(ctx.cwd, relative), `${JSON.stringify(bundle, null, 2)}\n`, "utf8");
|
|
3067
|
+
return relative;
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
function nextStepHint(diagnosis: WorkflowDiagnosis): string {
|
|
3071
|
+
if (diagnosis.kind === "blocked") return "运行 /devflow fix 查看处理办法。";
|
|
3072
|
+
if (diagnosis.kind === "no-feature" || diagnosis.kind === "complete") return "说「下一步」或运行 /devflow start <需求名> 创建需求。";
|
|
3073
|
+
return "说「下一步」或运行 /devflow next 继续。";
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
async function showWorkflowOverview(ctx: DevflowCommandContext): Promise<void> {
|
|
3077
|
+
const { diagnosis } = await diagnoseCurrentWorkflow(ctx.cwd);
|
|
3078
|
+
ctx.ui.notify(`${renderDiagnosis(diagnosis)}\n\n${nextStepHint(diagnosis)}`, diagnosis.kind === "blocked" ? "error" : "info");
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
/** Fixes that start or advance a step; `/devflow fix` never offers them. */
|
|
3082
|
+
const ADVANCING_FIXES = new Set<FixAction>(["review-and-advance", "rerun-step", "resume-rollover", "create-feature"]);
|
|
3083
|
+
|
|
3084
|
+
/**
|
|
3085
|
+
* Single user entry: diagnose → offer a fix from the closed catalog → let
|
|
3086
|
+
* Core execute it → re-diagnose. The model is never asked to repair control
|
|
3087
|
+
* state; every write goes through the existing runtime/Core paths.
|
|
3088
|
+
*/
|
|
3089
|
+
async function runNextStep(ctx: DevflowCommandContext, mode: "next" | "fix"): Promise<void> {
|
|
3090
|
+
if (activeWorkflowTurn || controlState.phase === "executing") {
|
|
3091
|
+
ctx.ui.notify("当前步骤正在执行,等这一轮结束后再说「下一步」。", "info");
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
3094
|
+
if (mode === "next") {
|
|
3095
|
+
const report = await runDoctor(ctx);
|
|
3096
|
+
if (!report.ok) {
|
|
3097
|
+
ctx.ui.notify(`运行环境检查没有通过,暂时不能推进:\n${formatDoctorReport(report).slice(0, 6000)}`, "error");
|
|
3098
|
+
return;
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
// Declining the template upgrade must not strand the step behind it.
|
|
3102
|
+
let skipWorkflowUpgrade = false;
|
|
3103
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
3104
|
+
const current = await diagnoseCurrentWorkflow(ctx.cwd, { skipWorkflowUpgrade });
|
|
3105
|
+
const { diagnosis, snapshot } = current;
|
|
3106
|
+
const summary = renderDiagnosis(diagnosis);
|
|
3107
|
+
if (diagnosis.kind === "execute" && snapshot) {
|
|
3108
|
+
if (mode === "fix") {
|
|
3109
|
+
ctx.ui.notify(`${summary}\n${nextStepHint(diagnosis)}`, "info");
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
3112
|
+
await resumeInspectedWorkflow(ctx, snapshot, { doctorAlreadyPassed: true });
|
|
3113
|
+
return;
|
|
3114
|
+
}
|
|
3115
|
+
const fixes = diagnosis.diagnoses
|
|
3116
|
+
.flatMap(item => item.fixes)
|
|
3117
|
+
.filter(fix => mode === "next" || !ADVANCING_FIXES.has(fix));
|
|
3118
|
+
ctx.ui.notify(fixes.length > 0 || mode === "next" ? summary : `${summary}\n${nextStepHint(diagnosis)}`, diagnosis.kind === "blocked" ? "error" : "info");
|
|
3119
|
+
if (fixes.length === 0) return;
|
|
3120
|
+
const skip = "暂不处理";
|
|
3121
|
+
const choice = await ctx.ui.select("选择处理方式", [
|
|
3122
|
+
...fixes.map(fix => ({ label: FIX_LABELS[fix].label, description: FIX_LABELS[fix].description })),
|
|
3123
|
+
{ label: skip, description: "保持现状,不做任何修改" },
|
|
3124
|
+
]);
|
|
3125
|
+
const fix = fixes.find(item => FIX_LABELS[item].label === choice);
|
|
3126
|
+
const upgradeOffered = diagnosis.diagnoses.some(item => item.code === "WORKFLOW_TEMPLATE_OUTDATED");
|
|
3127
|
+
if (!fix && upgradeOffered) {
|
|
3128
|
+
skipWorkflowUpgrade = true;
|
|
3129
|
+
ctx.ui.notify("本次不升级工作流定义,继续按现有定义诊断。", "info");
|
|
3130
|
+
continue;
|
|
3131
|
+
}
|
|
3132
|
+
if (!fix) {
|
|
3133
|
+
ctx.ui.notify("已保持现状,未做任何修改。", "info");
|
|
3134
|
+
return;
|
|
3135
|
+
}
|
|
3136
|
+
if (fix === "create-feature") {
|
|
3137
|
+
const name = (await ctx.ui.input("创建需求", "需求名称,例如:私海线索转回公海保护"))?.trim();
|
|
3138
|
+
if (!name) {
|
|
3139
|
+
ctx.ui.notify("未输入需求名称,未创建需求。", "info");
|
|
3140
|
+
return;
|
|
3141
|
+
}
|
|
3142
|
+
await devflowCommandRef?.(`start ${name}`, ctx);
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
if (fix === "review-and-advance") {
|
|
3146
|
+
setControlState(ctx, waitingUserState("等待用户审查步骤产物并明确决定"));
|
|
3147
|
+
if (!(await confirmAndAdvanceWorkflowStep(ctx))) {
|
|
3148
|
+
ctx.ui.notify("无法打开推进确认:当前步骤的产物已经变化。请重新说「下一步」。", "error");
|
|
3149
|
+
}
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
3152
|
+
if (fix === "revise-outputs") {
|
|
3153
|
+
setControlState(ctx, waitingUserState("等待用户审查步骤产物并明确决定"));
|
|
3154
|
+
ctx.ui.notify("已回到审查状态。请直接说明需要修改的内容;改完后说「批准执行」或「下一步」推进。", "info");
|
|
3155
|
+
return;
|
|
3156
|
+
}
|
|
3157
|
+
if (fix === "rerun-step" && snapshot) {
|
|
3158
|
+
const stepId = diagnosis.stepId ?? "当前步骤";
|
|
3159
|
+
setControlState(ctx, executingState("开工预检中"));
|
|
3160
|
+
void startWorkflowAfterPreflight(ctx, snapshot, "start", {
|
|
3161
|
+
doctorAlreadyPassed: true,
|
|
3162
|
+
executingLabel: `重新执行 ${stepId}`,
|
|
3163
|
+
}).catch(error => {
|
|
3164
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检未通过" });
|
|
3165
|
+
ctx.ui.notify(`开工预检未通过:${String(error)}`, "error");
|
|
3166
|
+
});
|
|
3167
|
+
return;
|
|
3168
|
+
}
|
|
3169
|
+
if (fix === "resume-rollover" && snapshot) {
|
|
3170
|
+
const rollover = current.rolloverState?.rollover as Record<string, unknown> | undefined;
|
|
3171
|
+
if (typeof current.rolloverState?.revision !== "number" || typeof rollover?.id !== "string") return;
|
|
3172
|
+
pendingRollover = { snapshot, preparedRevision: current.rolloverState.revision, rolloverId: rollover.id };
|
|
3173
|
+
await enqueueControlledRolloverRequest(ctx.cwd, rollover.id, ctx.sessionManager.getSessionId());
|
|
3174
|
+
setControlState(ctx, waitingUserState("正在恢复受控会话交接"));
|
|
3175
|
+
ctx.ui.notify("已恢复未完成的会话交接;当前回合结束后会切换到新会话并继续。", "info");
|
|
3176
|
+
return;
|
|
3177
|
+
}
|
|
3178
|
+
if (fix === "repair-registry") {
|
|
3179
|
+
const plan = snapshot?.recovery_plan as Record<string, unknown> | undefined;
|
|
3180
|
+
if (typeof plan?.expected_revision !== "number") return;
|
|
3181
|
+
const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/repair_control_plane.rb", [
|
|
3182
|
+
"--write", "--confirm", "--expected-revision", String(plan.expected_revision), "--json",
|
|
3183
|
+
]);
|
|
3184
|
+
if (!controlPlaneSucceeded(result)) {
|
|
3185
|
+
ctx.ui.notify(`登记表修复失败:${describeControlFailure(result)}`, "error");
|
|
3186
|
+
return;
|
|
3187
|
+
}
|
|
3188
|
+
ctx.ui.notify("登记表已修复,未推进工作流。正在重新诊断……", "info");
|
|
3189
|
+
continue;
|
|
3190
|
+
}
|
|
3191
|
+
if (fix === "upgrade-workflow") {
|
|
3192
|
+
const paths = diagnosis.diagnoses
|
|
3193
|
+
.find(item => item.code === "WORKFLOW_TEMPLATE_OUTDATED")?.evidence ?? [];
|
|
3194
|
+
const confirmed = await ctx.ui.confirm(
|
|
3195
|
+
"升级工作流定义",
|
|
3196
|
+
`将用当前版本内置模板替换以下工作流文件:\n${paths.map(item => `- ${item}`).join("\n")}\n\n这些文件中的手工修改会被替换;需求产物和 STATE 不会改变,也不会推进任何步骤。继续吗?`,
|
|
3197
|
+
);
|
|
3198
|
+
if (!confirmed) {
|
|
3199
|
+
skipWorkflowUpgrade = true;
|
|
3200
|
+
ctx.ui.notify("本次不升级工作流定义,继续按现有定义诊断。", "info");
|
|
3201
|
+
continue;
|
|
3202
|
+
}
|
|
3203
|
+
const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/init_workflow.rb", [
|
|
3204
|
+
"--write", "--force", "--json",
|
|
3205
|
+
]);
|
|
3206
|
+
if (!controlPlaneSucceeded(result)) {
|
|
3207
|
+
ctx.ui.notify(`工作流升级失败:${describeControlFailure(result)}`, "error");
|
|
3208
|
+
return;
|
|
3209
|
+
}
|
|
3210
|
+
const synced = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/sync_artifacts.rb", [
|
|
3211
|
+
"--write", "--no-preserve", "--json",
|
|
3212
|
+
]);
|
|
3213
|
+
if (!controlPlaneSucceeded(synced)) {
|
|
3214
|
+
ctx.ui.notify(`工作流升级后的产物同步失败:${describeControlFailure(synced)}`, "error");
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
activeContract = undefined;
|
|
3218
|
+
contractAgentCalls = 0;
|
|
3219
|
+
ctx.ui.notify("工作流定义已升级。正在重新诊断……", "info");
|
|
3220
|
+
continue;
|
|
3221
|
+
}
|
|
3222
|
+
if (fix === "help-bundle") {
|
|
3223
|
+
const bundlePath = await writeHelpBundle(ctx, current);
|
|
3224
|
+
ctx.ui.notify(`已生成求助包:${bundlePath}\n只包含诊断信息(状态摘要、校验错误、最近的工具报错),不含源码。可以把这个文件发给维护者。`, "info");
|
|
3225
|
+
return;
|
|
3226
|
+
}
|
|
3227
|
+
if (fix === "cancel-feature") {
|
|
3228
|
+
await devflowCommandRef?.("abandon", ctx);
|
|
3229
|
+
return;
|
|
3230
|
+
}
|
|
3231
|
+
return;
|
|
3232
|
+
}
|
|
3233
|
+
ctx.ui.notify("自动修复后仍有问题。请运行 /devflow fix 并选择「生成求助包」。", "error");
|
|
3234
|
+
}
|
|
3235
|
+
|
|
2912
3236
|
type ProjectControlCommandContext = {
|
|
2913
3237
|
cwd: string;
|
|
2914
3238
|
models: DoctorContext["models"];
|
|
@@ -3321,7 +3645,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3321
3645
|
// sole authority: only the original source session may resume a prepared
|
|
3322
3646
|
// transition, and only the already-recorded target may consume a claim.
|
|
3323
3647
|
const rolloverResult = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/session_rollover.rb", ["inspect", "--json"]);
|
|
3324
|
-
|
|
3648
|
+
let rolloverState = parseJsonResult(rolloverResult);
|
|
3325
3649
|
let rollover = rolloverState?.rollover as Record<string, unknown> | undefined;
|
|
3326
3650
|
const currentSessionId = ctx.sessionManager.getSessionId();
|
|
3327
3651
|
if (
|
|
@@ -3334,13 +3658,14 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3334
3658
|
]);
|
|
3335
3659
|
if (recovered.ok) {
|
|
3336
3660
|
const refreshed = parseJsonResult(await runRuby(ctx.cwd, "workflow-orchestrator/scripts/session_rollover.rb", ["inspect", "--json"]));
|
|
3661
|
+
rolloverState = refreshed;
|
|
3337
3662
|
rollover = refreshed?.rollover as Record<string, unknown> | undefined;
|
|
3338
3663
|
}
|
|
3339
3664
|
}
|
|
3340
3665
|
if (rolloverState && typeof rolloverState.revision === "number" && rollover?.status === "prepared" && typeof rollover.id === "string") {
|
|
3341
3666
|
const workflow = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
3342
|
-
const
|
|
3343
|
-
if (workflow.result.ok && workflow.snapshot &&
|
|
3667
|
+
const preparedRevision = preparedRolloverRevision(rolloverState);
|
|
3668
|
+
if (workflow.result.ok && workflow.snapshot && preparedRevision !== undefined && workflow.snapshot.revision === preparedRevision) {
|
|
3344
3669
|
pendingRollover = { snapshot: workflow.snapshot, preparedRevision: rolloverState.revision, rolloverId: rollover.id };
|
|
3345
3670
|
await enqueueControlledRolloverRequest(ctx.cwd, rollover.id, currentSessionId);
|
|
3346
3671
|
ctx.ui.notify("已恢复待完成的受控会话交接;当前回合结束后将继续切换。", "info");
|
|
@@ -3507,7 +3832,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3507
3832
|
if (event.source !== "interactive") return;
|
|
3508
3833
|
const controlAction = classifyNaturalControlAction(event.text);
|
|
3509
3834
|
const stepNote = event.text.trim();
|
|
3510
|
-
if (controlState.intent === "workflow-execute" && !controlAction && stepNote && !stepNote.startsWith("/")) {
|
|
3835
|
+
if (controlState.intent === "workflow-execute" && !controlAction && stepNote && !stepNote.startsWith("/") && !classifyNextStepPhrase(stepNote)) {
|
|
3511
3836
|
stepUserNotes.push(stepNote);
|
|
3512
3837
|
}
|
|
3513
3838
|
if (controlState.intent === "requirement-draft" && controlState.phase === "planning") {
|
|
@@ -3561,6 +3886,24 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3561
3886
|
}
|
|
3562
3887
|
return { handled: true };
|
|
3563
3888
|
}
|
|
3889
|
+
// Short phrases such as "下一步" open the same diagnose-and-fix entry as
|
|
3890
|
+
// /devflow next. They never approve by themselves: advancing still needs
|
|
3891
|
+
// the runtime confirmation dialog. "继续" is only taken over when a
|
|
3892
|
+
// feature is in progress so ordinary chat continuation reaches the model.
|
|
3893
|
+
const nextPhrase = classifyNextStepPhrase(event.text);
|
|
3894
|
+
if (nextPhrase && ctx.hasUI && ["idle", "waiting-user", "blocked"].includes(controlState.phase)) {
|
|
3895
|
+
let takeOver = nextPhrase === "next";
|
|
3896
|
+
if (nextPhrase === "continue") {
|
|
3897
|
+
const inspected = await inspectWorkflow(ctx.cwd);
|
|
3898
|
+
takeOver = typeof inspected.snapshot?.current_feature === "string"
|
|
3899
|
+
&& !["completed", "cancelled", "failed"].includes(String(inspected.snapshot.status));
|
|
3900
|
+
}
|
|
3901
|
+
if (takeOver) {
|
|
3902
|
+
// runNextStep only uses members shared by event and command contexts.
|
|
3903
|
+
await runNextStep(ctx as DevflowCommandContext, "next");
|
|
3904
|
+
return { handled: true };
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3564
3907
|
// The in-memory prompt state is intentionally not authoritative. A fresh
|
|
3565
3908
|
// session starts idle, so recover the approval arm from the durable Core
|
|
3566
3909
|
// snapshot before handing an explicit approval phrase to the model. This
|
|
@@ -3596,10 +3939,8 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3596
3939
|
return { handled: true };
|
|
3597
3940
|
}
|
|
3598
3941
|
if (checkpoint && stepId === "wireframe-review" && typeof inspected.snapshot.current_feature === "string") {
|
|
3599
|
-
const approval = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/wireframe_commit.rb",
|
|
3600
|
-
|
|
3601
|
-
"--expected-revision", String(revision), "--all-screens", "--runtime-confirmed", "--write", "--json",
|
|
3602
|
-
]);
|
|
3942
|
+
const approval = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/wireframe_commit.rb",
|
|
3943
|
+
wireframeApprovalArgs(inspected.snapshot.current_feature, revision));
|
|
3603
3944
|
if (!controlPlaneSucceeded(approval)) {
|
|
3604
3945
|
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "原型确认写入失败" });
|
|
3605
3946
|
ctx.ui.notify(`原型确认未写入:${approval.stderr || approval.stdout}`, "error");
|
|
@@ -3615,7 +3956,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3615
3956
|
]);
|
|
3616
3957
|
if (!result.ok || !controlPlaneSucceeded(result)) {
|
|
3617
3958
|
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "推进校验失败" });
|
|
3618
|
-
ctx.ui.notify(`未推进:${result
|
|
3959
|
+
ctx.ui.notify(`未推进:${describeControlFailure(result)}\n请补齐上述校验缺项后重新审批;重复批准不会绕过门禁。可以说「下一步」重新诊断,或选择重新执行这一步来修复产物。`, "error");
|
|
3619
3960
|
return { handled: true };
|
|
3620
3961
|
}
|
|
3621
3962
|
const after = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
@@ -3852,6 +4193,20 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3852
4193
|
reason: "规范需求图谱、派生视图、机器 Evidence 与合同审计只能由 Core writer 写入;Skill 只能提交候选 delta 或 agent_claim。",
|
|
3853
4194
|
};
|
|
3854
4195
|
}
|
|
4196
|
+
// Feature artifacts written while no step is running are never adopted by
|
|
4197
|
+
// the workflow (zhaoshang, 2026-10-22). Refuse early in the interactive
|
|
4198
|
+
// main session; subagents (no UI), review-feedback edits and controlled
|
|
4199
|
+
// turns keep their existing rules.
|
|
4200
|
+
if (
|
|
4201
|
+
ctx.hasUI &&
|
|
4202
|
+
controlState.phase === "idle" &&
|
|
4203
|
+
!activeWorkflowTurn &&
|
|
4204
|
+
!toolName.startsWith("devflow_") &&
|
|
4205
|
+
["write", "edit", "apply_patch", "notebook_edit"].includes(toolName) &&
|
|
4206
|
+
inputPaths(event.input, ctx.cwd).some(filePath => planningFeatureFromPath(filePath) !== null)
|
|
4207
|
+
) {
|
|
4208
|
+
return { block: true, reason: OUTSIDE_STEP_WRITE_REASON };
|
|
4209
|
+
}
|
|
3855
4210
|
if (
|
|
3856
4211
|
!toolName.startsWith("devflow_") &&
|
|
3857
4212
|
["write", "edit", "apply_patch", "notebook_edit"].includes(toolName)
|
|
@@ -4327,6 +4682,8 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
4327
4682
|
kind: z.string().optional(),
|
|
4328
4683
|
match: z.string().optional(),
|
|
4329
4684
|
expected: z.unknown().optional(),
|
|
4685
|
+
normalize_whitespace: z.boolean().optional(),
|
|
4686
|
+
case_sensitive: z.boolean().optional(),
|
|
4330
4687
|
name: z.string().optional(),
|
|
4331
4688
|
tc_id: z.string().optional(),
|
|
4332
4689
|
}),
|
|
@@ -4385,7 +4742,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
4385
4742
|
Object.assign(requestParams, { ref: params.ref, action: params.operation, value: params.value, value_from_secret: params.value_from_secret });
|
|
4386
4743
|
} else if (params.action === "assert") {
|
|
4387
4744
|
if (!params.ref || !params.kind || !["text", "visible", "value", "each_text"].includes(params.kind)) return fail("assert requires ref and a supported kind");
|
|
4388
|
-
Object.assign(requestParams, { ref: params.ref, kind: params.kind, match: params.match, expected: params.expected });
|
|
4745
|
+
Object.assign(requestParams, { ref: params.ref, kind: params.kind, match: params.match, expected: params.expected, normalize_whitespace: params.normalize_whitespace, case_sensitive: params.case_sensitive });
|
|
4389
4746
|
} else if (params.action === "draft") {
|
|
4390
4747
|
if (!params.name) return fail("draft requires name");
|
|
4391
4748
|
Object.assign(requestParams, { name: params.name, tc_id: params.tc_id });
|
|
@@ -5237,273 +5594,288 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
5237
5594
|
},
|
|
5238
5595
|
});
|
|
5239
5596
|
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5597
|
+
const devflowCommand = async (args: string, ctx: DevflowCommandContext): Promise<void> => {
|
|
5598
|
+
const trimmed = args.trim();
|
|
5599
|
+
if (trimmed === "" || trimmed === "status") {
|
|
5600
|
+
await showWorkflowOverview(ctx);
|
|
5601
|
+
return;
|
|
5602
|
+
}
|
|
5603
|
+
if (trimmed === "status --json") {
|
|
5604
|
+
const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/inspect_state.rb", ["--json"]);
|
|
5605
|
+
ctx.ui.notify((result.stdout || result.stderr).slice(0, 6000), result.ok ? "info" : "error");
|
|
5606
|
+
return;
|
|
5607
|
+
}
|
|
5608
|
+
if (trimmed === "next" || trimmed === "下一步") {
|
|
5609
|
+
await runNextStep(ctx, "next");
|
|
5610
|
+
return;
|
|
5611
|
+
}
|
|
5612
|
+
if (trimmed === "fix") {
|
|
5613
|
+
await runNextStep(ctx, "fix");
|
|
5614
|
+
return;
|
|
5615
|
+
}
|
|
5616
|
+
if (trimmed === "control") {
|
|
5617
|
+
await openProjectControlPanel(ctx);
|
|
5618
|
+
return;
|
|
5619
|
+
}
|
|
5620
|
+
if (trimmed === "explain-state") {
|
|
5621
|
+
const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
5622
|
+
ctx.ui.notify(
|
|
5623
|
+
inspected.result.ok ? explainWorkflowSnapshot(inspected.snapshot) : `状态读取失败:${inspected.result.stderr || inspected.result.stdout}`,
|
|
5624
|
+
inspected.result.ok ? "info" : "error",
|
|
5625
|
+
);
|
|
5626
|
+
setControlState(ctx, initialControlState());
|
|
5627
|
+
return;
|
|
5628
|
+
}
|
|
5629
|
+
if (trimmed === "recover") {
|
|
5630
|
+
const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
5631
|
+
const plan = inspected.snapshot?.recovery_plan as Record<string, unknown> | undefined;
|
|
5632
|
+
const revision = plan?.expected_revision;
|
|
5633
|
+
if (!inspected.result.ok || !plan || plan.classification !== "safe_repairable" || typeof revision !== "number") {
|
|
5634
|
+
const status = typeof inspected.snapshot?.status === "string" ? inspected.snapshot.status : "unknown";
|
|
5635
|
+
const step = inspected.snapshot ? selectedWorkflowStep(inspected.snapshot) : null;
|
|
5636
|
+
const stepId = typeof step?.id === "string" ? step.id : "当前步骤";
|
|
5637
|
+
if (inspected.result.ok && status === "in_progress") {
|
|
5638
|
+
ctx.ui.notify(`控制面状态一致,无需恢复。当前步骤是 ${stepId};请审查当前产物后明确回复“批准执行”或提出修改意见。`, "info");
|
|
5639
|
+
} else if (inspected.result.ok && status === "waiting_checkpoint") {
|
|
5640
|
+
ctx.ui.notify("控制面状态一致,无需恢复。当前正在等待 checkpoint;请审查产物后明确回复“批准执行”或提出修改意见。", "info");
|
|
5641
|
+
} else {
|
|
5642
|
+
ctx.ui.notify("当前状态没有可安全自动执行的恢复计划;请运行 /devflow explain-state,并按其中的阻塞证据处理。", "error");
|
|
5643
|
+
}
|
|
5247
5644
|
return;
|
|
5248
5645
|
}
|
|
5249
|
-
|
|
5250
|
-
|
|
5646
|
+
const confirmed = await ctx.ui.confirm(
|
|
5647
|
+
"确认修复 Devflow 控制面",
|
|
5648
|
+
"将只根据已存在的文件同步 artifact registry;不会推进步骤、确认 checkpoint 或修改需求产物。继续吗?",
|
|
5649
|
+
);
|
|
5650
|
+
if (!confirmed) {
|
|
5651
|
+
ctx.ui.notify("恢复未确认;状态保持不变。", "info");
|
|
5251
5652
|
return;
|
|
5252
5653
|
}
|
|
5253
|
-
|
|
5254
|
-
|
|
5654
|
+
const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/repair_control_plane.rb", [
|
|
5655
|
+
"--write",
|
|
5656
|
+
"--confirm",
|
|
5657
|
+
"--expected-revision",
|
|
5658
|
+
String(revision),
|
|
5659
|
+
"--json",
|
|
5660
|
+
]);
|
|
5661
|
+
if (!result.ok) {
|
|
5662
|
+
ctx.ui.notify(`控制面恢复失败:${result.stderr || result.stdout}`, "error");
|
|
5663
|
+
return;
|
|
5664
|
+
}
|
|
5665
|
+
ctx.ui.notify("控制面 registry 已修复;未推进工作流。请审查当前步骤产物后明确回复“批准执行”或提出修改意见。", "info");
|
|
5666
|
+
return;
|
|
5667
|
+
}
|
|
5668
|
+
if (trimmed === "abandon" || trimmed.startsWith("abandon ") || trimmed === "cancel" || trimmed.startsWith("cancel ")) {
|
|
5669
|
+
const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
5670
|
+
const snapshot = inspected.snapshot;
|
|
5671
|
+
const revision = typeof snapshot?.revision === "number" ? snapshot.revision : undefined;
|
|
5672
|
+
const feature = typeof snapshot?.current_feature === "string" ? snapshot.current_feature : undefined;
|
|
5673
|
+
const status = typeof snapshot?.status === "string" ? snapshot.status : undefined;
|
|
5674
|
+
if (!inspected.result.ok || revision === undefined || !feature) {
|
|
5675
|
+
ctx.ui.notify(`无法读取当前需求状态:${inspected.result.stderr || inspected.result.stdout}`, "error");
|
|
5676
|
+
return;
|
|
5677
|
+
}
|
|
5678
|
+
if (!status || ["completed", "cancelled", "failed"].includes(status)) {
|
|
5679
|
+
ctx.ui.notify(`当前没有可弃用的进行中需求(status=${status || "unknown"})。`, "info");
|
|
5680
|
+
return;
|
|
5681
|
+
}
|
|
5682
|
+
const reason = trimmed.split(/\s+/, 2)[1]?.trim() || "用户主动终止当前需求";
|
|
5683
|
+
const confirmed = await ctx.ui.confirm(
|
|
5684
|
+
"确认弃用当前需求",
|
|
5685
|
+
`需求:${feature}\n原因:${reason}\n\n将结束当前需求、释放项目占用并保留现有工件;不会把未完成步骤标记为完成。继续吗?`,
|
|
5686
|
+
);
|
|
5687
|
+
if (!confirmed) {
|
|
5688
|
+
ctx.ui.notify("已取消弃用操作;状态保持不变。", "info");
|
|
5689
|
+
return;
|
|
5690
|
+
}
|
|
5691
|
+
const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/advance_state.rb", [
|
|
5692
|
+
"--write", "--json", "--cancel", "--cancel-reason", reason, "--expected-revision", String(revision),
|
|
5693
|
+
]);
|
|
5694
|
+
if (!result.ok) {
|
|
5695
|
+
ctx.ui.notify(`弃用当前需求失败:${result.stderr || result.stdout}`, "error");
|
|
5696
|
+
return;
|
|
5697
|
+
}
|
|
5698
|
+
activeContract = undefined;
|
|
5699
|
+
contractAgentCalls = 0;
|
|
5700
|
+
setControlState(ctx, initialControlState());
|
|
5701
|
+
ctx.ui.notify(`需求“${feature}”已弃用;现有工件已保留。现在可以创建新需求。`, "info");
|
|
5702
|
+
return;
|
|
5703
|
+
}
|
|
5704
|
+
if (trimmed === "project-init") {
|
|
5705
|
+
const initialization = await collectProjectInitialization(ctx);
|
|
5706
|
+
if (!initialization) return;
|
|
5707
|
+
pendingProjectInitialization = initialization;
|
|
5708
|
+
if (initialization.mode === "apply") {
|
|
5709
|
+
setControlState(ctx, executingState("执行已确认的项目初始化"));
|
|
5710
|
+
activeWorkflowTurn = true;
|
|
5711
|
+
await pi.sendUserMessage(projectInitializationDirective(initialization.summary));
|
|
5712
|
+
return;
|
|
5713
|
+
}
|
|
5714
|
+
setControlState(ctx, planningState("project-init"));
|
|
5715
|
+
await pi.sendUserMessage(
|
|
5716
|
+
[
|
|
5717
|
+
"<devflow-planner-directive>",
|
|
5718
|
+
"用户已确认项目初始化的输入。你现在是 Planner,只允许只读分析和派发 project-scout / requirements-auditor / contract-auditor。",
|
|
5719
|
+
"不得创建 .devflow/planning 文件、不得调用状态迁移、不得执行实现。",
|
|
5720
|
+
"先输出:已知事实、待验证点、最少侦察任务和必须由用户决定的问题。没有证据时写 unknown。",
|
|
5721
|
+
initialization.summary,
|
|
5722
|
+
"</devflow-planner-directive>",
|
|
5723
|
+
].join("\n"),
|
|
5724
|
+
);
|
|
5725
|
+
return;
|
|
5726
|
+
}
|
|
5727
|
+
if (trimmed === "doctor") {
|
|
5728
|
+
const report = await runDoctor(ctx);
|
|
5729
|
+
ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), report.ok ? "info" : "error");
|
|
5730
|
+
return;
|
|
5731
|
+
}
|
|
5732
|
+
if (trimmed === "resume") {
|
|
5733
|
+
const report = await runDoctor(ctx);
|
|
5734
|
+
if (!report.ok) {
|
|
5735
|
+
ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), "error");
|
|
5736
|
+
return;
|
|
5737
|
+
}
|
|
5738
|
+
const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
5739
|
+
if (!inspected.result.ok || !inspected.snapshot) {
|
|
5255
5740
|
ctx.ui.notify(
|
|
5256
|
-
|
|
5257
|
-
|
|
5741
|
+
`Devflow resume failed: ${inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON"}`,
|
|
5742
|
+
"error",
|
|
5258
5743
|
);
|
|
5259
|
-
setControlState(ctx, initialControlState());
|
|
5260
5744
|
return;
|
|
5261
5745
|
}
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
);
|
|
5283
|
-
if (!confirmed) {
|
|
5284
|
-
ctx.ui.notify("恢复未确认;状态保持不变。", "info");
|
|
5285
|
-
return;
|
|
5286
|
-
}
|
|
5287
|
-
const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/repair_control_plane.rb", [
|
|
5288
|
-
"--write",
|
|
5289
|
-
"--confirm",
|
|
5290
|
-
"--expected-revision",
|
|
5291
|
-
String(revision),
|
|
5292
|
-
"--json",
|
|
5293
|
-
]);
|
|
5294
|
-
if (!result.ok) {
|
|
5295
|
-
ctx.ui.notify(`控制面恢复失败:${result.stderr || result.stdout}`, "error");
|
|
5296
|
-
return;
|
|
5297
|
-
}
|
|
5298
|
-
ctx.ui.notify("控制面 registry 已修复;未推进工作流。请审查当前步骤产物后明确回复“批准执行”或提出修改意见。", "info");
|
|
5746
|
+
// Re-arm a durable rollover left by an interrupted runtime. An
|
|
5747
|
+
// explicit resume is the user's authorization for a fresh session to
|
|
5748
|
+
// adopt the same project's integrity-checked prepared handoff.
|
|
5749
|
+
const rolloverResult = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/session_rollover.rb", ["inspect", "--json"]);
|
|
5750
|
+
let rolloverState = parseJsonResult(rolloverResult);
|
|
5751
|
+
const rollover = rolloverState?.rollover as Record<string, unknown> | undefined;
|
|
5752
|
+
const currentSessionId = ctx.sessionManager.getSessionId();
|
|
5753
|
+
if (
|
|
5754
|
+
rolloverState &&
|
|
5755
|
+
typeof rolloverState.revision === "number" &&
|
|
5756
|
+
rollover?.status === "prepared" &&
|
|
5757
|
+
typeof rollover.id === "string"
|
|
5758
|
+
) {
|
|
5759
|
+
pendingRollover = {
|
|
5760
|
+
snapshot: inspected.snapshot,
|
|
5761
|
+
preparedRevision: rolloverState.revision,
|
|
5762
|
+
rolloverId: rollover.id,
|
|
5763
|
+
};
|
|
5764
|
+
await enqueueControlledRolloverRequest(ctx.cwd, rollover.id, currentSessionId);
|
|
5765
|
+
setControlState(ctx, waitingUserState("正在恢复受控会话交接"));
|
|
5766
|
+
ctx.ui.notify("Devflow 已恢复待完成的受控会话交接;当前回合结束后将继续切换。", "info");
|
|
5299
5767
|
return;
|
|
5300
5768
|
}
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
ctx.ui.notify(`无法读取当前需求状态:${inspected.result.stderr || inspected.result.stdout}`, "error");
|
|
5309
|
-
return;
|
|
5310
|
-
}
|
|
5311
|
-
if (!status || ["completed", "cancelled", "failed"].includes(status)) {
|
|
5312
|
-
ctx.ui.notify(`当前没有可弃用的进行中需求(status=${status || "unknown"})。`, "info");
|
|
5313
|
-
return;
|
|
5314
|
-
}
|
|
5315
|
-
const reason = trimmed.split(/\s+/, 2)[1]?.trim() || "用户主动终止当前需求";
|
|
5316
|
-
const confirmed = await ctx.ui.confirm(
|
|
5317
|
-
"确认弃用当前需求",
|
|
5318
|
-
`需求:${feature}\n原因:${reason}\n\n将结束当前需求、释放项目占用并保留现有工件;不会把未完成步骤标记为完成。继续吗?`,
|
|
5319
|
-
);
|
|
5320
|
-
if (!confirmed) {
|
|
5321
|
-
ctx.ui.notify("已取消弃用操作;状态保持不变。", "info");
|
|
5322
|
-
return;
|
|
5323
|
-
}
|
|
5324
|
-
const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/advance_state.rb", [
|
|
5325
|
-
"--write", "--json", "--cancel", "--cancel-reason", reason, "--expected-revision", String(revision),
|
|
5326
|
-
]);
|
|
5327
|
-
if (!result.ok) {
|
|
5328
|
-
ctx.ui.notify(`弃用当前需求失败:${result.stderr || result.stdout}`, "error");
|
|
5329
|
-
return;
|
|
5330
|
-
}
|
|
5331
|
-
activeContract = undefined;
|
|
5332
|
-
contractAgentCalls = 0;
|
|
5333
|
-
setControlState(ctx, initialControlState());
|
|
5334
|
-
ctx.ui.notify(`需求“${feature}”已弃用;现有工件已保留。现在可以创建新需求。`, "info");
|
|
5769
|
+
await resumeInspectedWorkflow(ctx, inspected.snapshot, { doctorAlreadyPassed: true });
|
|
5770
|
+
return;
|
|
5771
|
+
}
|
|
5772
|
+
if (trimmed.startsWith("start ")) {
|
|
5773
|
+
const [feature, requestedPhase] = trimmed.slice(6).split("|", 2).map(value => value.trim());
|
|
5774
|
+
if (!feature) {
|
|
5775
|
+
ctx.ui.notify("Usage: /devflow start <feature>|<phase>", "error");
|
|
5335
5776
|
return;
|
|
5336
5777
|
}
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5778
|
+
const phase = requestedPhase || defaultPhaseId(feature);
|
|
5779
|
+
if (!validPhaseId(phase)) {
|
|
5780
|
+
ctx.ui.notify("Phase ID 必须是 1-64 位字母、数字、下划线或连字符,且不得包含路径分隔符。", "error");
|
|
5781
|
+
return;
|
|
5782
|
+
}
|
|
5783
|
+
const report = await runDoctor(ctx);
|
|
5784
|
+
if (!report.ok) {
|
|
5785
|
+
ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), "error");
|
|
5786
|
+
return;
|
|
5787
|
+
}
|
|
5788
|
+
const existing = await inspectWorkflow(ctx.cwd);
|
|
5789
|
+
const existingFeature = existing.snapshot?.current_feature;
|
|
5790
|
+
const previousWorkflowComplete = ["workflow_complete", "workflow_cancelled"].includes(existing.snapshot?.action as string);
|
|
5791
|
+
if (
|
|
5792
|
+
!previousWorkflowComplete &&
|
|
5793
|
+
typeof existingFeature === "string" &&
|
|
5794
|
+
existingFeature !== feature
|
|
5795
|
+
) {
|
|
5796
|
+
ctx.ui.notify(
|
|
5797
|
+
`当前仍有未完成需求:${existingFeature}。请在对话中继续处理当前步骤,完成后再创建新需求:${feature}。`,
|
|
5798
|
+
"error",
|
|
5357
5799
|
);
|
|
5358
5800
|
return;
|
|
5359
5801
|
}
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5802
|
+
// This dry-run happens before init_workflow.rb, so a rejected feature
|
|
5803
|
+
// can never overwrite generated workflow files belonging to an active
|
|
5804
|
+
// owner or a legacy phase directory that has no trustworthy owner.
|
|
5805
|
+
const ownership = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/phase_ownership.rb", [
|
|
5806
|
+
"claim",
|
|
5807
|
+
"--feature",
|
|
5808
|
+
feature,
|
|
5809
|
+
"--phase",
|
|
5810
|
+
phase,
|
|
5811
|
+
"--flow",
|
|
5812
|
+
"feature-development",
|
|
5813
|
+
"--json",
|
|
5814
|
+
]);
|
|
5815
|
+
if (!controlPlaneSucceeded(ownership)) {
|
|
5816
|
+
ctx.ui.notify((ownership.stderr || ownership.stdout).slice(0, 6000), "error");
|
|
5363
5817
|
return;
|
|
5364
5818
|
}
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
5372
|
-
if (!inspected.result.ok || !inspected.snapshot) {
|
|
5373
|
-
ctx.ui.notify(
|
|
5374
|
-
`Devflow resume failed: ${inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON"}`,
|
|
5375
|
-
"error",
|
|
5376
|
-
);
|
|
5377
|
-
return;
|
|
5378
|
-
}
|
|
5379
|
-
// Re-arm a durable rollover left by an interrupted runtime. An
|
|
5380
|
-
// explicit resume is the user's authorization for a fresh session to
|
|
5381
|
-
// adopt the same project's integrity-checked prepared handoff.
|
|
5382
|
-
const rolloverResult = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/session_rollover.rb", ["inspect", "--json"]);
|
|
5383
|
-
const rolloverState = parseJsonResult(rolloverResult);
|
|
5384
|
-
const rollover = rolloverState?.rollover as Record<string, unknown> | undefined;
|
|
5385
|
-
const currentSessionId = ctx.sessionManager.getSessionId();
|
|
5386
|
-
if (
|
|
5387
|
-
rolloverState &&
|
|
5388
|
-
typeof rolloverState.revision === "number" &&
|
|
5389
|
-
rollover?.status === "prepared" &&
|
|
5390
|
-
typeof rollover.id === "string"
|
|
5391
|
-
) {
|
|
5392
|
-
pendingRollover = {
|
|
5393
|
-
snapshot: inspected.snapshot,
|
|
5394
|
-
preparedRevision: rolloverState.revision,
|
|
5395
|
-
rolloverId: rollover.id,
|
|
5396
|
-
};
|
|
5397
|
-
await enqueueControlledRolloverRequest(ctx.cwd, rollover.id, currentSessionId);
|
|
5398
|
-
setControlState(ctx, waitingUserState("正在恢复受控会话交接"));
|
|
5399
|
-
ctx.ui.notify("Devflow 已恢复待完成的受控会话交接;当前回合结束后将继续切换。", "info");
|
|
5400
|
-
return;
|
|
5401
|
-
}
|
|
5402
|
-
await resumeInspectedWorkflow(ctx, inspected.snapshot, { doctorAlreadyPassed: true });
|
|
5819
|
+
const workflow = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/init_workflow.rb", [
|
|
5820
|
+
"--write",
|
|
5821
|
+
"--json",
|
|
5822
|
+
]);
|
|
5823
|
+
if (!controlPlaneSucceeded(workflow)) {
|
|
5824
|
+
ctx.ui.notify(workflow.stderr || workflow.stdout, "error");
|
|
5403
5825
|
return;
|
|
5404
5826
|
}
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
return;
|
|
5434
|
-
}
|
|
5435
|
-
// This dry-run happens before init_workflow.rb, so a rejected feature
|
|
5436
|
-
// can never overwrite generated workflow files belonging to an active
|
|
5437
|
-
// owner or a legacy phase directory that has no trustworthy owner.
|
|
5438
|
-
const ownership = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/phase_ownership.rb", [
|
|
5439
|
-
"claim",
|
|
5440
|
-
"--feature",
|
|
5441
|
-
feature,
|
|
5442
|
-
"--phase",
|
|
5443
|
-
phase,
|
|
5444
|
-
"--flow",
|
|
5445
|
-
"feature-development",
|
|
5446
|
-
"--json",
|
|
5447
|
-
]);
|
|
5448
|
-
if (!controlPlaneSucceeded(ownership)) {
|
|
5449
|
-
ctx.ui.notify((ownership.stderr || ownership.stdout).slice(0, 6000), "error");
|
|
5450
|
-
return;
|
|
5451
|
-
}
|
|
5452
|
-
const workflow = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/init_workflow.rb", [
|
|
5453
|
-
"--write",
|
|
5454
|
-
"--json",
|
|
5455
|
-
]);
|
|
5456
|
-
if (!controlPlaneSucceeded(workflow)) {
|
|
5457
|
-
ctx.ui.notify(workflow.stderr || workflow.stdout, "error");
|
|
5458
|
-
return;
|
|
5459
|
-
}
|
|
5460
|
-
const state = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/init_state.rb", [
|
|
5461
|
-
"--feature",
|
|
5462
|
-
feature,
|
|
5463
|
-
"--phase",
|
|
5464
|
-
phase,
|
|
5465
|
-
"--flow",
|
|
5466
|
-
"feature-development",
|
|
5467
|
-
"--write",
|
|
5468
|
-
"--json",
|
|
5469
|
-
...(previousWorkflowComplete ? ["--force"] : []),
|
|
5470
|
-
]);
|
|
5471
|
-
if (!controlPlaneSucceeded(state)) {
|
|
5472
|
-
ctx.ui.notify((state.stderr || state.stdout).slice(0, 6000), "error");
|
|
5473
|
-
return;
|
|
5474
|
-
}
|
|
5475
|
-
const artifacts = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/sync_artifacts.rb", [
|
|
5476
|
-
"--write",
|
|
5477
|
-
"--json",
|
|
5478
|
-
]);
|
|
5479
|
-
if (!controlPlaneSucceeded(artifacts)) {
|
|
5480
|
-
ctx.ui.notify((artifacts.stderr || artifacts.stdout).slice(0, 6000), "error");
|
|
5481
|
-
return;
|
|
5482
|
-
}
|
|
5483
|
-
const inspected = await inspectWorkflow(ctx.cwd);
|
|
5484
|
-
if (!inspected.result.ok || !inspected.snapshot) {
|
|
5485
|
-
ctx.ui.notify(
|
|
5486
|
-
`Devflow initialized but inspector failed: ${inspected.result.stderr || inspected.result.stdout || "invalid JSON"}`,
|
|
5487
|
-
"error",
|
|
5488
|
-
);
|
|
5489
|
-
return;
|
|
5490
|
-
}
|
|
5491
|
-
setControlState(ctx, planningState("requirement-draft"));
|
|
5492
|
-
await pi.sendUserMessage(
|
|
5493
|
-
[
|
|
5494
|
-
"<devflow-requirement-draft-directive>",
|
|
5495
|
-
`Devflow 已创建 Feature ${feature}。当前为安全的需求梳理阶段:允许澄清和只读证据收集,不得写入项目或迁移状态。`,
|
|
5496
|
-
"请先基于功能名说明你需要的最少业务背景,并邀请用户直接补充目标、使用者、约束和验收结果。收到用户的下一段需求描述后,运行时会将其原文注入 requirement-triage 并启动该步骤。",
|
|
5497
|
-
"不要让用户输入命令;也不要把普通需求文本当成批准或 checkpoint 确认。",
|
|
5498
|
-
"</devflow-requirement-draft-directive>",
|
|
5499
|
-
].join("\n"),
|
|
5827
|
+
const state = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/init_state.rb", [
|
|
5828
|
+
"--feature",
|
|
5829
|
+
feature,
|
|
5830
|
+
"--phase",
|
|
5831
|
+
phase,
|
|
5832
|
+
"--flow",
|
|
5833
|
+
"feature-development",
|
|
5834
|
+
"--write",
|
|
5835
|
+
"--json",
|
|
5836
|
+
...(previousWorkflowComplete ? ["--force"] : []),
|
|
5837
|
+
]);
|
|
5838
|
+
if (!controlPlaneSucceeded(state)) {
|
|
5839
|
+
ctx.ui.notify((state.stderr || state.stdout).slice(0, 6000), "error");
|
|
5840
|
+
return;
|
|
5841
|
+
}
|
|
5842
|
+
const artifacts = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/sync_artifacts.rb", [
|
|
5843
|
+
"--write",
|
|
5844
|
+
"--json",
|
|
5845
|
+
]);
|
|
5846
|
+
if (!controlPlaneSucceeded(artifacts)) {
|
|
5847
|
+
ctx.ui.notify((artifacts.stderr || artifacts.stdout).slice(0, 6000), "error");
|
|
5848
|
+
return;
|
|
5849
|
+
}
|
|
5850
|
+
const inspected = await inspectWorkflow(ctx.cwd);
|
|
5851
|
+
if (!inspected.result.ok || !inspected.snapshot) {
|
|
5852
|
+
ctx.ui.notify(
|
|
5853
|
+
`Devflow initialized but inspector failed: ${inspected.result.stderr || inspected.result.stdout || "invalid JSON"}`,
|
|
5854
|
+
"error",
|
|
5500
5855
|
);
|
|
5501
5856
|
return;
|
|
5502
5857
|
}
|
|
5503
|
-
ctx
|
|
5504
|
-
|
|
5505
|
-
|
|
5858
|
+
setControlState(ctx, planningState("requirement-draft"));
|
|
5859
|
+
await pi.sendUserMessage(
|
|
5860
|
+
[
|
|
5861
|
+
"<devflow-requirement-draft-directive>",
|
|
5862
|
+
`Devflow 已创建 Feature ${feature}。当前为安全的需求梳理阶段:允许澄清和只读证据收集,不得写入项目或迁移状态。`,
|
|
5863
|
+
"请先基于功能名说明你需要的最少业务背景,并邀请用户直接补充目标、使用者、约束和验收结果。收到用户的下一段需求描述后,运行时会将其原文注入 requirement-triage 并启动该步骤。",
|
|
5864
|
+
"不要让用户输入命令;也不要把普通需求文本当成批准或 checkpoint 确认。",
|
|
5865
|
+
"</devflow-requirement-draft-directive>",
|
|
5866
|
+
].join("\n"),
|
|
5506
5867
|
);
|
|
5507
|
-
|
|
5868
|
+
return;
|
|
5869
|
+
}
|
|
5870
|
+
ctx.ui.notify(
|
|
5871
|
+
"用法:/devflow(查看状态) | /devflow next(下一步) | /devflow fix(诊断修复) | /devflow cancel [原因] | /devflow control | /devflow start <需求名> | /devflow project-init | /devflow doctor\n兼容旧命令:status [--json] | explain-state | recover | abandon | resume",
|
|
5872
|
+
"warning",
|
|
5873
|
+
);
|
|
5874
|
+
};
|
|
5875
|
+
devflowCommandRef = devflowCommand;
|
|
5876
|
+
|
|
5877
|
+
pi.registerCommand("devflow", {
|
|
5878
|
+
description: "Devflow runtime: inspect, initialize, plan, run one step, or explicitly advance the controlled delivery pipeline",
|
|
5879
|
+
handler: devflowCommand,
|
|
5508
5880
|
});
|
|
5509
5881
|
}
|