@guanlin4924/devflow-pi 0.1.49 → 0.1.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -117,7 +117,7 @@ bash .skills/install.sh --apply --force-config
117
117
 
118
118
  ### 窗口操作指引与干净会话
119
119
 
120
- Devflow 会在输入框上方显示与当前状态对应的“Devflow 操作指引”:空闲时给出初始化、创建功能和状态查看入口;需求梳理时提示直接补充需求;节点审查时提示回复“批准执行”或直接描述修改项;阻塞时只展示解释状态和安全恢复入口。
120
+ Devflow 会在输入框上方显示“当前 Feature 交付链路”:它基于只读状态快照展示已知步骤、当前节点、完成数、工作流/执行状态、当前允许的操作,以及安全恢复方案。使用 `/devflow control` 打开交互式控制台:它只会在状态检查器允许时提供启动当前步骤、审查并批准推进、安全恢复或取消当前 Feature;每个有写入影响的操作都会重新读取状态并沿用已有确认流程。面板不提供重试或重置当前节点,避免破坏 revision、checkpoint 和证据链。
121
121
 
122
122
  正式节点经用户批准后,Devflow 会把已校验的状态和产物引用固化为内容校验的 handoff,再自动创建干净 Session 执行下一节点。交接期间工作流冻结;只有新 Session 成功认领并消费同一份 handoff 后才解除冻结。Devflow 不使用 OMP 的模型摘要 handoff,因此 `.planning/STATE.md` 仍是唯一流程事实源。
