@guanlin4924/devflow-pi 0.1.48 → 0.1.50

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.48",
3
+ "version": "0.1.50",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -974,7 +974,10 @@ export function isShellControlMutation(toolName: string, input: unknown): boolea
974
974
  command,
975
975
  );
976
976
  const namesControlPath = [...CONTROL_PATHS, ...CONTROL_PREFIXES].some(item => command.includes(item)) || /\.planning\/requirements\//.test(command);
977
- const looksLikeWrite = /(?:^|[;&|]\s*)(?:rm|mv|cp|install|truncate)\b|(?:^|\s)(?:tee|sed\s+-i|perl\s+-i)\b|(?:^|[\s;&|])(?:\d*)>{1,2}(?![>=])\s*\S/.test(
977
+ // `2>` and `2>>` only redirect stderr. They frequently accompany read-only
978
+ // probes of control-plane files and must not be treated as a file mutation.
979
+ // Keep bare `>`/`>>` and explicit stdout `1>`/`1>>` guarded.
980
+ const looksLikeWrite = /(?:^|[;&|]\s*)(?:rm|mv|cp|install|truncate)\b|(?:^|\s)(?:tee|sed\s+-i|perl\s+-i)\b|(?:^|[\s;&|])(?:1)?>{1,2}(?![>=&])\s*\S/.test(
978
981
  command,
979
982
  );
980
983
  return invokesWriter || (namesControlPath && looksLikeWrite);
@@ -1124,27 +1127,112 @@ export default function devflowCore(pi: ExtensionAPI) {
1124
1127
  })}\n`, "utf8");
1125
1128
  }
1126
1129
 
1127
- function controlGuide(state: SessionControlState): string[] {
1128
- const title = "Devflow 操作指引";
1129
- if (state.intent === "project-init" && state.phase === "planning") {
1130
- return [title, "初始化范围待确认:按界面完成选择;批准写入请回复“批准执行”。"];
1131
- }
1132
- if (state.intent === "requirement-draft" && state.phase === "planning") {
1133
- return [title, "需求梳理:直接补充需求正文、目标、边界和验收结果;此时不会推进实现。"];
1134
- }
1135
- if (state.phase === "waiting-user" || state.phase === "awaiting-approval") {
1136
- return [title, "当前节点等待审查:批准请回复“批准执行”;需修改请直接描述修改项。", "状态说明:/devflow explain-state"];
1137
- }
1138
- if (state.phase === "executing") {
1139
- return [title, `正在处理:${state.label}`, "请等待当前步骤输出;完成后会提示你审查。"];
1130
+ type ProjectControlUi = {
1131
+ setStatus(key: string, text: string | undefined): void;
1132
+ setWidget?(key: string, content: string[] | undefined, options?: { placement?: "aboveEditor" | "belowEditor" }): void;
1133
+ };
1134
+
1135
+ let projectControlRefreshRevision = 0;
1136
+
1137
+ function workflowSteps(snapshot: Record<string, unknown>): Array<{ id: string; label: string; status: string }> {
1138
+ if (!Array.isArray(snapshot.workflow_steps)) return [];
1139
+ return snapshot.workflow_steps.flatMap(step => {
1140
+ if (!step || typeof step !== "object") return [];
1141
+ const record = step as Record<string, unknown>;
1142
+ const id = typeof record.id === "string" ? record.id : "";
1143
+ if (!id) return [];
1144
+ return [{
1145
+ id,
1146
+ label: typeof record.label === "string" && record.label ? record.label : id,
1147
+ status: typeof record.status === "string" ? record.status : "unknown",
1148
+ }];
1149
+ });
1150
+ }
1151
+
1152
+ function workflowStepIcon(status: string): string {
1153
+ if (status === "completed") return "✓";
1154
+ if (status === "current") return "●";
1155
+ if (status === "waiting_review") return "◆";
1156
+ if (status === "blocked") return "!";
1157
+ return "○";
1158
+ }
1159
+
1160
+ function projectActionLabel(snapshot: Record<string, unknown>): string {
1161
+ switch (snapshot.action) {
1162
+ case "run_next_step":
1163
+ case "run_parallel_window":
1164
+ case "route_failure_branch":
1165
+ return "可启动当前步骤";
1166
+ case "await_user_confirmation":
1167
+ return "审查产物后批准推进";
1168
+ case "recover_artifact_registry":
1169
+ return "可执行安全恢复(仅 registry)";
1170
+ case "workflow_complete":
1171
+ return "当前 Feature 已完成";
1172
+ case "workflow_cancelled":
1173
+ return "当前 Feature 已取消";
1174
+ case "blocked":
1175
+ return "先处理阻塞证据";
1176
+ default:
1177
+ return "正在重新确定可执行操作";
1140
1178
  }
1141
- if (state.phase === "blocked") {
1142
- return [title, "当前步骤被阻塞:使用 /devflow explain-state 查看证据与可恢复路径。", "仅在系统给出安全恢复计划后使用 /devflow recover。"];
1179
+ }
1180
+
1181
+ function projectExecutionLabel(state: SessionControlState, snapshot: Record<string, unknown>): string {
1182
+ if (state.phase === "executing") return `执行中:${state.label}`;
1183
+ if (state.phase === "waiting-user" || state.phase === "awaiting-approval") return "等待人工审查";
1184
+ if (state.phase === "blocked") return `已阻塞:${state.label}`;
1185
+ if (snapshot.action === "await_user_confirmation") return "等待人工审查";
1186
+ if (snapshot.action === "blocked") return "已阻塞";
1187
+ return "当前会话未运行 Agent";
1188
+ }
1189
+
1190
+ function projectControlPanel(snapshot: Record<string, unknown> | null, state: SessionControlState): string[] {
1191
+ if (!snapshot) {
1192
+ return ["Devflow 当前 Feature 交付链路", "尚未读取到有效控制面状态。"];
1143
1193
  }
1144
- return [title, "初始化项目:/devflow project-init", "创建功能:/devflow start <功能名> 查看状态:/devflow explain-state"];
1194
+ const steps = workflowSteps(snapshot);
1195
+ const completedCount = steps.filter(step => step.status === "completed").length;
1196
+ const current = compactStep(snapshot.current_step) ?? compactStep(snapshot.next_step);
1197
+ const recovery = snapshot.recovery_plan && typeof snapshot.recovery_plan === "object"
1198
+ ? snapshot.recovery_plan as Record<string, unknown>
1199
+ : undefined;
1200
+ const recoveryLabel = recovery?.classification === "safe_repairable"
1201
+ ? "已提供安全恢复方案(不推进步骤)"
1202
+ : snapshot.action === "blocked"
1203
+ ? "没有可自动执行的重置方案;请先查看阻塞证据。"
1204
+ : "当前无需恢复或重置。";
1205
+ const chain = steps.length > 0
1206
+ ? steps.map(step => `${workflowStepIcon(step.status)} ${step.label}`).join(" → ")
1207
+ : "尚无可展示的已知工作流步骤。";
1208
+ return [
1209
+ `Devflow 当前 Feature 交付链路 · ${String(snapshot.current_feature ?? "未开始")}`,
1210
+ `阶段:${String(snapshot.current_phase ?? "unknown")} Revision:${String(snapshot.revision ?? "unknown")}`,
1211
+ `链路:${chain}`,
1212
+ `进度:已完成 ${completedCount} / 当前已知 ${steps.length} 步`,
1213
+ `当前节点:${current?.label ? String(current.label) : String(current?.id ?? "未开始")}`,
1214
+ `工作流状态:${String(snapshot.status ?? "unknown")} 执行状态:${projectExecutionLabel(state, snapshot)}`,
1215
+ `当前可执行操作:${projectActionLabel(snapshot)}`,
1216
+ `恢复与重置:${recoveryLabel}`,
1217
+ ];
1218
+ }
1219
+
1220
+ function refreshProjectControlPanel(ctx: { cwd: string; ui: ProjectControlUi }): void {
1221
+ const request = ++projectControlRefreshRevision;
1222
+ void inspectWorkflow(ctx.cwd).then(({ result, snapshot }) => {
1223
+ if (request !== projectControlRefreshRevision) return;
1224
+ const content = result.ok
1225
+ ? projectControlPanel(snapshot, controlState)
1226
+ : ["Devflow 当前 Feature 交付链路", "状态读取失败;未执行任何恢复或状态迁移。"];
1227
+ ctx.ui.setWidget?.("devflow-project-control", content, { placement: "aboveEditor" });
1228
+ }).catch(() => {
1229
+ if (request !== projectControlRefreshRevision) return;
1230
+ ctx.ui.setWidget?.("devflow-project-control", ["Devflow 当前 Feature 交付链路", "状态读取失败;未执行任何恢复或状态迁移。"], { placement: "aboveEditor" });
1231
+ });
1145
1232
  }
1146
1233
 
1147
1234
  function setControlState(ctx: {
1235
+ cwd: string;
1148
1236
  ui: {
1149
1237
  setStatus(key: string, text: string | undefined): void;
1150
1238
  setWidget?(key: string, content: string[] | undefined, options?: { placement?: "aboveEditor" | "belowEditor" }): void;
@@ -1152,7 +1240,7 @@ export default function devflowCore(pi: ExtensionAPI) {
1152
1240
  }, state: SessionControlState) {
1153
1241
  controlState = state;
1154
1242
  ctx.ui.setStatus("devflow-control", statusText(state));
1155
- ctx.ui.setWidget?.("devflow-operation-guide", controlGuide(state), { placement: "aboveEditor" });
1243
+ refreshProjectControlPanel(ctx);
1156
1244
  }
1157
1245
 
1158
1246
  function normalizeContractSegment(value: string, label: string): string {
@@ -1776,6 +1864,127 @@ export default function devflowCore(pi: ExtensionAPI) {
1776
1864
  return true;
1777
1865
  }
1778
1866
 
1867
+ type ProjectControlCommandContext = {
1868
+ cwd: string;
1869
+ sessionManager: { getSessionId(): string };
1870
+ ui: ProjectControlUi & {
1871
+ select(title: string, options: string[]): Promise<string | undefined>;
1872
+ confirm(title: string, body: string): Promise<boolean>;
1873
+ input(title: string, placeholder?: string): Promise<string | undefined>;
1874
+ notify(message: string, level: "info" | "error"): void;
1875
+ };
1876
+ };
1877
+
1878
+ function hasSafeRecoveryPlan(snapshot: Record<string, unknown>): boolean {
1879
+ const plan = snapshot.recovery_plan;
1880
+ return Boolean(plan && typeof plan === "object" && (plan as Record<string, unknown>).classification === "safe_repairable");
1881
+ }
1882
+
1883
+ async function executeSafeRecoveryFromPanel(ctx: ProjectControlCommandContext): Promise<void> {
1884
+ // Do not use inspectWorkflowWithRegistryRecovery here. A view action must
1885
+ // never silently repair the control plane before the user sees and accepts
1886
+ // the currently inspected recovery plan.
1887
+ const inspected = await inspectWorkflow(ctx.cwd);
1888
+ const plan = inspected.snapshot?.recovery_plan as Record<string, unknown> | undefined;
1889
+ const revision = plan?.expected_revision;
1890
+ if (!inspected.result.ok || !inspected.snapshot || !hasSafeRecoveryPlan(inspected.snapshot) || typeof revision !== "number") {
1891
+ ctx.ui.notify("当前没有可执行的安全恢复方案。", "error");
1892
+ return;
1893
+ }
1894
+ const confirmed = await ctx.ui.confirm(
1895
+ "确认执行安全恢复",
1896
+ "将只同步已存在文件对应的 artifact registry;不会推进步骤、确认 checkpoint、重试或重置当前节点。继续吗?",
1897
+ );
1898
+ if (!confirmed) return;
1899
+ const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/repair_control_plane.rb", [
1900
+ "--write", "--confirm", "--expected-revision", String(revision), "--json",
1901
+ ]);
1902
+ if (!controlPlaneSucceeded(result)) {
1903
+ ctx.ui.notify(`安全恢复失败:${result.stderr || result.stdout}`, "error");
1904
+ return;
1905
+ }
1906
+ setControlState(ctx, initialControlState());
1907
+ ctx.ui.notify("控制面 registry 已安全恢复;工作流未推进。", "info");
1908
+ }
1909
+
1910
+ async function cancelFeatureFromPanel(ctx: ProjectControlCommandContext): Promise<void> {
1911
+ const inspected = await inspectWorkflow(ctx.cwd);
1912
+ const snapshot = inspected.snapshot;
1913
+ const revision = typeof snapshot?.revision === "number" ? snapshot.revision : undefined;
1914
+ const feature = typeof snapshot?.current_feature === "string" ? snapshot.current_feature : undefined;
1915
+ const status = typeof snapshot?.status === "string" ? snapshot.status : undefined;
1916
+ if (!inspected.result.ok || revision === undefined || !feature || !status || ["completed", "cancelled", "failed"].includes(status)) {
1917
+ ctx.ui.notify("当前没有可取消的进行中 Feature。", "error");
1918
+ return;
1919
+ }
1920
+ const reason = await ctx.ui.input("取消当前 Feature", "请输入取消原因");
1921
+ if (!reason?.trim()) return;
1922
+ const confirmed = await ctx.ui.confirm(
1923
+ "确认取消当前 Feature",
1924
+ `Feature:${feature}\n原因:${reason.trim()}\n\n将结束当前 Feature、释放项目占用并保留现有工件;未完成步骤不会被标记为完成。继续吗?`,
1925
+ );
1926
+ if (!confirmed) return;
1927
+ const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/advance_state.rb", [
1928
+ "--write", "--json", "--cancel", "--cancel-reason", reason.trim(), "--expected-revision", String(revision),
1929
+ ]);
1930
+ if (!controlPlaneSucceeded(result)) {
1931
+ ctx.ui.notify(`取消当前 Feature 失败:${result.stderr || result.stdout}`, "error");
1932
+ return;
1933
+ }
1934
+ activeContract = undefined;
1935
+ contractAgentCalls = 0;
1936
+ setControlState(ctx, initialControlState());
1937
+ ctx.ui.notify(`Feature“${feature}”已取消;现有工件已保留。`, "info");
1938
+ }
1939
+
1940
+ async function openProjectControlPanel(ctx: ProjectControlCommandContext): Promise<void> {
1941
+ for (;;) {
1942
+ // The control panel is a read-only projection. Each action below reads a
1943
+ // fresh snapshot again before it can mutate anything.
1944
+ const inspected = await inspectWorkflow(ctx.cwd);
1945
+ if (!inspected.result.ok || !inspected.snapshot) {
1946
+ ctx.ui.notify(`项目控制台读取失败:${inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON"}`, "error");
1947
+ return;
1948
+ }
1949
+ const snapshot = inspected.snapshot;
1950
+ const options = ["刷新状态", "查看状态说明"];
1951
+ if (["run_next_step", "run_parallel_window", "route_failure_branch"].includes(String(snapshot.action))) options.push("启动当前可执行步骤");
1952
+ if (snapshot.action === "await_user_confirmation") options.push("审查并批准推进…");
1953
+ if (hasSafeRecoveryPlan(snapshot)) options.push("执行安全恢复…");
1954
+ if (typeof snapshot.current_feature === "string" && !["completed", "cancelled", "failed"].includes(String(snapshot.status))) options.push("取消当前 Feature…");
1955
+ options.push("关闭");
1956
+
1957
+ const selected = await ctx.ui.select(projectControlPanel(snapshot, controlState).join("\n"), options);
1958
+ if (!selected || selected === "关闭") return;
1959
+ if (selected === "刷新状态") continue;
1960
+ if (selected === "查看状态说明") {
1961
+ ctx.ui.notify(explainWorkflowSnapshot(snapshot), "info");
1962
+ return;
1963
+ }
1964
+ if (selected === "启动当前可执行步骤") {
1965
+ const fresh = await inspectWorkflow(ctx.cwd);
1966
+ if (!fresh.result.ok || !fresh.snapshot) {
1967
+ ctx.ui.notify(`启动前状态读取失败:${fresh.result.stderr || fresh.result.stdout}`, "error");
1968
+ return;
1969
+ }
1970
+ await resumeInspectedWorkflow(ctx, fresh.snapshot);
1971
+ return;
1972
+ }
1973
+ if (selected === "审查并批准推进…") {
1974
+ await confirmAndAdvanceWorkflowStep(ctx);
1975
+ return;
1976
+ }
1977
+ if (selected === "执行安全恢复…") {
1978
+ await executeSafeRecoveryFromPanel(ctx);
1979
+ return;
1980
+ }
1981
+ if (selected === "取消当前 Feature…") {
1982
+ await cancelFeatureFromPanel(ctx);
1983
+ return;
1984
+ }
1985
+ }
1986
+ }
1987
+
1779
1988
  async function reconcileParallelLifecycle(event: Partial<SubagentLifecyclePayload>, context: { branch_id: string; attempt: number; branch_revision: number } | undefined): Promise<void> {
1780
1989
  if (!context || !projectRoot || !event.status || event.status === "started") return;
1781
1990
  const workflowInspection = await inspectWorkflow(projectRoot);
@@ -3409,6 +3618,10 @@ export default function devflowCore(pi: ExtensionAPI) {
3409
3618
  ctx.ui.notify((result.stdout || result.stderr).slice(0, 6000), result.ok ? "info" : "error");
3410
3619
  return;
3411
3620
  }
3621
+ if (trimmed === "control") {
3622
+ await openProjectControlPanel(ctx);
3623
+ return;
3624
+ }
3412
3625
  if (trimmed === "explain-state") {
3413
3626
  const inspected = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
3414
3627
  ctx.ui.notify(
@@ -3660,7 +3873,7 @@ export default function devflowCore(pi: ExtensionAPI) {
3660
3873
  return;
3661
3874
  }
3662
3875
  ctx.ui.notify(
3663
- "Usage: /devflow doctor | status | explain-state | recover | abandon [reason] | project-init | start <feature>|<phase> | resume",
3876
+ "Usage: /devflow doctor | status | control | explain-state | recover | abandon [reason] | project-init | start <feature>|<phase> | resume",
3664
3877
  "warning",
3665
3878
  );
3666
3879
  },
@@ -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.48";
2
+ export const PRODUCT_VERSION = "0.1.50";
3
3
  export const PROJECT_SCHEMA_VERSION = 1;
4
4
  export const OMP_RUNTIME_VERSION = "17.2.7";
5
5