@guanlin4924/devflow-pi 0.1.101 → 0.1.103
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/skills/wireframe-proposal/SKILL.md +2 -0
- package/plugins/devflow-core/skills/wireframe-render/SKILL.md +2 -0
- package/plugins/devflow-core/skills/wireframe-review/SKILL.md +2 -0
- package/plugins/devflow-core/skills/wireframe-validate/SKILL.md +2 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +98 -27
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checkpoint_meta.rb +12 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_runtime.rb +14 -5
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +4 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +1 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/repair_control_plane.rb +10 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/review_preflight.py +9 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/startup_preflight.py +1 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +99 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/workflow_gate_contract.rb +43 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +23 -1
- package/plugins/devflow-requirements/skills/workflow-requirement-baseline/SKILL.md +3 -1
- package/src/constants.ts +1 -1
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -5,6 +5,8 @@ description: Generate or revise a low-fidelity wireframe proposal from controlle
|
|
|
5
5
|
|
|
6
6
|
# Wireframe Proposal
|
|
7
7
|
|
|
8
|
+
Run only when Core reports `wireframe-proposal` as the current step. A no-UI decision is evaluated and recorded by Core after CP-1; do not infer it from requirement prose, edit workflow state, or self-skip this step.
|
|
9
|
+
|
|
8
10
|
Read the controlled requirement baseline and the wireframe schema/registry. Produce exactly two candidate files:
|
|
9
11
|
|
|
10
12
|
- .devflow/planning/features/{feature}/proposal.yaml
|
|
@@ -5,4 +5,6 @@ description: Render validated canonical wireframe JSON into deterministic Markdo
|
|
|
5
5
|
|
|
6
6
|
# Wireframe Render
|
|
7
7
|
|
|
8
|
+
Run only when Core reports `wireframe-render` as the current step. A no-UI decision is evaluated and recorded by Core after CP-1; do not infer it from requirement prose, edit workflow state, or self-skip this step.
|
|
9
|
+
|
|
8
10
|
Call the workflow-orchestrator wireframe_render.py script only with the current Core-built canonical JSON and the validation metadata. Generate preview Markdown, flow SVG, one SVG per declared state under `wireframes/screens/<screen-id>/<platform>/<state>.svg` (`default.svg` is always present), and the optional static gallery under the wireframe output directory. The `wireframes/screens` directory is a required render output and is covered by the execution contract. The `canonical.meta.json` file is created later by the Core approval path and is not an input to this step. Never read proposal YAML as a substitute, mutate canonical JSON, write approval, or modify workflow state.
|
|
@@ -5,4 +5,6 @@ description: Present validated wireframe previews and unresolved questions for e
|
|
|
5
5
|
|
|
6
6
|
# Wireframe Review
|
|
7
7
|
|
|
8
|
+
Run only when Core reports `wireframe-review` as the current step. A no-UI decision is evaluated and recorded by Core after CP-1; do not infer it from requirement prose, edit workflow state, or self-skip this step.
|
|
9
|
+
|
|
8
10
|
Show the current canonical-derived previews, renderer metadata, validation verdict, and unresolved question IDs. If the verdict is await_user, ask the user to answer the listed blocking questions. If the user approves, return control to the Core checkpoint path. Never write approval, pass runtime confirmation, confirm a checkpoint, or alter canonical/state yourself.
|
|
@@ -5,6 +5,8 @@ description: Run deterministic validation for a wireframe proposal and build can
|
|
|
5
5
|
|
|
6
6
|
# Wireframe Validate
|
|
7
7
|
|
|
8
|
+
Run only when Core reports `wireframe-validate` as the current step. A no-UI decision is evaluated and recorded by Core after CP-1; do not infer it from requirement prose, edit workflow state, or self-skip this step.
|
|
9
|
+
|
|
8
10
|
Run the bundled validator through Core's `devflow_skill_script` tool:
|
|
9
11
|
|
|
10
12
|
```text
|
|
@@ -206,15 +206,18 @@ def build_artifact_registry(root, workflow, state)
|
|
|
206
206
|
end
|
|
207
207
|
|
|
208
208
|
completed = Array(state["completed_steps"])
|
|
209
|
+
skipped = state["skipped_steps"].is_a?(Hash) ? state["skipped_steps"] : {}
|
|
209
210
|
current_step = state["current_step"]
|
|
210
211
|
content_checks = workflow.dig("artifact_registry", "content_checks") || {}
|
|
211
|
-
preservable_statuses = %w[approved completed passed pending_rerun stale failed]
|
|
212
|
+
preservable_statuses = %w[approved completed passed skipped pending_rerun stale failed]
|
|
212
213
|
|
|
213
214
|
artifacts = collect_outputs(workflow).map do |step, output|
|
|
214
215
|
path = interpolate_path(output.fetch("path"), state)
|
|
215
216
|
existing = existing_by_path[path] || {}
|
|
216
217
|
generated_status =
|
|
217
|
-
if
|
|
218
|
+
if skipped.key?(step.fetch("id"))
|
|
219
|
+
"skipped"
|
|
220
|
+
elsif completed.include?(step.fetch("id"))
|
|
218
221
|
step["checkpoint"] ? "approved" : "completed"
|
|
219
222
|
elsif step.fetch("id") == current_step && state["status"] == "waiting_checkpoint"
|
|
220
223
|
"generated"
|
|
@@ -222,7 +225,8 @@ def build_artifact_registry(root, workflow, state)
|
|
|
222
225
|
"planned"
|
|
223
226
|
end
|
|
224
227
|
|
|
225
|
-
|
|
228
|
+
skip_record = skipped[step.fetch("id")]
|
|
229
|
+
validity = skip_record ? { "valid" => true, "issues" => [] } : content_validity(root, path, content_checks)
|
|
226
230
|
artifact_path, = DevflowControlIO.resolve_control_path(root, path, :artifact)
|
|
227
231
|
file_exists = File.exist?(artifact_path)
|
|
228
232
|
status = if preservable_statuses.include?(existing["status"])
|
|
@@ -242,7 +246,8 @@ def build_artifact_registry(root, workflow, state)
|
|
|
242
246
|
"downstream_steps" => Array(existing["downstream_steps"]),
|
|
243
247
|
"exists" => file_exists,
|
|
244
248
|
"content_valid" => validity["valid"],
|
|
245
|
-
"content_issues" => validity["issues"].empty? ? nil : validity["issues"]
|
|
249
|
+
"content_issues" => validity["issues"].empty? ? nil : validity["issues"],
|
|
250
|
+
"skip_reason" => skip_record && skip_record["reason"]
|
|
246
251
|
}.reject { |_, value| value.nil? || value == [] }
|
|
247
252
|
end
|
|
248
253
|
|
|
@@ -429,6 +434,60 @@ def next_pending_step_id(state, pending_ids, step_by_id, completed_ids)
|
|
|
429
434
|
end
|
|
430
435
|
end
|
|
431
436
|
|
|
437
|
+
def conditional_skip_policies(workflow, checkpoint_id)
|
|
438
|
+
policies = workflow.dig("governance", "conditional_step_skips")
|
|
439
|
+
return [] unless policies.is_a?(Hash)
|
|
440
|
+
|
|
441
|
+
policies.each_with_object([]) do |(policy_id, policy), matches|
|
|
442
|
+
next unless policy.is_a?(Hash) && policy["trigger_checkpoint"].to_s == checkpoint_id.to_s
|
|
443
|
+
|
|
444
|
+
matches << [policy_id.to_s, policy]
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Conditional skips are Core-owned transitions. The baseline skill can only
|
|
449
|
+
# submit the typed metadata fact; it cannot edit STATE or omit workflow steps.
|
|
450
|
+
def apply_conditional_step_skips!(root, state, workflow, checkpoint_id, next_revision)
|
|
451
|
+
applied = []
|
|
452
|
+
conditional_skip_policies(workflow, checkpoint_id).each do |policy_id, policy|
|
|
453
|
+
source_path = interpolate_path(policy.fetch("source_path"), state)
|
|
454
|
+
source_abs, = DevflowControlIO.resolve_control_path(root, source_path, :artifact)
|
|
455
|
+
next unless File.file?(source_abs)
|
|
456
|
+
|
|
457
|
+
source = JSON.parse(File.read(source_abs))
|
|
458
|
+
source_field = policy.fetch("source_field").to_s
|
|
459
|
+
next unless source.key?(source_field) && source[source_field] == policy["equals"]
|
|
460
|
+
|
|
461
|
+
step_ids = Array(policy.fetch("steps")).map(&:to_s)
|
|
462
|
+
completed = Array(state["completed_steps"]).map(&:to_s)
|
|
463
|
+
pending = Array(state["pending_steps"]).map(&:to_s)
|
|
464
|
+
conflicts = step_ids.reject { |step_id| pending.include?(step_id) && !completed.include?(step_id) }
|
|
465
|
+
unless conflicts.empty?
|
|
466
|
+
raise ArgumentError, "conditional skip #{policy_id} requires every target step to be pending: #{conflicts.join(', ')}"
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
evidence = {
|
|
470
|
+
"status" => "skipped",
|
|
471
|
+
"policy" => policy_id,
|
|
472
|
+
"reason" => policy.fetch("reason").to_s,
|
|
473
|
+
"source" => source_path,
|
|
474
|
+
"source_field" => source_field,
|
|
475
|
+
"source_value" => source[source_field],
|
|
476
|
+
"source_sha256" => Digest::SHA256.file(source_abs).hexdigest,
|
|
477
|
+
"recorded_at_revision" => next_revision
|
|
478
|
+
}
|
|
479
|
+
state["skipped_steps"] = {} unless state["skipped_steps"].is_a?(Hash)
|
|
480
|
+
step_ids.each { |step_id| state["skipped_steps"][step_id] = evidence.dup }
|
|
481
|
+
checkpoint = policy.fetch("checkpoint").to_s
|
|
482
|
+
state["skipped_checkpoints"] = {} unless state["skipped_checkpoints"].is_a?(Hash)
|
|
483
|
+
state["skipped_checkpoints"][checkpoint] = evidence.merge("steps" => step_ids)
|
|
484
|
+
state["completed_steps"] = (completed + step_ids).uniq
|
|
485
|
+
state["pending_steps"] = pending - step_ids
|
|
486
|
+
applied << { "policy" => policy_id, "steps" => step_ids, "checkpoint" => checkpoint, "reason" => evidence["reason"] }
|
|
487
|
+
end
|
|
488
|
+
applied
|
|
489
|
+
end
|
|
490
|
+
|
|
432
491
|
def track_boundary?(state, pending_ids)
|
|
433
492
|
track = active_track(state)
|
|
434
493
|
return false unless track
|
|
@@ -555,6 +614,7 @@ HARD_FAILURE_REASONS = %w[
|
|
|
555
614
|
parallel_machine_evidence_failed
|
|
556
615
|
parallel_join_failed
|
|
557
616
|
parallel_branch_transition_failed
|
|
617
|
+
conditional_step_skip_failed
|
|
558
618
|
cross_track_consistency_failed
|
|
559
619
|
business_cross_model_check_failed
|
|
560
620
|
gate_boundary_rejected
|
|
@@ -1238,30 +1298,41 @@ result =
|
|
|
1238
1298
|
end
|
|
1239
1299
|
end
|
|
1240
1300
|
if result.nil?
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
changed = true
|
|
1251
|
-
actions << "checkpoint_confirmed_track_complete"
|
|
1252
|
-
action_description = "Checkpoint confirmed. Active track output scope is complete."
|
|
1253
|
-
next_intent_requested = true
|
|
1254
|
-
else
|
|
1255
|
-
state["checkpoint_decision"] = "approved"
|
|
1256
|
-
state["current_step"] = next_id || pending.first
|
|
1257
|
-
state["status"] = "in_progress"
|
|
1258
|
-
state.delete("checkpoint")
|
|
1259
|
-
state.delete("checkpoint_label")
|
|
1260
|
-
changed = true
|
|
1261
|
-
actions << "checkpoint_confirmed"
|
|
1262
|
-
action_description = "Checkpoint confirmed. Advanced to next step."
|
|
1263
|
-
next_intent_requested = true
|
|
1301
|
+
begin
|
|
1302
|
+
applied_skips = apply_conditional_step_skips!(root, state, workflow, state["checkpoint"], current_revision + 1)
|
|
1303
|
+
unless applied_skips.empty?
|
|
1304
|
+
actions << "conditional_steps_skipped"
|
|
1305
|
+
actions.concat(applied_skips.flat_map { |entry| entry["steps"].map { |step_id| "step_skipped:#{step_id}" } })
|
|
1306
|
+
end
|
|
1307
|
+
rescue JSON::ParserError, KeyError, ArgumentError, DevflowControlIO::InvalidControlPath => error
|
|
1308
|
+
result = fail_result("conditional_step_skip_failed", "message" => error.message, "checkpoint" => state["checkpoint"])
|
|
1309
|
+
end
|
|
1264
1310
|
end
|
|
1311
|
+
if result.nil?
|
|
1312
|
+
pending = Array(state["pending_steps"])
|
|
1313
|
+
completed = Array(state["completed_steps"])
|
|
1314
|
+
next_id = next_pending_step_id(state, pending, step_by_id, completed)
|
|
1315
|
+
if next_id.nil? && track_boundary?(state, pending)
|
|
1316
|
+
state["checkpoint_decision"] = "approved"
|
|
1317
|
+
state["status"] = "blocked"
|
|
1318
|
+
state["block_reason"] = "track_scope_complete"
|
|
1319
|
+
state.delete("checkpoint")
|
|
1320
|
+
state.delete("checkpoint_label")
|
|
1321
|
+
changed = true
|
|
1322
|
+
actions << "checkpoint_confirmed_track_complete"
|
|
1323
|
+
action_description = "Checkpoint confirmed. Active track output scope is complete."
|
|
1324
|
+
next_intent_requested = true
|
|
1325
|
+
else
|
|
1326
|
+
state["checkpoint_decision"] = "approved"
|
|
1327
|
+
state["current_step"] = next_id || pending.first
|
|
1328
|
+
state["status"] = "in_progress"
|
|
1329
|
+
state.delete("checkpoint")
|
|
1330
|
+
state.delete("checkpoint_label")
|
|
1331
|
+
changed = true
|
|
1332
|
+
actions << "checkpoint_confirmed"
|
|
1333
|
+
action_description = "Checkpoint confirmed. Advanced to next step."
|
|
1334
|
+
next_intent_requested = true
|
|
1335
|
+
end
|
|
1265
1336
|
end
|
|
1266
1337
|
end
|
|
1267
1338
|
end
|
|
@@ -4,8 +4,20 @@
|
|
|
4
4
|
require "json"
|
|
5
5
|
|
|
6
6
|
module CheckpointMeta
|
|
7
|
+
LEGACY_APPROVAL_OUTPUTS = {
|
|
8
|
+
"CP-WIREFRAME" => [".devflow/planning/features/{feature}/canonical.meta.json"]
|
|
9
|
+
}.freeze
|
|
10
|
+
|
|
7
11
|
module_function
|
|
8
12
|
|
|
13
|
+
def approval_output?(checkpoint, path)
|
|
14
|
+
return false unless checkpoint.is_a?(Hash)
|
|
15
|
+
|
|
16
|
+
declared = Array(checkpoint["approval_outputs"]).map(&:to_s)
|
|
17
|
+
legacy = LEGACY_APPROVAL_OUTPUTS.fetch(checkpoint["id"].to_s, [])
|
|
18
|
+
(declared + legacy).include?(path.to_s)
|
|
19
|
+
end
|
|
20
|
+
|
|
9
21
|
def validate(root:, state:, checkpoint:)
|
|
10
22
|
return [] unless checkpoint.is_a?(Hash)
|
|
11
23
|
|
|
@@ -35,7 +35,9 @@ module GateRuntime
|
|
|
35
35
|
# During the chain-skill migration Core is diagnostic-only. It still
|
|
36
36
|
# validates the contract shape above, but never blocks a transition on
|
|
37
37
|
# graph, artifact, output, checker, review, or topology observations.
|
|
38
|
-
|
|
38
|
+
# Checkpoint confirmation remains fail-closed because approval-path outputs
|
|
39
|
+
# do not exist until the user approves the waiting checkpoint.
|
|
40
|
+
if contract.fetch("hard_gate_mode", "core").to_s == "chain" && transition_kind.to_s != "confirm_checkpoint"
|
|
39
41
|
return GateResult.allow(**base.merge(evidence: base.fetch(:evidence).merge("core_hard_gate_mode" => "chain", "core_hard_gate_bypassed" => true)))
|
|
40
42
|
end
|
|
41
43
|
return GateResult.block(**base, code: "REVISION_CONFLICT", message: "current revision does not match expected revision", recovery: { "kind" => "reinspect", "expected_revision" => expected_revision }) unless expected_revision.nil? || current_revision.to_i == expected_revision.to_i
|
|
@@ -79,9 +81,12 @@ module GateRuntime
|
|
|
79
81
|
return GateResult.block(**base, code: "REQUIRED_OUTPUT_MISSING", message: "required step output is missing", details: { "missing" => missing_outputs }) unless missing_outputs.empty?
|
|
80
82
|
checkpoint = contract.dig("topology", "checkpoint")
|
|
81
83
|
if checkpoint.is_a?(Hash) && checkpoint["producing_step"].to_s == step_id.to_s
|
|
82
|
-
|
|
83
|
-
unless
|
|
84
|
-
|
|
84
|
+
meta_path = checkpoint["required_meta_output"]
|
|
85
|
+
unless CheckpointMeta.approval_output?(checkpoint, meta_path)
|
|
86
|
+
meta_errors = CheckpointMeta.validate(root: root, state: workflow_state, checkpoint: checkpoint)
|
|
87
|
+
unless meta_errors.empty?
|
|
88
|
+
return GateResult.block(**base, code: "CHECKPOINT_META_INVALID", message: "checkpoint meta output is missing or invalid", details: { "checkpoint" => checkpoint["id"], "errors" => meta_errors })
|
|
89
|
+
end
|
|
85
90
|
end
|
|
86
91
|
end
|
|
87
92
|
invalid_outputs = validate_declared_inputs(contract.dig("outputs", "required_inputs"), root, workflow_state, purpose: :completion)
|
|
@@ -111,12 +116,16 @@ module GateRuntime
|
|
|
111
116
|
checkpoint = contract.dig("topology", "checkpoint")
|
|
112
117
|
checkpoint_matches = checkpoint.is_a?(Hash) && checkpoint["id"].to_s == checkpoint_id
|
|
113
118
|
unless workflow_state["status"].to_s == "waiting_checkpoint" && checkpoint_matches && checkpoint_authorized
|
|
114
|
-
|
|
119
|
+
return GateResult.block(**base, code: "CHECKPOINT_NOT_AUTHORIZED", message: "checkpoint confirmation is not authorized by the runtime", details: { "checkpoint" => checkpoint_id })
|
|
115
120
|
end
|
|
116
121
|
unless workflow_state["completed_steps"].to_a.include?(checkpoint["producing_step"].to_s)
|
|
117
122
|
return GateResult.block(**base, code: "CHECKPOINT_PRODUCER_INCOMPLETE", message: "checkpoint producing step is not completed", details: { "checkpoint" => checkpoint_id })
|
|
118
123
|
end
|
|
119
124
|
if root
|
|
125
|
+
missing_outputs = required_paths(checkpoint["required_outputs"], root, workflow_state)
|
|
126
|
+
unless missing_outputs.empty?
|
|
127
|
+
return GateResult.block(**base, code: "CHECKPOINT_OUTPUT_MISSING", message: "checkpoint required output is missing", details: { "checkpoint" => checkpoint_id, "missing" => missing_outputs })
|
|
128
|
+
end
|
|
120
129
|
meta_errors = CheckpointMeta.validate(root: root, state: workflow_state, checkpoint: checkpoint)
|
|
121
130
|
unless meta_errors.empty?
|
|
122
131
|
return GateResult.block(**base, code: "CHECKPOINT_META_INVALID", message: "checkpoint meta output is missing or invalid", details: { "checkpoint" => checkpoint_id, "errors" => meta_errors })
|
|
@@ -141,13 +141,16 @@ end
|
|
|
141
141
|
# authority for that distinction.
|
|
142
142
|
def workflow_steps_payload(steps, state, result)
|
|
143
143
|
completed_ids = Array(state["completed_steps"])
|
|
144
|
+
skipped_ids = state["skipped_steps"].is_a?(Hash) ? state["skipped_steps"].keys : []
|
|
144
145
|
pending_ids = Array(state["pending_steps"])
|
|
145
146
|
current_id = state["current_step"]
|
|
146
147
|
action = result[:action] || result["action"]
|
|
147
148
|
|
|
148
149
|
Array(steps).map do |step|
|
|
149
150
|
id = step["id"]
|
|
150
|
-
status = if
|
|
151
|
+
status = if skipped_ids.include?(id)
|
|
152
|
+
"skipped"
|
|
153
|
+
elsif completed_ids.include?(id)
|
|
151
154
|
"completed"
|
|
152
155
|
elsif id == current_id && action == "await_user_confirmation"
|
|
153
156
|
"waiting_review"
|
|
@@ -85,7 +85,7 @@ def independent_evidence(root, feature, step, state, workflow, graph, files, che
|
|
|
85
85
|
|
|
86
86
|
{
|
|
87
87
|
"collector" => "core.machine_evidence/v1",
|
|
88
|
-
"state" => { "revision" => state["revision"], "step" => step },
|
|
88
|
+
"state" => { "revision" => state["revision"], "step" => step, "skipped_steps" => state["skipped_steps"] },
|
|
89
89
|
"parallel_binding" => binding,
|
|
90
90
|
"input_fingerprints" => inputs.select { |item| item["exists"] }.map { |item| item["sha256"] },
|
|
91
91
|
"output_fingerprints" => files.select { |item| item["exists"] }.map { |item| item["sha256"] },
|
|
@@ -42,9 +42,18 @@ def build_registry(root, workflow, state)
|
|
|
42
42
|
issues << "missing_required_header(#{checks["required_header"].inspect})" unless head.include?(checks["required_header"])
|
|
43
43
|
end
|
|
44
44
|
completed = Array(state["completed_steps"]).include?(step.fetch("id"))
|
|
45
|
-
|
|
45
|
+
skip_record = state["skipped_steps"].is_a?(Hash) && state["skipped_steps"][step.fetch("id")]
|
|
46
|
+
status = if skip_record
|
|
47
|
+
"skipped"
|
|
48
|
+
elsif completed
|
|
49
|
+
step["checkpoint"] ? "approved" : "completed"
|
|
50
|
+
else
|
|
51
|
+
"planned"
|
|
52
|
+
end
|
|
53
|
+
issues = [] if skip_record
|
|
46
54
|
entry = { "path" => path, "type" => output["type"], "workflow_step" => step.fetch("id"), "produced_by" => step["skill"] || step["action"], "status" => status, "exists" => exists, "content_valid" => issues.empty? }
|
|
47
55
|
entry["checkpoint"] = step["checkpoint"] if step["checkpoint"]
|
|
56
|
+
entry["skip_reason"] = skip_record["reason"] if skip_record
|
|
48
57
|
entry["content_issues"] = issues unless issues.empty?
|
|
49
58
|
entry
|
|
50
59
|
end
|
|
@@ -14,6 +14,14 @@ def sha256(path: Path) -> str:
|
|
|
14
14
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def input_present(path):
|
|
18
|
+
"""A declared input without a trailing "/" may still be a rendered
|
|
19
|
+
directory (e.g. wireframes/screens); accept it when it holds a file."""
|
|
20
|
+
if path.is_file():
|
|
21
|
+
return True
|
|
22
|
+
return path.is_dir() and any(child.is_file() for child in path.rglob("*"))
|
|
23
|
+
|
|
24
|
+
|
|
17
25
|
def within(root: Path, relative: str) -> Path:
|
|
18
26
|
if not isinstance(relative, str) or not relative or Path(relative).is_absolute() or "\x00" in relative:
|
|
19
27
|
raise ValueError("invalid relative path")
|
|
@@ -166,7 +174,7 @@ def preflight(root: Path, feature: str, step: str, contract_path: str,
|
|
|
166
174
|
elif path.exists() or stage != "entry":
|
|
167
175
|
issues.append({"code": "MISSING_INPUT_FACT", "index": index, "path": relative})
|
|
168
176
|
elif producer == "existing":
|
|
169
|
-
if not path
|
|
177
|
+
if not input_present(path):
|
|
170
178
|
issues.append({"code": "MISSING_INPUT_FACT", "index": index, "path": relative})
|
|
171
179
|
elif obligation.get("sha256") is not None and obligation.get("sha256") != sha256(path):
|
|
172
180
|
issues.append({"code": "STALE_INPUT_FACT", "index": index, "path": relative})
|
|
@@ -123,7 +123,7 @@ def startup_preflight(root: Path, feature: str, step: str,
|
|
|
123
123
|
collection = relative.endswith("/")
|
|
124
124
|
try:
|
|
125
125
|
target = review_preflight.within(root, rendered)
|
|
126
|
-
exists = target.is_dir() if collection else
|
|
126
|
+
exists = target.is_dir() if collection else review_preflight.input_present(target)
|
|
127
127
|
except ValueError:
|
|
128
128
|
exists = False
|
|
129
129
|
# Same rule as gate_runtime.rb: a collection input may be absent at entry.
|
|
@@ -27,7 +27,7 @@ module WorkflowStateValidator
|
|
|
27
27
|
pending_steps
|
|
28
28
|
].freeze
|
|
29
29
|
|
|
30
|
-
TERMINAL_ARTIFACT_STATUSES = %w[approved completed passed].freeze
|
|
30
|
+
TERMINAL_ARTIFACT_STATUSES = %w[approved completed passed skipped].freeze
|
|
31
31
|
NON_TERMINAL_ARTIFACT_STATUSES = %w[planned generated pending_rerun stale failed].freeze
|
|
32
32
|
TERMINAL_WORKFLOW_STATUSES = %w[completed failed cancelled].freeze
|
|
33
33
|
# Core quality checks are advisory by default. Keep only structural and
|
|
@@ -70,6 +70,7 @@ module WorkflowStateValidator
|
|
|
70
70
|
return true if value.start_with?("duplicate_", "unknown_", "missing_tracked_step")
|
|
71
71
|
return true if value.start_with?("step_in_", "completed_", "terminal_", "non_completed_")
|
|
72
72
|
return true if value.start_with?("waiting_checkpoint_")
|
|
73
|
+
return true if value.start_with?("skipped_")
|
|
73
74
|
return true if %w[missing_required_output output_not_registered].include?(value)
|
|
74
75
|
|
|
75
76
|
false
|
|
@@ -127,6 +128,7 @@ module WorkflowStateValidator
|
|
|
127
128
|
tracked_ids = completed_ids + pending_ids
|
|
128
129
|
|
|
129
130
|
validate_step_tracking(issues, state, step_ids, completed_ids, pending_ids, tracked_ids)
|
|
131
|
+
validate_skipped_transitions(issues, root, state, workflow, step_by_id, completed_ids, pending_ids)
|
|
130
132
|
if state["parallel_run_ref"]
|
|
131
133
|
validate_parallel_run(issues, root, state, workflow, step_by_id)
|
|
132
134
|
else
|
|
@@ -335,6 +337,91 @@ unless %w[prepared claimed consumed superseded failed].include?(status)
|
|
|
335
337
|
end
|
|
336
338
|
end
|
|
337
339
|
|
|
340
|
+
def validate_skipped_transitions(issues, root, state, workflow, step_by_id, completed_ids, pending_ids)
|
|
341
|
+
skipped_steps = state["skipped_steps"]
|
|
342
|
+
skipped_checkpoints = state["skipped_checkpoints"]
|
|
343
|
+
if skipped_steps && !skipped_steps.is_a?(Hash)
|
|
344
|
+
issues << issue("error", "skipped_steps_invalid", "STATE.skipped_steps must be a mapping")
|
|
345
|
+
return
|
|
346
|
+
end
|
|
347
|
+
if skipped_checkpoints && !skipped_checkpoints.is_a?(Hash)
|
|
348
|
+
issues << issue("error", "skipped_checkpoints_invalid", "STATE.skipped_checkpoints must be a mapping")
|
|
349
|
+
return
|
|
350
|
+
end
|
|
351
|
+
return if skipped_steps.nil? && skipped_checkpoints.nil?
|
|
352
|
+
|
|
353
|
+
policies = workflow.dig("governance", "conditional_step_skips")
|
|
354
|
+
policies = {} unless policies.is_a?(Hash)
|
|
355
|
+
(skipped_steps || {}).each do |step_id, record|
|
|
356
|
+
unless record.is_a?(Hash)
|
|
357
|
+
issues << issue("error", "skipped_step_record_invalid", "Skipped step record must be a mapping", step: step_id)
|
|
358
|
+
next
|
|
359
|
+
end
|
|
360
|
+
policy_id = record["policy"].to_s
|
|
361
|
+
policy = policies[policy_id]
|
|
362
|
+
unless policy.is_a?(Hash) && Array(policy["steps"]).map(&:to_s).include?(step_id.to_s)
|
|
363
|
+
issues << issue("error", "skipped_step_policy_invalid", "Skipped step is not authorized by the workflow policy", step: step_id, policy: policy_id)
|
|
364
|
+
next
|
|
365
|
+
end
|
|
366
|
+
issues << issue("error", "skipped_step_unknown", "Skipped step is not defined in the workflow", step: step_id) unless step_by_id.key?(step_id.to_s)
|
|
367
|
+
issues << issue("error", "skipped_step_not_completed", "Skipped step must be tracked as completed", step: step_id) unless completed_ids.include?(step_id)
|
|
368
|
+
issues << issue("error", "skipped_step_still_pending", "Skipped step cannot remain pending", step: step_id) if pending_ids.include?(step_id)
|
|
369
|
+
issues << issue("error", "skipped_step_status_invalid", "Skipped step record status must be skipped", step: step_id) unless record["status"] == "skipped"
|
|
370
|
+
issues << issue("error", "skipped_step_reason_invalid", "Skipped step reason does not match its policy", step: step_id) unless record["reason"].to_s == policy["reason"].to_s
|
|
371
|
+
recorded_revision = record["recorded_at_revision"]
|
|
372
|
+
unless recorded_revision.is_a?(Integer) && recorded_revision.positive? && recorded_revision <= state.fetch("revision", 0).to_i
|
|
373
|
+
issues << issue("error", "skipped_step_revision_invalid", "Skipped step decision revision is invalid", step: step_id, recorded_at_revision: recorded_revision)
|
|
374
|
+
end
|
|
375
|
+
validate_skip_source(issues, root, state, step_id, record, policy)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
(skipped_checkpoints || {}).each do |checkpoint_id, record|
|
|
379
|
+
unless record.is_a?(Hash)
|
|
380
|
+
issues << issue("error", "skipped_checkpoint_record_invalid", "Skipped checkpoint record must be a mapping", checkpoint: checkpoint_id)
|
|
381
|
+
next
|
|
382
|
+
end
|
|
383
|
+
policy = policies[record["policy"].to_s]
|
|
384
|
+
unless policy.is_a?(Hash) && policy["checkpoint"].to_s == checkpoint_id.to_s
|
|
385
|
+
issues << issue("error", "skipped_checkpoint_policy_invalid", "Skipped checkpoint is not authorized by the workflow policy", checkpoint: checkpoint_id)
|
|
386
|
+
next
|
|
387
|
+
end
|
|
388
|
+
expected_steps = Array(policy["steps"]).map(&:to_s)
|
|
389
|
+
unless Array(record["steps"]).map(&:to_s) == expected_steps && expected_steps.all? { |step_id| skipped_steps.is_a?(Hash) && skipped_steps.key?(step_id) }
|
|
390
|
+
issues << issue("error", "skipped_checkpoint_steps_invalid", "Skipped checkpoint must reference every policy step", checkpoint: checkpoint_id)
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
applied_policies = (skipped_steps || {}).values.each_with_object([]) do |record, matches|
|
|
395
|
+
policy = record.is_a?(Hash) && policies[record["policy"].to_s]
|
|
396
|
+
matches << policy if policy
|
|
397
|
+
end
|
|
398
|
+
applied_policies.uniq.each do |policy|
|
|
399
|
+
checkpoint_id = policy["checkpoint"].to_s
|
|
400
|
+
unless skipped_checkpoints.is_a?(Hash) && skipped_checkpoints.key?(checkpoint_id)
|
|
401
|
+
issues << issue("error", "skipped_checkpoint_missing", "Conditional step skip must record its skipped checkpoint", checkpoint: checkpoint_id)
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def validate_skip_source(issues, root, state, step_id, record, policy)
|
|
407
|
+
expected_path = interpolate_path(policy.fetch("source_path"), state)
|
|
408
|
+
unless record["source"].to_s == expected_path && record["source_field"].to_s == policy["source_field"].to_s && record["source_value"] == policy["equals"]
|
|
409
|
+
issues << issue("error", "skipped_step_source_binding_invalid", "Skipped step source binding does not match its policy", step: step_id)
|
|
410
|
+
return
|
|
411
|
+
end
|
|
412
|
+
source_abs, = DevflowControlIO.resolve_control_path(root, expected_path, :artifact)
|
|
413
|
+
source = JSON.parse(File.read(source_abs))
|
|
414
|
+
source_field = policy.fetch("source_field").to_s
|
|
415
|
+
unless source.key?(source_field) && source[source_field] == policy["equals"]
|
|
416
|
+
issues << issue("error", "skipped_step_source_value_invalid", "Skip source no longer authorizes the skipped step", step: step_id, path: expected_path)
|
|
417
|
+
end
|
|
418
|
+
unless record["source_sha256"].to_s == Digest::SHA256.file(source_abs).hexdigest
|
|
419
|
+
issues << issue("error", "skipped_step_source_fingerprint_invalid", "Skip source fingerprint does not match the recorded decision", step: step_id, path: expected_path)
|
|
420
|
+
end
|
|
421
|
+
rescue KeyError, JSON::ParserError, Errno::ENOENT, DevflowControlIO::InvalidControlPath => error
|
|
422
|
+
issues << issue("error", "skipped_step_source_invalid", "Skip source cannot be verified", step: step_id, message: error.message)
|
|
423
|
+
end
|
|
424
|
+
|
|
338
425
|
def validate_parallel_run(issues, root, state, workflow, step_by_id)
|
|
339
426
|
feature = state["current_feature"].to_s
|
|
340
427
|
run = ParallelRun.load(root, feature)
|
|
@@ -447,7 +534,10 @@ unless %w[prepared claimed consumed superseded failed].include?(status)
|
|
|
447
534
|
end
|
|
448
535
|
|
|
449
536
|
def validate_completed_outputs(issues, root, state, step_by_id, completed_ids, artifact_by_path, strict_artifact_status, workflow)
|
|
537
|
+
skipped_ids = state["skipped_steps"].is_a?(Hash) ? state["skipped_steps"].keys : []
|
|
450
538
|
completed_ids.each do |step_id|
|
|
539
|
+
next if skipped_ids.include?(step_id)
|
|
540
|
+
|
|
451
541
|
step = step_by_id[step_id]
|
|
452
542
|
next unless step
|
|
453
543
|
|
|
@@ -472,8 +562,14 @@ unless %w[prepared claimed consumed superseded failed].include?(status)
|
|
|
472
562
|
validate_step_outputs(issues, root, state, step, artifact_by_path, strict_artifact_status, true)
|
|
473
563
|
checkpoint_id = state["checkpoint"] || step["checkpoint"]
|
|
474
564
|
checkpoint = workflow.fetch("checkpoints", {})[checkpoint_id]
|
|
475
|
-
|
|
476
|
-
|
|
565
|
+
checkpoint = checkpoint.merge("id" => checkpoint_id) if checkpoint.is_a?(Hash)
|
|
566
|
+
meta_path = checkpoint.is_a?(Hash) && checkpoint["required_meta_output"]
|
|
567
|
+
unless CheckpointMeta.approval_output?(checkpoint, meta_path)
|
|
568
|
+
CheckpointMeta.validate(root: root, state: state, checkpoint: checkpoint).each do |entry|
|
|
569
|
+
details = { step: step["id"], checkpoint: checkpoint_id }
|
|
570
|
+
.merge(entry.reject { |key, _| %w[code message].include?(key) })
|
|
571
|
+
issues << issue("error", entry.fetch("code"), entry.fetch("message"), details)
|
|
572
|
+
end
|
|
477
573
|
end
|
|
478
574
|
end
|
|
479
575
|
|
|
@@ -60,6 +60,14 @@ module WorkflowGateContract
|
|
|
60
60
|
end
|
|
61
61
|
errors << error("CHECKPOINT_PRODUCER_MISMATCH", "gate_after must equal producing_step", checkpoint: id) if checkpoint["gate_after"].to_s != checkpoint["producing_step"].to_s
|
|
62
62
|
errors << error("CHECKPOINT_OUTPUTS_INVALID", "required_outputs must be an array", checkpoint: id) unless checkpoint["required_outputs"].is_a?(Array)
|
|
63
|
+
if checkpoint.key?("approval_outputs")
|
|
64
|
+
approval_outputs = checkpoint["approval_outputs"]
|
|
65
|
+
errors << error("CHECKPOINT_APPROVAL_OUTPUTS_INVALID", "approval_outputs must be an array", checkpoint: id) unless approval_outputs.is_a?(Array)
|
|
66
|
+
if approval_outputs.is_a?(Array) && checkpoint["required_outputs"].is_a?(Array)
|
|
67
|
+
undeclared = approval_outputs.map(&:to_s) - checkpoint["required_outputs"].map(&:to_s)
|
|
68
|
+
errors << error("CHECKPOINT_APPROVAL_OUTPUT_UNDECLARED", "approval_outputs must also be required_outputs", checkpoint: id, outputs: undeclared) unless undeclared.empty?
|
|
69
|
+
end
|
|
70
|
+
end
|
|
63
71
|
errors << error("CHECKPOINT_META_FIELDS_INVALID", "required_meta_fields must be an array", checkpoint: id) unless checkpoint["required_meta_fields"].is_a?(Array)
|
|
64
72
|
errors << error("CHECKPOINT_OPTIONS_INVALID", "confirm_options must include approve and needs_fix", checkpoint: id) unless Array(checkpoint["confirm_options"]).sort == %w[approve needs_fix]
|
|
65
73
|
end
|
|
@@ -68,6 +76,41 @@ module WorkflowGateContract
|
|
|
68
76
|
errors << error("CHECKPOINT_REF_NOT_FOUND", "enforced checkpoint is not defined", checkpoint: id) unless checkpoints.key?(id)
|
|
69
77
|
end
|
|
70
78
|
|
|
79
|
+
conditional_skips = workflow.dig("governance", "conditional_step_skips")
|
|
80
|
+
if conditional_skips && !conditional_skips.is_a?(Hash)
|
|
81
|
+
errors << error("CONDITIONAL_STEP_SKIPS_SCHEMA_INVALID", "conditional_step_skips must be a mapping")
|
|
82
|
+
end
|
|
83
|
+
(conditional_skips.is_a?(Hash) ? conditional_skips : {}).each do |policy_id, policy|
|
|
84
|
+
unless policy.is_a?(Hash)
|
|
85
|
+
errors << error("CONDITIONAL_STEP_SKIP_POLICY_INVALID", "conditional step skip policy must be a mapping", policy: policy_id)
|
|
86
|
+
next
|
|
87
|
+
end
|
|
88
|
+
%w[trigger_checkpoint source_path source_field equals reason steps checkpoint].each do |field|
|
|
89
|
+
errors << error("CONDITIONAL_STEP_SKIP_FIELD_MISSING", "conditional step skip field is required", policy: policy_id, field: field) unless policy.key?(field)
|
|
90
|
+
end
|
|
91
|
+
errors << error("CONDITIONAL_STEP_SKIP_TRIGGER_UNKNOWN", "conditional step skip trigger checkpoint is not defined", policy: policy_id, checkpoint: policy["trigger_checkpoint"]) unless checkpoints.key?(policy["trigger_checkpoint"])
|
|
92
|
+
skipped_checkpoint = checkpoints[policy["checkpoint"]]
|
|
93
|
+
if skipped_checkpoint.nil?
|
|
94
|
+
errors << error("CONDITIONAL_STEP_SKIP_CHECKPOINT_UNKNOWN", "conditional step skip checkpoint is not defined", policy: policy_id, checkpoint: policy["checkpoint"])
|
|
95
|
+
elsif skipped_checkpoint["skip_policy"].to_s != policy_id.to_s
|
|
96
|
+
errors << error("CONDITIONAL_STEP_SKIP_CHECKPOINT_MISMATCH", "skipped checkpoint must declare the same skip policy", policy: policy_id, checkpoint: policy["checkpoint"])
|
|
97
|
+
end
|
|
98
|
+
unless policy["equals"] == true || policy["equals"] == false
|
|
99
|
+
errors << error("CONDITIONAL_STEP_SKIP_VALUE_INVALID", "conditional step skip equals value must be boolean", policy: policy_id)
|
|
100
|
+
end
|
|
101
|
+
unless policy["steps"].is_a?(Array) && policy["steps"].any?
|
|
102
|
+
errors << error("CONDITIONAL_STEP_SKIP_STEPS_INVALID", "conditional step skip steps must be a non-empty array", policy: policy_id)
|
|
103
|
+
end
|
|
104
|
+
Array(policy["steps"]).each do |step_id|
|
|
105
|
+
step = step_by_id[step_id.to_s]
|
|
106
|
+
if step.nil?
|
|
107
|
+
errors << error("CONDITIONAL_STEP_SKIP_STEP_UNKNOWN", "conditional step skip references an unknown step", policy: policy_id, step: step_id)
|
|
108
|
+
elsif step["skip_policy"].to_s != policy_id.to_s
|
|
109
|
+
errors << error("CONDITIONAL_STEP_SKIP_STEP_MISMATCH", "skipped step must declare the same skip policy", policy: policy_id, step: step_id)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
71
114
|
step_checkers = workflow.dig("governance", "step_checkers")
|
|
72
115
|
if step_checkers.is_a?(Hash)
|
|
73
116
|
step_checkers.each do |step_id, checker_ids|
|
package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml
CHANGED
|
@@ -29,6 +29,19 @@ governance:
|
|
|
29
29
|
- CP-2
|
|
30
30
|
- CP-3
|
|
31
31
|
- CP-4
|
|
32
|
+
conditional_step_skips:
|
|
33
|
+
no_ui_change:
|
|
34
|
+
trigger_checkpoint: CP-1
|
|
35
|
+
source_path: ".devflow/planning/requirements/baseline.meta.json"
|
|
36
|
+
source_field: ui_change_required
|
|
37
|
+
equals: false
|
|
38
|
+
reason: no_ui_change
|
|
39
|
+
steps:
|
|
40
|
+
- wireframe-proposal
|
|
41
|
+
- wireframe-validate
|
|
42
|
+
- wireframe-render
|
|
43
|
+
- wireframe-review
|
|
44
|
+
checkpoint: CP-WIREFRAME
|
|
32
45
|
step_checkers:
|
|
33
46
|
requirement-baseline:
|
|
34
47
|
- requirement-graph-check
|
|
@@ -234,6 +247,10 @@ checkpoints:
|
|
|
234
247
|
- ".devflow/planning/features/{feature}/wireframes/preview.md"
|
|
235
248
|
- ".devflow/planning/features/{feature}/wireframes/flow.svg"
|
|
236
249
|
- ".devflow/planning/features/{feature}/wireframes/screens"
|
|
250
|
+
# Core writes this only after the user approves the waiting checkpoint.
|
|
251
|
+
# Entry validation defers it; checkpoint confirmation still requires it.
|
|
252
|
+
approval_outputs:
|
|
253
|
+
- ".devflow/planning/features/{feature}/canonical.meta.json"
|
|
237
254
|
required_meta_output: ".devflow/planning/features/{feature}/canonical.meta.json"
|
|
238
255
|
required_meta_fields:
|
|
239
256
|
- prototype_fingerprint
|
|
@@ -247,7 +264,7 @@ checkpoints:
|
|
|
247
264
|
confirm_options:
|
|
248
265
|
- approve
|
|
249
266
|
- needs_fix
|
|
250
|
-
skip_policy:
|
|
267
|
+
skip_policy: no_ui_change
|
|
251
268
|
resume_behavior: "user confirms, then continue with api-contract"
|
|
252
269
|
CP-API:
|
|
253
270
|
name: API contract confirmation
|
|
@@ -520,6 +537,7 @@ steps:
|
|
|
520
537
|
risks: { type: array, required: false }
|
|
521
538
|
key_decisions: { type: array, max: 5, required: false }
|
|
522
539
|
traceability_status: { type: string, required: true }
|
|
540
|
+
ui_change_required: { type: boolean, required: false }
|
|
523
541
|
checkpoint: CP-1
|
|
524
542
|
|
|
525
543
|
- id: wireframe-proposal
|
|
@@ -527,6 +545,7 @@ steps:
|
|
|
527
545
|
label: Wireframe proposal
|
|
528
546
|
skill: wireframe-proposal
|
|
529
547
|
engine_evidence_gate: enforced
|
|
548
|
+
skip_policy: no_ui_change
|
|
530
549
|
depends_on:
|
|
531
550
|
- requirement-baseline
|
|
532
551
|
inputs:
|
|
@@ -548,6 +567,7 @@ steps:
|
|
|
548
567
|
label: Validate wireframe proposal
|
|
549
568
|
skill: wireframe-validate
|
|
550
569
|
engine_evidence_gate: enforced
|
|
570
|
+
skip_policy: no_ui_change
|
|
551
571
|
depends_on:
|
|
552
572
|
- wireframe-proposal
|
|
553
573
|
inputs:
|
|
@@ -572,6 +592,7 @@ steps:
|
|
|
572
592
|
label: Render wireframe previews
|
|
573
593
|
skill: wireframe-render
|
|
574
594
|
engine_evidence_gate: enforced
|
|
595
|
+
skip_policy: no_ui_change
|
|
575
596
|
depends_on:
|
|
576
597
|
- wireframe-validate
|
|
577
598
|
inputs:
|
|
@@ -593,6 +614,7 @@ steps:
|
|
|
593
614
|
label: Wireframe review
|
|
594
615
|
skill: wireframe-review
|
|
595
616
|
engine_evidence_gate: enforced
|
|
617
|
+
skip_policy: no_ui_change
|
|
596
618
|
depends_on:
|
|
597
619
|
- wireframe-render
|
|
598
620
|
inputs:
|
|
@@ -106,7 +106,7 @@ When triage found partial overlap or prior rejection, preserve it as source mate
|
|
|
106
106
|
Always create or update:
|
|
107
107
|
|
|
108
108
|
- `.devflow/planning/requirements/baseline.md` — unified requirement baseline
|
|
109
|
-
- `.devflow/planning/requirements/baseline.meta.json` — structured metadata. Schema defined in `feature-development.yaml` under `steps[requirement-baseline].meta_output.fields`: `review_status`, `fp_count`, `br_count`, `bc_count`, `blocking_pending_count`, `critical_image_missing_count`, `challenge_pending_count`, `code_reality_missing_count`, `current_feature`, `depends_on_count`, plus optional `pending_items`, `risks`, and `
|
|
109
|
+
- `.devflow/planning/requirements/baseline.meta.json` — structured metadata. Schema defined in `feature-development.yaml` under `steps[requirement-baseline].meta_output.fields`: `review_status`, `fp_count`, `br_count`, `bc_count`, `blocking_pending_count`, `critical_image_missing_count`, `challenge_pending_count`, `code_reality_missing_count`, `current_feature`, `depends_on_count`, plus optional `pending_items`, `risks`, `key_decisions`, and `ui_change_required`.
|
|
110
110
|
- `.devflow/planning/requirements/{feature}/traceability.json` — copy the orchestrator `templates/traceability.json` schema and create one atomic `REQ-*` record for every confirmed in-scope requirement. Every record must define `input`、`output`、`behavior`、`orchestration`、`failure`、`verification`; use `N/A` only with a reason when a dimension truly does not apply.
|
|
111
111
|
|
|
112
112
|
Before the CP-1 result is reported, run:
|
|
@@ -666,6 +666,8 @@ Write `.devflow/planning/requirements/baseline.meta.json` with the fields requir
|
|
|
666
666
|
- `challenge_pending_count`: unanswered CR decisions affecting scope, semantics, ownership, compatibility, implementation cost, or downstream contracts
|
|
667
667
|
- `code_reality_missing_count`: required code reality comparisons that were not completed
|
|
668
668
|
|
|
669
|
+
Also write `ui_change_required` as a JSON boolean. Set it to `false` only when the confirmed baseline explicitly states that the feature adds or changes no page, interaction, displayed/input field, or UI permission behavior. Set it to `true` for any UI impact or ambiguity. This field is a candidate fact only: never edit `STATE.md`, skip a step, or bypass a checkpoint. Core evaluates the field after CP-1 is approved and records any controlled skip.
|
|
670
|
+
|
|
669
671
|
Completion gate: baseline and baseline.meta.json are written and contain no unresolved template placeholders. If updating an existing baseline, ALL prior feature points are still present.
|
|
670
672
|
|
|
671
673
|
## Phase 7 — Final Review Gate (MANDATORY — do not skip)
|
package/src/constants.ts
CHANGED