123
123
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanlin4924/devflow-pi",
3
- "version": "0.1.49",
3
+ "version": "0.1.51",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -4,6 +4,7 @@ import { appendFile, mkdir, rename, writeFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
7
+ import { Box, Text } from "@oh-my-pi/pi-tui";
7
8
  import {
8
9
  TASK_SUBAGENT_LIFECYCLE_CHANNEL,
9
10
  type SubagentLifecyclePayload,
@@ -27,6 +28,96 @@ type ProjectInitializationRequest = {
27
28
  mode: ProjectInitializationMode;
28
29
  };
29
30
 
31
+ const workflowStepNames: Record<string, string> = {
32
+ "requirement-triage": "需求梳理",
33
+ "requirement-wayfinder": "需求路径规划",
34
+ "domain-modeling": "领域建模",
35
+ "requirement-feasibility-review": "需求可行性审查",
36
+ "requirement-baseline": "需求基线确认",
37
+ "wireframe-proposal": "原型方案",
38
+ "wireframe-validate": "原型校验",
39
+ "wireframe-render": "原型预览生成",
40
+ "wireframe-review": "原型审查",
41
+ "api-contract": "接口契约",
42
+ "architecture-feasibility": "架构可行性评估",
43
+ "ui-spec": "界面规格",
44
+ "feature-breakdown": "功能拆分",
45
+ "frontend-tech-spec": "前端技术方案",
46
+ "backend-tech-spec": "后端技术方案",
47
+ "api-call-guide": "接口调用说明",
48
+ "api-supplement": "接口补充设计",
49
+ "integration-verify": "集成验证",
50
+ "change-plan": "变更计划",
51
+ "acceptance-test-design": "验收测试设计",
52
+ "phase-structure": "阶段结构确认",
53
+ "execute": "实现执行",
54
+ "code-index-refresh": "代码索引刷新",
55
+ "code-review": "代码审查",
56
+ "generate-mainflow-cases": "主流程用例生成",
57
+ "run-acceptance-tests": "冻结验收测试执行",
58
+ "release-closeout": "发布收尾与里程碑归档",
59
+ "dual-track-design": "前后端并行设计",
60
+ "acceptance-failed-remediation": "验收失败修复",
61
+ "diagnose": "验收失败诊断",
62
+ "execute-fix": "修复执行",
63
+ };
64
+
65
+ const workflowStatusNames: Record<string, string> = {
66
+ completed: "已完成",
67
+ current: "进行中",
68
+ pending: "未开始",
69
+ waiting_review: "等待审查",
70
+ blocked: "已阻塞",
71
+ failed: "执行失败",
72
+ cancelled: "已取消",
73
+ unknown: "未知",
74
+ };
75
+
76
+ const workflowActionNames: Record<string, string> = {
77
+ run_next_step: "执行下一步骤",
78
+ run_parallel_window: "执行并行步骤",
79
+ route_failure_branch: "进入失败处理分支",
80
+ await_user_confirmation: "等待人工确认",
81
+ recover_artifact_registry: "恢复工件登记",
82
+ workflow_complete: "功能交付已完成",
83
+ workflow_cancelled: "功能交付已取消",
84
+ blocked: "处理阻塞项",
85
+ };
86
+
87
+ function workflowSteps(snapshot: Record<string, unknown>): Array<{ id: string; label: string; status: string }> {
88
+ if (!Array.isArray(snapshot.workflow_steps)) return [];
89
+ return snapshot.workflow_steps.flatMap(step => {
90
+ if (!step || typeof step !== "object") return [];
91
+ const record = step as Record<string, unknown>;
92
+ const id = typeof record.id === "string" ? record.id : "";
93
+ if (!id) return [];
94
+ return [{
95
+ id,
96
+ label: typeof record.label === "string" && record.label ? record.label : id,
97
+ status: typeof record.status === "string" ? record.status : "unknown",
98
+ }];
99
+ });
100
+ }
101
+
102
+ function workflowStepName(id: string | undefined, fallback?: string): string {
103
+ if (id && workflowStepNames[id]) return workflowStepNames[id];
104
+ return fallback || id || "未开始";
105
+ }
106
+
107
+ function workflowStatusName(status: unknown): string {
108
+ return typeof status === "string" ? workflowStatusNames[status] ?? status : "未知";
109
+ }
110
+
111
+ function workflowActionName(action: unknown): string {
112
+ return typeof action === "string" ? workflowActionNames[action] ?? action : "正在确认";
113
+ }
114
+
115
+ function workflowPhaseName(phase: unknown): string {
116
+ if (typeof phase !== "string" || !phase) return "未开始";
117
+ if (phase.startsWith("feature-")) return "功能交付";
118
+ return phase;
119
+ }
120
+
30
121
  type ActiveExecutionContract = {
31
122
  contractId: string;
32
123
  feature: string;
@@ -897,20 +988,21 @@ function projectInitializationDirective(summary: string): string {
897
988
  }
898
989
 
899
990
  function explainWorkflowSnapshot(snapshot: Record<string, unknown> | null): string {
900
- if (!snapshot) return "Devflow 当前没有可解释的工作流状态。可使用 /devflow start <feature> 创建新 Feature。";
991
+ if (!snapshot) return "Devflow 当前没有可解释的工作流状态。可使用 /devflow start <功能名> 创建新功能。";
901
992
  const step = compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step);
902
993
  const lines = [
903
- `Feature:${String(snapshot.current_feature ?? "未开始")}`,
904
- `状态:${String(snapshot.status ?? "unknown")}`,
905
- `动作:${String(snapshot.action ?? "unknown")}`,
906
- `Revision:${String(snapshot.revision ?? "unknown")}`,
994
+ `功能:${String(snapshot.current_feature ?? "未开始")}`,
995
+ `阶段:${workflowPhaseName(snapshot.current_phase)}`,
996
+ `状态:${workflowStatusName(snapshot.status)}`,
997
+ `动作:${workflowActionName(snapshot.action)}`,
998
+ `版本:${String(snapshot.revision ?? "未知")}`,
907
999
  ];
908
- if (step?.id) lines.push(`当前/下一步骤:${String(step.id)}${step.label ? `(${String(step.label)})` : ""}`);
909
- if (snapshot.checkpoint) lines.push(`Checkpoint:${String(snapshot.checkpoint)}`);
1000
+ if (step?.id) lines.push(`当前/下一节点:${workflowStepName(step.id, step.label)}`);
1001
+ if (snapshot.checkpoint) lines.push(`检查点:${String(snapshot.checkpoint)}`);
910
1002
  if (snapshot.action === "workflow_complete") {
911
- lines.push("结论:上一 Feature 已完成;可以使用 /devflow start <新功能名> 创建下一 Feature。");
1003
+ lines.push("结论:上一功能已完成;可以使用 /devflow start <功能名> 创建下一功能。");
912
1004
  } else {
913
- lines.push("结论:这是只读解释;未执行任何状态迁移或文件写入。");
1005
+ lines.push("结论:这是只读说明;未执行任何状态迁移或文件写入。");
914
1006
  }
915
1007
  return lines.join("\n");
916
1008
  }
@@ -1127,35 +1219,119 @@ export default function devflowCore(pi: ExtensionAPI) {
1127
1219
  })}\n`, "utf8");
1128
1220
  }
1129
1221
 
1130
- function controlGuide(state: SessionControlState): string[] {
1131
- const title = "Devflow 操作指引";
1132
- if (state.intent === "project-init" && state.phase === "planning") {
1133
- return [title, "初始化范围待确认:按界面完成选择;批准写入请回复“批准执行”。"];
1134
- }
1135
- if (state.intent === "requirement-draft" && state.phase === "planning") {
1136
- return [title, "需求梳理:直接补充需求正文、目标、边界和验收结果;此时不会推进实现。"];
1137
- }
1138
- if (state.phase === "waiting-user" || state.phase === "awaiting-approval") {
1139
- return [title, "当前节点等待审查:批准请回复“批准执行”;需修改请直接描述修改项。", "状态说明:/devflow explain-state"];
1140
- }
1141
- if (state.phase === "executing") {
1142
- return [title, `正在处理:${state.label}`, "请等待当前步骤输出;完成后会提示你审查。"];
1143
- }
1144
- if (state.phase === "blocked") {
1145
- return [title, "当前步骤被阻塞:使用 /devflow explain-state 查看证据与可恢复路径。", "仅在系统给出安全恢复计划后使用 /devflow recover。"];
1222
+ type ProjectControlUi = {
1223
+ setStatus(key: string, text: string | undefined): void;
1224
+ // OMP widgets accept either plain lines or a component factory. Keep this
1225
+ // interface structural because command contexts are supplied by OMP.
1226
+ setWidget?(key: string, content: unknown, options?: { placement?: "aboveEditor" | "belowEditor" }): void;
1227
+ };
1228
+
1229
+ let projectControlRefreshRevision = 0;
1230
+
1231
+
1232
+ function projectActionLabel(snapshot: Record<string, unknown>): string {
1233
+ switch (snapshot.action) {
1234
+ case "run_next_step":
1235
+ case "run_parallel_window":
1236
+ case "route_failure_branch":
1237
+ return "可启动当前步骤";
1238
+ case "await_user_confirmation":
1239
+ return "审查产物后批准推进";
1240
+ case "recover_artifact_registry":
1241
+ return "可执行安全恢复(仅工件登记)";
1242
+ case "workflow_complete":
1243
+ return "当前功能已完成";
1244
+ case "workflow_cancelled":
1245
+ return "当前功能已取消";
1246
+ case "blocked":
1247
+ return "先处理阻塞证据";
1248
+ default:
1249
+ return "正在重新确定可执行操作";
1146
1250
  }
1147
- return [title, "初始化项目:/devflow project-init", "创建功能:/devflow start <功能名> 查看状态:/devflow explain-state"];
1148
1251
  }
1149
1252
 
1150
- function setControlState(ctx: {
1151
- ui: {
1152
- setStatus(key: string, text: string | undefined): void;
1153
- setWidget?(key: string, content: string[] | undefined, options?: { placement?: "aboveEditor" | "belowEditor" }): void;
1253
+ function projectExecutionLabel(state: SessionControlState, snapshot: Record<string, unknown>): string {
1254
+ if (state.phase === "executing") return `执行中:${state.label}`;
1255
+ if (state.phase === "waiting-user" || state.phase === "awaiting-approval") return "等待人工审查";
1256
+ if (state.phase === "blocked") return `已阻塞:${state.label}`;
1257
+ if (snapshot.action === "await_user_confirmation") return "等待人工审查";
1258
+ if (snapshot.action === "blocked") return "已阻塞";
1259
+ return "当前会话未运行智能体";
1260
+ }
1261
+
1262
+ function projectControlPanel(snapshot: Record<string, unknown> | null, state: SessionControlState): string[] {
1263
+ if (!snapshot) {
1264
+ return ["Devflow 当前功能交付", "── 项目状态 ──", "尚未读取到有效控制面状态。"];
1265
+ }
1266
+ const steps = workflowSteps(snapshot);
1267
+ const completedCount = steps.filter(step => step.status === "completed").length;
1268
+ const selectedStep = compactStep(snapshot.current_step) ?? compactStep(snapshot.next_step);
1269
+ const currentStep = selectedStep
1270
+ ? workflowStepName(selectedStep.id, selectedStep.label)
1271
+ : "未开始";
1272
+ const nextStep = steps.find(step => step.status !== "completed" && step.id !== selectedStep?.id)
1273
+ ?? steps.find(step => step.status !== "completed");
1274
+ const recovery = snapshot.recovery_plan && typeof snapshot.recovery_plan === "object"
1275
+ ? snapshot.recovery_plan as Record<string, unknown>
1276
+ : undefined;
1277
+ const recoveryLabel = recovery?.classification === "safe_repairable"
1278
+ ? "可执行安全恢复(不会推进节点)"
1279
+ : snapshot.action === "blocked"
1280
+ ? "暂无自动重置方案,请先查看阻塞证据"
1281
+ : "当前无需恢复或重置";
1282
+ return [
1283
+ `Devflow 当前功能交付 · ${String(snapshot.current_feature ?? "未开始")}`,
1284
+ `阶段:${workflowPhaseName(snapshot.current_phase)} 版本:${String(snapshot.revision ?? "未知")}`,
1285
+ "── 交付链路 ──",
1286
+ `进度:已完成 ${completedCount} / 当前已知 ${steps.length} 步 工作流:${workflowStatusName(snapshot.status)}`,
1287
+ `当前:${currentStep}${nextStep ? ` 后续:${workflowStepName(nextStep.id, nextStep.label)}` : ""}`,
1288
+ "── 当前节点 ──",
1289
+ `节点状态:${workflowStatusName(selectedStep?.status)} 执行状态:${projectExecutionLabel(state, snapshot)}`,
1290
+ `节点动作:${workflowActionName(snapshot.action)}`,
1291
+ "── 下一步执行 ──",
1292
+ `可执行操作:${projectActionLabel(snapshot)}`,
1293
+ "── 恢复与重置 ──",
1294
+ `处理建议:${recoveryLabel}`,
1295
+ ];
1296
+ }
1297
+
1298
+ function projectControlWidget(lines: string[]) {
1299
+ return (_tui: unknown, theme: { fg(color: "accent" | "muted" | "dim", text: string): string; bold(text: string): string }) => {
1300
+ const panel = new Box(1, 0, undefined, {
1301
+ chars: { topLeft: "╭", topRight: "╮", bottomLeft: "╰", bottomRight: "╯", horizontal: "─", vertical: "│" },
1302
+ color: text => theme.fg("dim", text),
1303
+ });
1304
+ const content = lines.map((line, index) => {
1305
+ if (index === 0) return theme.bold(theme.fg("accent", line));
1306
+ if (line.startsWith("──")) return theme.fg("accent", line);
1307
+ return theme.fg("muted", line);
1308
+ }).join("\n");
1309
+ panel.addChild(new Text(content, 0, 0));
1310
+ return panel;
1154
1311
  };
1312
+ }
1313
+
1314
+ function refreshProjectControlPanel(ctx: { cwd: string; ui: ProjectControlUi }): void {
1315
+ const request = ++projectControlRefreshRevision;
1316
+ void inspectWorkflow(ctx.cwd).then(({ result, snapshot }) => {
1317
+ if (request !== projectControlRefreshRevision) return;
1318
+ const content = result.ok
1319
+ ? projectControlPanel(snapshot, controlState)
1320
+ : ["Devflow 当前功能交付", "── 项目状态 ──", "状态读取失败;未执行任何恢复或状态迁移。"];
1321
+ ctx.ui.setWidget?.("devflow-project-control", projectControlWidget(content), { placement: "aboveEditor" });
1322
+ }).catch(() => {
1323
+ if (request !== projectControlRefreshRevision) return;
1324
+ ctx.ui.setWidget?.("devflow-project-control", projectControlWidget(["Devflow 当前功能交付", "── 项目状态 ──", "状态读取失败;未执行任何恢复或状态迁移。"]), { placement: "aboveEditor" });
1325
+ });
1326
+ }
1327
+
1328
+ function setControlState(ctx: {
1329
+ cwd: string;
1330
+ ui: ProjectControlUi;
1155
1331
  }, state: SessionControlState) {
1156
1332
  controlState = state;
1157
1333
  ctx.ui.setStatus("devflow-control", statusText(state));
1158
- ctx.ui.setWidget?.("devflow-operation-guide", controlGuide(state), { placement: "aboveEditor" });
1334
+ refreshProjectControlPanel(ctx);
1159
1335
  }
1160
1336
 
1161
1337
  function normalizeContractSegment(value: string, label: string): string {
@@ -1779,6 +1955,127 @@ export default function devflowCore(pi: ExtensionAPI) {
1779
1955
  return true;
1780
1956
  }
1781
1957
 
1958
+ type ProjectControlCommandContext = {
1959
+ cwd: string;
1960
+ sessionManager: { getSessionId(): string };
1961
+ ui: ProjectControlUi & {
1962
+ select(title: string, options: string[]): Promise<string | undefined>;
1963
+ confirm(title: string, body: string): Promise<boolean>;
1964
+ input(title: string, placeholder?: string): Promise<string | undefined>;
1965
+ notify(message: string, level: "info" | "error"): void;
1966
+ };
1967
+ };
1968
+
1969
+ function hasSafeRecoveryPlan(snapshot: Record<string, unknown>): boolean {
1970
+ const plan = snapshot.recovery_plan;
1971
+ return Boolean(plan && typeof plan === "object" && (plan as Record<string, unknown>).classification === "safe_repairable");
1972
+ }
1973
+
1974
+ async function executeSafeRecoveryFromPanel(ctx: ProjectControlCommandContext): Promise<void> {
1975
+ // Do not use inspectWorkflowWithRegistryRecovery here. A view action must
1976
+ // never silently repair the control plane before the user sees and accepts
1977
+ // the currently inspected recovery plan.
1978
+ const inspected = await inspectWorkflow(ctx.cwd);
1979
+ const plan = inspected.snapshot?.recovery_plan as Record<string, unknown> | undefined;
1980
+ const revision = plan?.expected_revision;
1981
+ if (!inspected.result.ok || !inspected.snapshot || !hasSafeRecoveryPlan(inspected.snapshot) || typeof revision !== "number") {
1982
+ ctx.ui.notify("当前没有可执行的安全恢复方案。", "error");
1983
+ return;
1984
+ }
1985
+ const confirmed = await ctx.ui.confirm(
1986
+ "确认执行安全恢复",
1987
+ "将只同步已存在文件对应的 artifact registry;不会推进步骤、确认 checkpoint、重试或重置当前节点。继续吗?",
1988
+ );
1989
+ if (!confirmed) return;
1990
+ const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/repair_control_plane.rb", [
1991
+ "--write", "--confirm", "--expected-revision", String(revision), "--json",
1992
+ ]);
1993
+ if (!controlPlaneSucceeded(result)) {
1994
+ ctx.ui.notify(`安全恢复失败:${result.stderr || result.stdout}`, "error");
1995
+ return;
1996
+ }
1997
+ setControlState(ctx, initialControlState());
1998
+ ctx.ui.notify("控制面 registry 已安全恢复;工作流未推进。", "info");
1999
+ }
2000
+
2001
+ async function cancelFeatureFromPanel(ctx: ProjectControlCommandContext): Promise<void> {
2002
+ const inspected = await inspectWorkflow(ctx.cwd);
2003
+ const snapshot = inspected.snapshot;
2004
+ const revision = typeof snapshot?.revision === "number" ? snapshot.revision : undefined;
2005
+ const feature = typeof snapshot?.current_feature === "string" ? snapshot.current_feature : undefined;
2006
+ const status = typeof snapshot?.status === "string" ? snapshot.status : undefined;
2007
+ if (!inspected.result.ok || revision === undefined || !feature || !status || ["completed", "cancelled", "failed"].includes(status)) {
2008
+ ctx.ui.notify("当前没有可取消的进行中功能。", "error");
2009
+ return;
2010
+ }
2011
+ const reason = await ctx.ui.input("取消当前功能", "请输入取消原因");
2012
+ if (!reason?.trim()) return;
2013
+ const confirmed = await ctx.ui.confirm(
2014
+ "确认取消当前功能",
2015
+ `功能:${feature}\n原因:${reason.trim()}\n\n将结束当前功能、释放项目占用并保留现有工件;未完成步骤不会被标记为完成。继续吗?`,
2016
+ );
2017
+ if (!confirmed) return;
2018
+ const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/advance_state.rb", [
2019
+ "--write", "--json", "--cancel", "--cancel-reason", reason.trim(), "--expected-revision", String(revision),
2020
+ ]);
2021
+ if (!controlPlaneSucceeded(result)) {
2022
+ ctx.ui.notify(`取消当前功能失败:${result.stderr || result.stdout}`, "error");
2023
+ return;
2024
+ }
2025
+ activeContract = undefined;
2026
+ contractAgentCalls = 0;
2027
+ setControlState(ctx, initialControlState());
2028
+ ctx.ui.notify(`功能“${feature}”已取消;现有工件已保留。`, "info");
2029
+ }
2030
+
2031
+ async function openProjectControlPanel(ctx: ProjectControlCommandContext): Promise<void> {
2032
+ for (;;) {
2033
+ // The control panel is a read-only projection. Each action below reads a
2034
+ // fresh snapshot again before it can mutate anything.
2035
+ const inspected = await inspectWorkflow(ctx.cwd);
2036
+ if (!inspected.result.ok || !inspected.snapshot) {
2037
+ ctx.ui.notify(`项目控制台读取失败:${inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON"}`, "error");
2038
+ return;
2039
+ }
2040
+ const snapshot = inspected.snapshot;
2041
+ const options = ["刷新状态", "查看状态说明"];
2042
+ if (["run_next_step", "run_parallel_window", "route_failure_branch"].includes(String(snapshot.action))) options.push("启动当前可执行步骤");
2043
+ if (snapshot.action === "await_user_confirmation") options.push("审查并批准推进…");
2044
+ if (hasSafeRecoveryPlan(snapshot)) options.push("执行安全恢复…");
2045
+ if (typeof snapshot.current_feature === "string" && !["completed", "cancelled", "failed"].includes(String(snapshot.status))) options.push("取消当前功能…");
2046
+ options.push("关闭");
2047
+
2048
+ const selected = await ctx.ui.select("项目控制操作", options);
2049
+ if (!selected || selected === "关闭") return;
2050
+ if (selected === "刷新状态") continue;
2051
+ if (selected === "查看状态说明") {
2052
+ ctx.ui.notify(explainWorkflowSnapshot(snapshot), "info");
2053
+ return;
2054
+ }
2055
+ if (selected === "启动当前可执行步骤") {
2056
+ const fresh = await inspectWorkflow(ctx.cwd);
2057
+ if (!fresh.result.ok || !fresh.snapshot) {
2058
+ ctx.ui.notify(`启动前状态读取失败:${fresh.result.stderr || fresh.result.stdout}`, "error");
2059
+ return;
2060
+ }
2061
+ await resumeInspectedWorkflow(ctx, fresh.snapshot);
2062
+ return;
2063
+ }
2064
+ if (selected === "审查并批准推进…") {
2065
+ await confirmAndAdvanceWorkflowStep(ctx);
2066
+ return;
2067
+ }
2068
+ if (selected === "执行安全恢复…") {
2069
+ await executeSafeRecoveryFromPanel(ctx);
2070
+ return;
2071
+ }
2072
+ if (selected === "取消当前功能…") {
2073
+ await cancelFeatureFromPanel(ctx);
2074
+ return;
2075
+ }
2076
+ }
2077
+ }
2078
+
1782
2079
  async function reconcileParallelLifecycle(event: Partial<SubagentLifecyclePayload>, context: { branch_id: string; attempt: number; branch_revision: number } | undefined): Promise<void> {
1783
2080
  if (!context || !projectRoot || !event.status || event.status === "started") return;
1784
2081
  const workflowInspection = await inspectWorkflow(projectRoot);
@@ -3412,6 +3709,10 @@ export default function devflowCore(pi: ExtensionAPI) {
3412
3709
  ctx.ui.notify((result.stdout || result.stderr).slice(0, 6000), result.ok ? "info" : "error");
3413
3710
  return;
3414
3711
  }
3712
+ if (trimmed === "control") {
3713
+ await openProjectControlPanel(ctx);
3714
+ return;
3715
+ }
3415
3716
  if (trimmed === "explain-state") {
3416
3717
  const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
3417
3718
  ctx.ui.notify(
@@ -3663,7 +3964,7 @@ export default function devflowCore(pi: ExtensionAPI) {
3663
3964
  return;
3664
3965
  }
3665
3966
  ctx.ui.notify(
3666
- "Usage: /devflow doctor | status | explain-state | recover | abandon [reason] | project-init | start <feature>|<phase> | resume",
3967
+ "Usage: /devflow doctor | status | control | explain-state | recover | abandon [reason] | project-init | start <feature>|<phase> | resume",
3667
3968
  "warning",
3668
3969
  );
3669
3970
  },
@@ -126,6 +126,39 @@ def step_payload(step, state, root, artifact_by_path)
126
126
  }
127
127
  end
128
128
 
129
+ # This is a read model for the TUI, not a second workflow state. In
130
+ # particular, the current step may be waiting for human review or blocked,
131
+ # while it remains in pending_steps in STATE.md. The inspector action is the
132
+ # authority for that distinction.
133
+ def workflow_steps_payload(steps, state, result)
134
+ completed_ids = Array(state["completed_steps"])
135
+ pending_ids = Array(state["pending_steps"])
136
+ current_id = state["current_step"]
137
+ action = result[:action] || result["action"]
138
+
139
+ Array(steps).map do |step|
140
+ id = step["id"]
141
+ status = if completed_ids.include?(id)
142
+ "completed"
143
+ elsif id == current_id && action == "await_user_confirmation"
144
+ "waiting_review"
145
+ elsif id == current_id && action == "blocked"
146
+ "blocked"
147
+ elsif id == current_id
148
+ "current"
149
+ elsif pending_ids.include?(id)
150
+ "pending"
151
+ else
152
+ "unknown"
153
+ end
154
+ {
155
+ "id" => id,
156
+ "label" => step["label"] || id,
157
+ "status" => status
158
+ }
159
+ end
160
+ end
161
+
129
162
  def dependencies_satisfied?(step, completed_ids)
130
163
  Array(step["depends_on"]).all? { |dependency| completed_ids.include?(dependency) }
131
164
  end
@@ -617,6 +650,9 @@ if defined?(state) && state.is_a?(Hash) && result.is_a?(Hash)
617
650
  source_flow: state["source_flow"],
618
651
  source_workflow_config: state["source_workflow_config"]
619
652
  )
653
+ # Include the declared, currently known workflow chain so a presentation
654
+ # layer never has to infer it from filenames or raw control-plane files.
655
+ result["workflow_steps"] = workflow_steps_payload(steps, state, result) if defined?(steps) && steps.is_a?(Array)
620
656
  end
621
657
 
622
658
  if options[:json]
package/src/constants.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PRODUCT_NAME = "Devflow Pi";
2
- export const PRODUCT_VERSION = "0.1.49";
2
+ export const PRODUCT_VERSION = "0.1.51";
3
3
  export const PROJECT_SCHEMA_VERSION = 1;
4
4
  export const OMP_RUNTIME_VERSION = "17.2.7";
5
5