@guanlin4924/devflow-pi 0.1.83 → 0.1.86

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanlin4924/devflow-pi",
3
- "version": "0.1.83",
3
+ "version": "0.1.86",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -923,6 +923,12 @@ function compactStep(value: unknown): Record<string, unknown> | null {
923
923
  };
924
924
  }
925
925
 
926
+ function selectedWorkflowStep(snapshot: Record<string, unknown>): Record<string, unknown> | null {
927
+ return snapshot.action === "run_parallel_window"
928
+ ? compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step)
929
+ : compactStep(snapshot.current_step) ?? compactStep(snapshot.next_step);
930
+ }
931
+
926
932
  /**
927
933
  * The approval prompt is session-local, while the completed step outputs are
928
934
  * durable. After a rollover, compaction, or runtime restart the in-memory
@@ -968,7 +974,7 @@ export function workflowResumeDisposition(snapshot: Record<string, unknown>): Wo
968
974
 
969
975
  const feature = typeof snapshot.current_feature === "string" ? snapshot.current_feature : "";
970
976
  const revision = snapshot.revision;
971
- const selectedStep = compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step);
977
+ const selectedStep = selectedWorkflowStep(snapshot);
972
978
  const stepId = typeof selectedStep?.id === "string" ? selectedStep.id : "";
973
979
  if (!feature || !stepId || typeof revision !== "number" || !Number.isInteger(revision)) return "idle";
974
980
 
@@ -985,7 +991,7 @@ export function workflowResumeDisposition(snapshot: Record<string, unknown>): Wo
985
991
  }
986
992
 
987
993
  function workflowDirective(snapshot: Record<string, unknown>, source: "start" | "approval", requirementDraft?: string): string {
988
- const selectedStep = compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step);
994
+ const selectedStep = selectedWorkflowStep(snapshot);
989
995
  const envelope = {
990
996
  source,
991
997
  action: snapshot.action,
@@ -1037,7 +1043,7 @@ function projectInitializationDirective(summary: string): string {
1037
1043
 
1038
1044
  function explainWorkflowSnapshot(snapshot: Record<string, unknown> | null): string {
1039
1045
  if (!snapshot) return "Devflow 当前没有可解释的工作流状态。可使用 /devflow start <功能名> 创建新功能。";
1040
- const step = compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step);
1046
+ const step = selectedWorkflowStep(snapshot);
1041
1047
  const lines = [
1042
1048
  `功能:${String(snapshot.current_feature ?? "未开始")}`,
1043
1049
  `阶段:${workflowPhaseName(snapshot.current_phase)}`,
@@ -2045,7 +2051,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2045
2051
  snapshot: Record<string, unknown>,
2046
2052
  ): Promise<WorkflowResumeDisposition> {
2047
2053
  const disposition = workflowResumeDisposition(snapshot);
2048
- const stepId = String((compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step))?.id ?? "当前步骤");
2054
+ const stepId = String(selectedWorkflowStep(snapshot)?.id ?? "当前步骤");
2049
2055
  if (disposition === "execute") {
2050
2056
  setControlState(ctx, executingState(`恢复并执行 ${stepId}`));
2051
2057
  ctx.ui.notify(`Devflow 已恢复到 ${stepId},当前步骤产物尚未完成,正在继续执行。`, "info");
@@ -2598,7 +2604,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2598
2604
  }
2599
2605
  const revision = typeof inspected.snapshot.revision === "number" ? inspected.snapshot.revision : undefined;
2600
2606
  const checkpoint = inspected.snapshot.status === "waiting_checkpoint";
2601
- const step = compactStep(inspected.snapshot.next_step) ?? compactStep(inspected.snapshot.current_step);
2607
+ const step = selectedWorkflowStep(inspected.snapshot);
2602
2608
  const stepId = typeof step?.id === "string" ? step.id : undefined;
2603
2609
  if (revision === undefined || (!checkpoint && !stepId)) {
2604
2610
  ctx.ui.notify("当前状态缺少可确认的 revision 或步骤,拒绝推进。", "error");
@@ -3971,7 +3977,7 @@ export default function devflowCore(pi: ExtensionAPI) {
3971
3977
  const revision = plan?.expected_revision;
3972
3978
  if (!inspected.result.ok || !plan || plan.classification !== "safe_repairable" || typeof revision !== "number") {
3973
3979
  const status = typeof inspected.snapshot?.status === "string" ? inspected.snapshot.status : "unknown";
3974
- const step = compactStep(inspected.snapshot?.next_step) ?? compactStep(inspected.snapshot?.current_step);
3980
+ const step = inspected.snapshot ? selectedWorkflowStep(inspected.snapshot) : null;
3975
3981
  const stepId = typeof step?.id === "string" ? step.id : "当前步骤";
3976
3982
  if (inspected.result.ok && status === "in_progress") {
3977
3983
  ctx.ui.notify(`控制面状态一致,无需恢复。当前步骤是 ${stepId};请审查当前产物后明确回复“批准执行”或提出修改意见。`, "info");
@@ -15,6 +15,12 @@ POLICY = {
15
15
  "requirement-baseline" => { "checkers" => %w[requirement-graph-check business-closure-check slice-independence-check acceptance-coverage-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[requirement rule boundary slice acceptance] },
16
16
  "api-contract" => { "checkers" => %w[requirement-graph-check contract-coverage-check api-test-schema-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[api] },
17
17
  "architecture-feasibility" => { "checkers" => %w[requirement-graph-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[workload_shape workload_constraint decision risk metric read_model] },
18
+ "backend-tech-spec" => { "checkers" => %w[requirement-graph-check change-impact-check], "agents" => ["implementation-worker"], "budget" => 1, "allowed_graph_delta" => %w[decision risk] },
19
+ "api-call-guide" => { "checkers" => %w[requirement-graph-check contract-coverage-check], "agents" => ["implementation-worker"], "budget" => 1, "allowed_graph_delta" => %w[api decision] },
20
+ "ui-spec" => { "checkers" => %w[requirement-graph-check change-impact-check], "agents" => ["implementation-worker"], "budget" => 1, "allowed_graph_delta" => %w[decision risk] },
21
+ "feature-breakdown" => { "checkers" => %w[requirement-graph-check], "agents" => ["implementation-worker"], "budget" => 1, "allowed_graph_delta" => %w[requirement rule] },
22
+ "frontend-tech-spec" => { "checkers" => %w[requirement-graph-check change-impact-check], "agents" => ["implementation-worker"], "budget" => 1, "allowed_graph_delta" => %w[decision risk] },
23
+ "api-supplement" => { "checkers" => %w[requirement-graph-check contract-coverage-check], "agents" => ["implementation-worker"], "budget" => 1, "allowed_graph_delta" => %w[api decision] },
18
24
  "integration-verify" => { "checkers" => %w[requirement-graph-check slice-independence-check contract-coverage-check change-impact-check cross-track-consistency-check business-claim-reference-check business-cross-model-check], "agents" => ["verification-runner"], "budget" => 1, "allowed_graph_delta" => %w[requirement risk decision] },
19
25
  "acceptance-test-design" => { "checkers" => %w[requirement-graph-check contract-coverage-check acceptance-coverage-check api-test-matrix-check test-coverage-check test-obligation-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[test_case] },
20
26
  "change-plan" => { "checkers" => %w[requirement-graph-check slice-independence-check contract-coverage-check api-test-matrix-check test-coverage-check test-obligation-check test-task-binding-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[acceptance] },
@@ -45,6 +51,27 @@ def interpolate(value, state)
45
51
  value.gsub("{feature}", state.fetch("current_feature").to_s).gsub("{phase}", state.fetch("current_phase").to_s)
46
52
  end
47
53
 
54
+ def render_execution_contract_markdown(contract)
55
+ lines = [
56
+ "# Execution Contract",
57
+ "",
58
+ "> Core-generated derived view. The immutable contract JSON under `.planning/contracts/` is authoritative.",
59
+ "",
60
+ "- Contract ID: `#{contract.fetch("contract_id")}`",
61
+ "- Feature: `#{contract.fetch("feature")}`",
62
+ "- Step: `#{contract.fetch("step")}`",
63
+ "- STATE revision: `#{contract.fetch("state_revision")}`",
64
+ "- Requirement graph revision: `#{contract.fetch("graph_revision")}`",
65
+ "- Frozen hash: `#{contract.fetch("frozen_hash")}`",
66
+ "",
67
+ "## Allowed paths",
68
+ "",
69
+ ]
70
+ lines.concat(Array(contract["allowed_paths"]).map { |path| "- `#{path}`" })
71
+ lines.concat(["", "## Allowed agents", "", *Array(contract["allowed_agents"]).map { |agent| "- `#{agent}`" }, "", "## Checkers", "", *Array(contract["checker_ids"]).map { |checker| "- `#{checker}`" }, "", "## Allowed tools", "", *Array(contract["allowed_tools"]).map { |tool| "- `#{tool}`" }, ""])
72
+ lines.join("\n")
73
+ end
74
+
48
75
  root = Dir.pwd
49
76
  lock = %w[create inspect].include?(command) ? DevflowControlIO.acquire_project_lock(root) : nil
50
77
  begin
@@ -92,6 +119,7 @@ begin
92
119
  raise ArgumentError, "--write is required" unless options[:write]
93
120
  destination = File.join(root, ".planning/contracts", options[:feature], "#{contract_id}.json")
94
121
  DevflowControlIO.atomic_write(destination, JSON.pretty_generate(frozen) + "\n")
122
+ DevflowControlIO.atomic_write(File.join(root, ".planning/execution-contract.md"), render_execution_contract_markdown(frozen) + "\n")
95
123
  audit = File.join(root, ".planning/contracts", options[:feature], "control-events.jsonl")
96
124
  existing = File.exist?(audit) ? File.read(audit) : ""
97
125
  DevflowControlIO.atomic_write(audit, existing + JSON.generate({ "event" => "execution_contract_frozen", "contract_id" => contract_id, "state_revision" => state["revision"], "graph_revision" => graph["revision"], "frozen_hash" => frozen["frozen_hash"] }) + "\n")
@@ -212,6 +212,10 @@ def track_boundary(state, pending_ids, completed_ids)
212
212
  end
213
213
 
214
214
  def active_parallel_window(workflow, state, root, step_by_id, completed_ids, pending_ids, artifact_by_path)
215
+ # A failed/unsupported parallel dispatch may be explicitly converted to the
216
+ # declared serial fallback. Keep the join topology in the workflow, but do
217
+ # not re-enter the same parallel window on every inspection.
218
+ return nil if state["parallel_execution_mode"].to_s == "serial_fallback"
215
219
  Array(workflow["parallel_windows"]).each do |window|
216
220
  after_step = window["after_step"]
217
221
  branch_map = window["branches"].is_a?(Hash) ? window["branches"] : {}
@@ -206,7 +206,7 @@ if $PROGRAM_NAME == __FILE__
206
206
  begin
207
207
  options = { action: nil, feature: nil, state_revision: nil, run_revision: nil, branch_id: nil, status: nil, workflow_window: nil, branches: [], state_path: ".planning/STATE.md", json: false }
208
208
  OptionParser.new do |opts|
209
- opts.on("--action ACTION", %w[inspect prepare update retry rebase]) { |value| options[:action] = value }
209
+ opts.on("--action ACTION", %w[inspect prepare update retry rebase serial-fallback]) { |value| options[:action] = value }
210
210
  opts.on("--feature FEATURE") { |value| options[:feature] = value }
211
211
  opts.on("--state-revision REV", Integer) { |value| options[:state_revision] = value }
212
212
  opts.on("--run-revision REV", Integer) { |value| options[:run_revision] = value }
@@ -214,6 +214,8 @@ if $PROGRAM_NAME == __FILE__
214
214
  opts.on("--status STATUS") { |value| options[:status] = value }
215
215
  opts.on("--workflow-window NAME") { |value| options[:workflow_window] = value }
216
216
  opts.on("--state PATH") { |value| options[:state_path] = value }
217
+ opts.on("--step STEP") { |value| options[:serial_step] = value }
218
+ opts.on("--expected-state-revision REV", Integer) { |value| options[:expected_state_revision] = value }
217
219
  opts.on("--branch STEP:TRACK", "Register a branch as workflow_step:track") do |value|
218
220
  workflow_step, track = value.split(":", 2)
219
221
  raise ArgumentError, "--branch must be WORKFLOW_STEP:TRACK" if workflow_step.to_s.empty? || track.to_s.empty?
@@ -233,7 +235,55 @@ if $PROGRAM_NAME == __FILE__
233
235
  ParallelRun.update_branch(root: Dir.pwd, feature: options[:feature], branch_id: options[:branch_id], expected_revision: options[:run_revision], status: options[:status])
234
236
  when "retry"
235
237
  ParallelRun.retry_branch(root: Dir.pwd, feature: options[:feature], branch_id: options[:branch_id], expected_revision: options[:run_revision])
236
- when "rebase"
238
+ when "serial-fallback"
239
+ root = Dir.pwd
240
+ feature = ParallelRun.safe_feature(options[:feature])
241
+ lock = DevflowControlIO.acquire_project_lock(root)
242
+ begin
243
+ record = ParallelRun.load(root, feature) || raise(ArgumentError, "parallel run not found")
244
+ state_path, = DevflowControlIO.resolve_control_path(root, options[:state_path], :state)
245
+ state_text = File.read(state_path)
246
+ front = state_text.match(/\A---\n(.*?)\n---\n/m)&.[](1)
247
+ raise ArgumentError, "STATE.md has no YAML front matter" unless front
248
+ state = YAML.safe_load(front, permitted_classes: [], aliases: false)
249
+ raise ArgumentError, "STATE feature mismatch" unless state["current_feature"].to_s == feature
250
+ raise ArgumentError, "STATE does not reference this parallel run" unless state["parallel_run_ref"].to_s == record["run_id"].to_s
251
+ actual_revision = Integer(state.fetch("revision"))
252
+ if options[:expected_state_revision] && actual_revision != options[:expected_state_revision]
253
+ raise ArgumentError, "STATE revision conflict"
254
+ end
255
+ branches = Array(record["branches"])
256
+ raise ArgumentError, "serial fallback is only safe before any branch succeeds or runs" if branches.any? { |branch| %w[running succeeded].include?(branch["status"].to_s) }
257
+ workflow_path, = DevflowControlIO.resolve_control_path(root, state.fetch("workflow_config"), :workflow)
258
+ workflow = YAML.safe_load(File.read(workflow_path), permitted_classes: [], aliases: false)
259
+ policy = Array(workflow["parallel_windows"]).find { |window| window.is_a?(Hash) && window["id"].to_s == record["workflow_window"].to_s }&.dig("execution_policy", "serial_fallback_order")
260
+ order = Array(policy).map(&:to_s)
261
+ selected = options[:serial_step].to_s
262
+ selected = order.find do |step_id|
263
+ branch = branches.find { |item| item["workflow_step"].to_s == step_id }
264
+ branch && branch["status"].to_s == "pending"
265
+ end if selected.empty?
266
+ raise ArgumentError, "serial fallback has no pending branch" if selected.to_s.empty?
267
+ raise ArgumentError, "serial fallback step is not a pending branch" unless branches.any? { |branch| branch["workflow_step"].to_s == selected && branch["status"].to_s == "pending" }
268
+
269
+ state.delete("parallel_run_ref")
270
+ state["parallel_execution_mode"] = "serial_fallback"
271
+ state["current_step"] = selected
272
+ state["revision"] = actual_revision + 1
273
+ state["state_updated_at"] = Date.today.to_s
274
+ body = state_text.sub(/\A---\n.*?\n---\n/m, "---\n#{state.to_yaml.sub(/\A---\n/, "")}---\n")
275
+ DevflowControlIO.atomic_write(state_path, body)
276
+ record["status"] = "abandoned_serial_fallback"
277
+ record["abandoned_at"] = Time.now.utc.iso8601
278
+ record["abandon_reason"] = "no child Agent lifecycle started; converted to declared serial_fallback_order"
279
+ record["serial_fallback_step"] = selected
280
+ record["updated_at"] = Time.now.utc.iso8601
281
+ ParallelRun.write(root, feature, record)
282
+ result = { "ok" => true, "state_revision" => state["revision"], "serial_step" => selected, "run" => record }
283
+ ensure
284
+ DevflowControlIO.release_lock(lock)
285
+ end
286
+ when "rebase"
237
287
  raise ArgumentError, "--state-revision is required" if options[:state_revision].nil?
238
288
  ParallelRun.rebase_state_revision(root: Dir.pwd, feature: options[:feature], state_revision: options[:state_revision])
239
289
  .then { |run| run ? { "ok" => true, "run" => run } : { "ok" => false, "reason" => "parallel_run_not_found" } }
@@ -175,7 +175,13 @@ unless %w[prepared claimed consumed superseded failed].include?(status)
175
175
  return nil
176
176
  end
177
177
 
178
- YAML.safe_load(File.read(path), permitted_classes: [], aliases: false)
178
+ raw = File.read(path)
179
+ if code == "workflow_config"
180
+ WorkflowGateContract.duplicate_yaml_keys(raw).each do |key_path|
181
+ issues << issue("error", "duplicate_workflow_yaml_key", "Workflow contains a duplicate YAML mapping key", path: key_path)
182
+ end
183
+ end
184
+ YAML.safe_load(raw, permitted_classes: [], aliases: false)
179
185
  rescue Psych::SyntaxError => e
180
186
  issues << issue("error", "invalid_#{code}", e.message, path: path)
181
187
  nil
@@ -14,6 +14,31 @@ module WorkflowGateContract
14
14
 
15
15
  module_function
16
16
 
17
+ # Psych silently keeps the last value for duplicate mapping keys. Detect
18
+ # duplicates before loading a workflow so a checker declaration can never be
19
+ # overwritten without a gate failure.
20
+ def duplicate_yaml_keys(yaml)
21
+ duplicates = []
22
+ walk = lambda do |node, path|
23
+ return unless node
24
+ if node.is_a?(Psych::Nodes::Mapping)
25
+ seen = {}
26
+ children = node.children
27
+ children.each_slice(2) do |key_node, value_node|
28
+ key = key_node.respond_to?(:value) ? key_node.value.to_s : key_node.to_s
29
+ key_path = path.empty? ? key : "#{path}.#{key}"
30
+ duplicates << key_path if seen.key?(key)
31
+ seen[key] = true
32
+ walk.call(value_node, key_path)
33
+ end
34
+ elsif node.respond_to?(:children)
35
+ Array(node.children).each { |child| walk.call(child, path) }
36
+ end
37
+ end
38
+ Psych.parse_stream(yaml).children.each { |document| walk.call(document.root, "") }
39
+ duplicates.uniq
40
+ end
41
+
17
42
  def validate(workflow)
18
43
  errors = []
19
44
  unless workflow.is_a?(Hash)
@@ -139,7 +164,11 @@ if $PROGRAM_NAME == __FILE__
139
164
  opts.on("--json") { options[:json] = true }
140
165
  end.parse!
141
166
  raise ArgumentError, "--workflow is required" unless options[:workflow]
142
- result = WorkflowGateContract.validate(YAML.safe_load(File.read(options[:workflow]), permitted_classes: [], aliases: false))
167
+ raw = File.read(options[:workflow])
168
+ duplicate_errors = WorkflowGateContract.duplicate_yaml_keys(raw).map do |path|
169
+ WorkflowGateContract.error("DUPLICATE_YAML_KEY", "workflow contains a duplicate YAML mapping key", path: path)
170
+ end
171
+ result = duplicate_errors + WorkflowGateContract.validate(YAML.safe_load(raw, permitted_classes: [], aliases: false))
143
172
  puts(options[:json] ? JSON.generate(result) : YAML.dump(result))
144
173
  rescue StandardError => e
145
174
  warn e.message
@@ -70,10 +70,7 @@ governance:
70
70
  - api-test-matrix-check
71
71
  - test-coverage-check
72
72
  - acceptance-test-results-check
73
- - change-impact-check
74
- run-acceptance-tests:
75
73
  - acceptance-coverage-check
76
- - acceptance-test-results-check
77
74
  - change-impact-check
78
75
 
79
76
  artifact_registry:
package/src/constants.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PRODUCT_NAME = "Devflow Pi";
2
- export const PRODUCT_VERSION = "0.1.83";
2
+ export const PRODUCT_VERSION = "0.1.86";
3
3
  export const PROJECT_SCHEMA_VERSION = 1;
4
4
  export const OMP_RUNTIME_VERSION = "17.2.7";
5
5