@guanlin4924/devflow-pi 0.1.52 → 0.1.53
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 +1 -1
- package/plugins/devflow-core/extensions/devflow-core.ts +23 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_coverage.rb +37 -2
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +6 -0
- package/plugins/devflow-requirements/skills/workflow-wayfinder/SKILL.md +23 -0
- package/src/constants.ts +1 -1
package/package.json
CHANGED
|
@@ -369,7 +369,7 @@ export function isRequirementDraftToolAllowed(toolName: string, input: unknown,
|
|
|
369
369
|
return normalized === "task" && taskAgents.length > 0 && taskAgents.every(agent => PLANNING_AGENTS.has(agent));
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
-
type InternalDevflowControlResource = "contract" | "state" | "mainflow" | "requirement_graph";
|
|
372
|
+
type InternalDevflowControlResource = "contract" | "state" | "mainflow" | "requirement_graph" | "requirement_coverage";
|
|
373
373
|
|
|
374
374
|
/** Resolve an xdev target only when all supplied target aliases agree.
|
|
375
375
|
* A direct/eval caller can otherwise provide `path` for a real file plus an
|
|
@@ -395,6 +395,7 @@ export function internalDevflowControlResource(toolName: string, input: unknown)
|
|
|
395
395
|
if (target === "xd://devflow_state") return "state";
|
|
396
396
|
if (target === "xd://devflow_mainflow") return "mainflow";
|
|
397
397
|
if (target === "xd://devflow_requirement_graph") return "requirement_graph";
|
|
398
|
+
if (target === "xd://devflow_requirement_coverage") return "requirement_coverage";
|
|
398
399
|
return undefined;
|
|
399
400
|
}
|
|
400
401
|
|
|
@@ -1118,6 +1119,7 @@ function stepScopedMutationOwner(toolName: string, input: unknown): string | nul
|
|
|
1118
1119
|
if (["write", "edit", "apply_patch", "notebook_edit", "task", "bash", "shell", "exec_command"].includes(normalized)) return null;
|
|
1119
1120
|
if (!normalized.startsWith("devflow_") || CONTRACT_CONTROL_TOOLS.has(normalized) || CONTRACT_READ_TOOLS.has(normalized)) return undefined;
|
|
1120
1121
|
const action = inputAction(input);
|
|
1122
|
+
if (normalized === "devflow_requirement_coverage") return action === "validate" ? undefined : "requirement-wayfinder";
|
|
1121
1123
|
if (normalized === "devflow_code_index") return action === "scan" ? "code-index-refresh" : undefined;
|
|
1122
1124
|
if (normalized === "devflow_requirement_graph") {
|
|
1123
1125
|
const writeContext = input && typeof input === "object" && (input as Record<string, unknown>).write === true;
|
|
@@ -2398,7 +2400,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2398
2400
|
recoveredApprovalState = true;
|
|
2399
2401
|
}
|
|
2400
2402
|
}
|
|
2401
|
-
if (recoveredApprovalState || controlState.phase === "awaiting-approval" || controlState.phase === "waiting-user") {
|
|
2403
|
+
if (recoveredApprovalState || controlState.phase === "awaiting-approval" || controlState.phase === "waiting-user" || (controlState.phase === "blocked" && controlState.label === "推进校验失败")) {
|
|
2402
2404
|
if (controlAction === "reject") {
|
|
2403
2405
|
ctx.ui.notify("已保留当前步骤和产物。请直接说明需要修改的内容;未执行任何推进。", "info");
|
|
2404
2406
|
return { handled: true };
|
|
@@ -2437,7 +2439,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2437
2439
|
]);
|
|
2438
2440
|
if (!result.ok) {
|
|
2439
2441
|
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "推进校验失败" });
|
|
2440
|
-
ctx.ui.notify(`未推进:${result.stderr || result.stdout}
|
|
2442
|
+
ctx.ui.notify(`未推进:${result.stderr || result.stdout}\n请补齐上述校验缺项后重新审批;重复批准不会绕过门禁。可请求重新执行当前步骤以修复产物。`, "error");
|
|
2441
2443
|
return { handled: true };
|
|
2442
2444
|
}
|
|
2443
2445
|
const after = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
@@ -2459,6 +2461,10 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2459
2461
|
return { handled: true };
|
|
2460
2462
|
}
|
|
2461
2463
|
}
|
|
2464
|
+
if (controlAction) {
|
|
2465
|
+
ctx.ui.notify("当前没有可推进的审批节点。请先完成或修复当前步骤产物,再请求审批;状态未推进。", "info");
|
|
2466
|
+
return { handled: true };
|
|
2467
|
+
}
|
|
2462
2468
|
const intent = classifyNaturalLanguageIntent(event.text);
|
|
2463
2469
|
if (intent === "explain-state") {
|
|
2464
2470
|
const inspected = await inspectWorkflow(ctx.cwd);
|
|
@@ -2826,6 +2832,20 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2826
2832
|
},
|
|
2827
2833
|
});
|
|
2828
2834
|
|
|
2835
|
+
pi.registerTool({
|
|
2836
|
+
name: "devflow_requirement_coverage",
|
|
2837
|
+
label: "Devflow Requirement Coverage",
|
|
2838
|
+
description: "Generate Core source units from a requirement source file, or validate wayfinder coverage before requesting approval. Generation never creates coverage claims.",
|
|
2839
|
+
parameters: z.object({ action: z.enum(["generate", "validate"]), feature: z.string().min(1), source: z.string().optional() }),
|
|
2840
|
+
approval: args => args.action === "generate" ? "write" : "read",
|
|
2841
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
2842
|
+
return renderResult(await runRuby(ctx.cwd, "workflow-orchestrator/scripts/requirement_coverage.rb", [
|
|
2843
|
+
"--root", ctx.cwd, "--feature", params.feature,
|
|
2844
|
+
...(params.action === "generate" ? ["--source", params.source ?? "", "--write"] : []),
|
|
2845
|
+
]));
|
|
2846
|
+
},
|
|
2847
|
+
});
|
|
2848
|
+
|
|
2829
2849
|
pi.registerTool({
|
|
2830
2850
|
|
|
2831
2851
|
name: "devflow_requirement_graph",
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
# Canonical wayfinder quality checker. It validates references and structured
|
|
5
5
|
# coverage only; AI-provided equivalence labels are never trusted.
|
|
6
6
|
require "digest"
|
|
7
|
+
require "date"
|
|
7
8
|
require "json"
|
|
8
9
|
require "optparse"
|
|
9
10
|
require "yaml"
|
|
@@ -12,6 +13,33 @@ require_relative "requirement_graph"
|
|
|
12
13
|
module RequirementCoverage
|
|
13
14
|
module_function
|
|
14
15
|
|
|
16
|
+
def generate(root:, feature:, source:)
|
|
17
|
+
state = YAML.safe_load(File.read(File.join(root, ".planning/STATE.md")).split(/^---\s*$/, 3)[1], permitted_classes: [Date], aliases: false)
|
|
18
|
+
raise ArgumentError, "source generation requires the active wayfinder step" unless state["current_feature"] == feature && state["current_step"] == "requirement-wayfinder"
|
|
19
|
+
source_path = File.realpath(File.expand_path(source, root))
|
|
20
|
+
raise ArgumentError, "source must be a project file" unless source_path.start_with?(File.realpath(root) + File::SEPARATOR)
|
|
21
|
+
text = File.read(source_path)
|
|
22
|
+
fingerprint = sha(text)
|
|
23
|
+
units = text.lines.each_with_index.map do |line, index|
|
|
24
|
+
excerpt = line.strip
|
|
25
|
+
next if excerpt.empty?
|
|
26
|
+
id = "SU-#{sha("#{source_path.delete_prefix(File.realpath(root))}:#{index + 1}:#{excerpt}")[0, 16]}"
|
|
27
|
+
{ "stable_unit_id" => id, "source_document_fingerprint" => fingerprint,
|
|
28
|
+
"excerpt" => excerpt, "excerpt_fingerprint" => sha(excerpt), "line" => index + 1,
|
|
29
|
+
"required_assertions" => [{ "assertion_id" => "#{id}-A1", "source_unit_id" => id,
|
|
30
|
+
"type" => "source_statement", "predicate" => excerpt }] }
|
|
31
|
+
end.compact
|
|
32
|
+
raise ArgumentError, "source must not be empty" if units.empty?
|
|
33
|
+
directory = RequirementGraph.feature_dir(root, feature)
|
|
34
|
+
path = File.join(directory, "source_units.yaml")
|
|
35
|
+
payload = { "schema_version" => 1, "produced_by" => "core.source_unitizer/v1",
|
|
36
|
+
"source_document" => source_path.delete_prefix(File.realpath(root) + File::SEPARATOR),
|
|
37
|
+
"source_document_fingerprint" => fingerprint, "units" => units }
|
|
38
|
+
File.write(path + ".tmp", YAML.dump(payload))
|
|
39
|
+
File.rename(path + ".tmp", path)
|
|
40
|
+
{ "ok" => true, "path" => path, "unit_count" => units.length }
|
|
41
|
+
end
|
|
42
|
+
|
|
15
43
|
def validate(root:, feature:)
|
|
16
44
|
directory = RequirementGraph.feature_dir(root, feature)
|
|
17
45
|
errors = []
|
|
@@ -165,9 +193,16 @@ if $PROGRAM_NAME == __FILE__
|
|
|
165
193
|
OptionParser.new do |opts|
|
|
166
194
|
opts.on("--root PATH") { |value| options[:root] = value }
|
|
167
195
|
opts.on("--feature NAME") { |value| options[:feature] = value }
|
|
196
|
+
opts.on("--source PATH") { |value| options[:source] = value }
|
|
197
|
+
opts.on("--write") { options[:write] = true }
|
|
168
198
|
end.parse!
|
|
169
199
|
abort("--root and --feature are required") unless options[:root] && options[:feature]
|
|
170
|
-
payload =
|
|
200
|
+
payload = if options[:write]
|
|
201
|
+
abort("--source is required") if options[:source].to_s.empty?
|
|
202
|
+
RequirementCoverage.generate(root: options[:root], feature: options[:feature], source: options[:source])
|
|
203
|
+
else
|
|
204
|
+
RequirementCoverage.validate(root: options[:root], feature: options[:feature])
|
|
205
|
+
end
|
|
171
206
|
puts JSON.pretty_generate(payload)
|
|
172
|
-
exit(payload["
|
|
207
|
+
exit(payload["ok"] ? 0 : 1)
|
|
173
208
|
end
|
package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml
CHANGED
|
@@ -409,6 +409,12 @@ steps:
|
|
|
409
409
|
- path: ".planning/requirements/{feature}/决策地图.md"
|
|
410
410
|
type: requirement-wayfinder
|
|
411
411
|
required: true
|
|
412
|
+
- path: ".planning/requirements/{feature}/source_units.yaml"
|
|
413
|
+
type: requirement-source-units
|
|
414
|
+
required: true
|
|
415
|
+
- path: ".planning/requirements/{feature}/coverage.yaml"
|
|
416
|
+
type: requirement-coverage
|
|
417
|
+
required: true
|
|
412
418
|
meta_output:
|
|
413
419
|
path: ".planning/requirements/{feature}/决策地图.meta.json"
|
|
414
420
|
fields:
|
|
@@ -13,6 +13,8 @@ Always create or update:
|
|
|
13
13
|
|
|
14
14
|
- `.planning/requirements/{功能名}/决策地图.md`
|
|
15
15
|
- `.planning/requirements/{功能名}/决策地图.meta.json`
|
|
16
|
+
- `.planning/requirements/{功能名}/source_units.yaml` (Core-generated)
|
|
17
|
+
- `.planning/requirements/{功能名}/coverage.yaml`
|
|
16
18
|
- `.planning/requirements/{功能名}/wayfinder/WF-xxx.md` for each ticket
|
|
17
19
|
|
|
18
20
|
`决策地图.meta.json` minimum fields:
|
|
@@ -166,8 +168,29 @@ After resolution, update the map immediately. Do not solve hidden Fog tickets be
|
|
|
166
168
|
|
|
167
169
|
## Phase 4 — Exit Criteria
|
|
168
170
|
|
|
171
|
+
### Required Coverage Gate
|
|
172
|
+
|
|
173
|
+
Before setting ready_for_baseline, call `devflow_requirement_coverage(action="generate", feature=..., source=...)`
|
|
174
|
+
with the project-relative authoritative requirement source file. If the source is spread over several documents,
|
|
175
|
+
first consolidate the complete source with provenance; do not substitute the decision map or silently omit source text.
|
|
176
|
+
Core conservatively preserves every nonempty source line as an assertion; these are source statements, not automatically business rules.
|
|
177
|
+
Never hand-author source_units.yaml or its produced_by marker.
|
|
178
|
+
|
|
179
|
+
Read the generated units and write coverage.yaml with schema_version: 1, units and claims arrays.
|
|
180
|
+
Each units entry has unit_id and disposition: {kind: active}. Each claim has claim_id, source_unit_id,
|
|
181
|
+
assertion_ids, inherited_constraints and an evidence_ref pointing to the actual map/ticket coverage.
|
|
182
|
+
inherited_constraints must preserve the assertion's type and predicate (and any other constraint fields).
|
|
183
|
+
Review each source statement against its evidence before claiming coverage; copying constraints alone is not evidence.
|
|
184
|
+
Deferred/out_of_scope dispositions require real, source-fingerprint-bound approval records; never invent approval IDs.
|
|
185
|
+
|
|
186
|
+
Call `devflow_requirement_coverage(action="validate", feature=...)`. Only verdict=allow satisfies this gate.
|
|
187
|
+
Missing files, uncertain coverage and approval-required results must be repaired or reported as blocked,
|
|
188
|
+
not described as completed. Reuse existing tickets and decisions during repair.
|
|
189
|
+
|
|
169
190
|
Exit this step to `workflow-domain-modeling` only when:
|
|
170
191
|
|
|
192
|
+
- The Core coverage checker returns verdict=allow.
|
|
193
|
+
|
|
171
194
|
- Destination is still valid.
|
|
172
195
|
- All L1/L2 blocking decisions are resolved.
|
|
173
196
|
- Remaining L3/L4 questions are either resolved or explicitly non-blocking.
|
package/src/constants.ts
CHANGED