@guanlin4924/devflow-pi 0.1.51 → 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 +27 -5
- 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/src/runtime.ts +12 -2
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";
|
|
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
|
|
@@ -394,6 +394,8 @@ export function internalDevflowControlResource(toolName: string, input: unknown)
|
|
|
394
394
|
if (target === "xd://devflow_contract") return "contract";
|
|
395
395
|
if (target === "xd://devflow_state") return "state";
|
|
396
396
|
if (target === "xd://devflow_mainflow") return "mainflow";
|
|
397
|
+
if (target === "xd://devflow_requirement_graph") return "requirement_graph";
|
|
398
|
+
if (target === "xd://devflow_requirement_coverage") return "requirement_coverage";
|
|
397
399
|
return undefined;
|
|
398
400
|
}
|
|
399
401
|
|
|
@@ -1117,6 +1119,7 @@ function stepScopedMutationOwner(toolName: string, input: unknown): string | nul
|
|
|
1117
1119
|
if (["write", "edit", "apply_patch", "notebook_edit", "task", "bash", "shell", "exec_command"].includes(normalized)) return null;
|
|
1118
1120
|
if (!normalized.startsWith("devflow_") || CONTRACT_CONTROL_TOOLS.has(normalized) || CONTRACT_READ_TOOLS.has(normalized)) return undefined;
|
|
1119
1121
|
const action = inputAction(input);
|
|
1122
|
+
if (normalized === "devflow_requirement_coverage") return action === "validate" ? undefined : "requirement-wayfinder";
|
|
1120
1123
|
if (normalized === "devflow_code_index") return action === "scan" ? "code-index-refresh" : undefined;
|
|
1121
1124
|
if (normalized === "devflow_requirement_graph") {
|
|
1122
1125
|
const writeContext = input && typeof input === "object" && (input as Record<string, unknown>).write === true;
|
|
@@ -1781,8 +1784,9 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1781
1784
|
if (!controlPlaneSucceeded(synced)) {
|
|
1782
1785
|
throw new Error(synced.stderr || synced.stdout || "artifact registry sync after rollover prepare failed");
|
|
1783
1786
|
}
|
|
1784
|
-
|
|
1787
|
+
// The runtime can observe the request immediately; arm the switch handler first.
|
|
1785
1788
|
pendingRollover = { snapshot, preparedRevision: payload.revision, rolloverId };
|
|
1789
|
+
await enqueueControlledRolloverRequest(ctx.cwd, rolloverId, ctx.sessionManager.getSessionId());
|
|
1786
1790
|
}
|
|
1787
1791
|
|
|
1788
1792
|
function parallelBranches(snapshot: Record<string, unknown>): Array<{ workflow_step: string; track: string }> {
|
|
@@ -2396,7 +2400,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2396
2400
|
recoveredApprovalState = true;
|
|
2397
2401
|
}
|
|
2398
2402
|
}
|
|
2399
|
-
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 === "推进校验失败")) {
|
|
2400
2404
|
if (controlAction === "reject") {
|
|
2401
2405
|
ctx.ui.notify("已保留当前步骤和产物。请直接说明需要修改的内容;未执行任何推进。", "info");
|
|
2402
2406
|
return { handled: true };
|
|
@@ -2435,7 +2439,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2435
2439
|
]);
|
|
2436
2440
|
if (!result.ok) {
|
|
2437
2441
|
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "推进校验失败" });
|
|
2438
|
-
ctx.ui.notify(`未推进:${result.stderr || result.stdout}
|
|
2442
|
+
ctx.ui.notify(`未推进:${result.stderr || result.stdout}\n请补齐上述校验缺项后重新审批;重复批准不会绕过门禁。可请求重新执行当前步骤以修复产物。`, "error");
|
|
2439
2443
|
return { handled: true };
|
|
2440
2444
|
}
|
|
2441
2445
|
const after = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
|
|
@@ -2457,6 +2461,10 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2457
2461
|
return { handled: true };
|
|
2458
2462
|
}
|
|
2459
2463
|
}
|
|
2464
|
+
if (controlAction) {
|
|
2465
|
+
ctx.ui.notify("当前没有可推进的审批节点。请先完成或修复当前步骤产物,再请求审批;状态未推进。", "info");
|
|
2466
|
+
return { handled: true };
|
|
2467
|
+
}
|
|
2460
2468
|
const intent = classifyNaturalLanguageIntent(event.text);
|
|
2461
2469
|
if (intent === "explain-state") {
|
|
2462
2470
|
const inspected = await inspectWorkflow(ctx.cwd);
|
|
@@ -2824,6 +2832,20 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2824
2832
|
},
|
|
2825
2833
|
});
|
|
2826
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
|
+
|
|
2827
2849
|
pi.registerTool({
|
|
2828
2850
|
|
|
2829
2851
|
name: "devflow_requirement_graph",
|
|
@@ -2850,7 +2872,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2850
2872
|
propagation: z.enum(["one_hop", "full"]).optional(),
|
|
2851
2873
|
}).optional(),
|
|
2852
2874
|
}),
|
|
2853
|
-
approval: (args) => args.action === "propose" || args.action === "confirm" ? "write" : "read",
|
|
2875
|
+
approval: (args) => args.action === "propose" || args.action === "confirm" || (args.action === "context" && args.write === true) ? "write" : "read",
|
|
2854
2876
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
2855
2877
|
const graphWrite = params.action === "propose" || params.action === "confirm" || (params.action === "context" && params.write === true);
|
|
2856
2878
|
if (graphWrite) {
|
|
@@ -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
package/src/runtime.ts
CHANGED
|
@@ -209,7 +209,7 @@ export async function runInteractiveRuntime(project: ProjectContext, options: Ru
|
|
|
209
209
|
// authority remains STATE.rollover, which the extension verifies and claims
|
|
210
210
|
// during session_switch before it starts the next workflow turn.
|
|
211
211
|
const consumeControlledRollover = async (): Promise<void> => {
|
|
212
|
-
if (rolloverConsumptionInFlight) return;
|
|
212
|
+
if (rolloverConsumptionInFlight || interactiveSubmissionInFlight || runtime.session.isStreaming) return;
|
|
213
213
|
rolloverConsumptionInFlight = true;
|
|
214
214
|
try {
|
|
215
215
|
const requestPath = path.join(project.root, ".planning", "rollover-request.json");
|
|
@@ -238,6 +238,8 @@ export async function runInteractiveRuntime(project: ProjectContext, options: Ru
|
|
|
238
238
|
// already durable and STATE remains frozen until the target consumes it.
|
|
239
239
|
let switched = false;
|
|
240
240
|
for (let attempt = 0; attempt < 3 && !switched; attempt += 1) {
|
|
241
|
+
// newSession aborts a running turn; leave the durable request for the poller.
|
|
242
|
+
if (interactiveSubmissionInFlight || runtime.session.isStreaming) return;
|
|
241
243
|
switched = await runtime.session.newSession();
|
|
242
244
|
if (!switched && attempt < 2) await new Promise(resolve => setTimeout(resolve, 100));
|
|
243
245
|
}
|
|
@@ -259,8 +261,11 @@ export async function runInteractiveRuntime(project: ProjectContext, options: Ru
|
|
|
259
261
|
rolloverConsumptionInFlight = false;
|
|
260
262
|
mode.showWarning(`Devflow 会话轮换运行时异常,将自动重试:${String(error)}`);
|
|
261
263
|
scheduleControlledRolloverRetry();
|
|
264
|
+
} finally {
|
|
265
|
+
rolloverConsumptionInFlight = false;
|
|
262
266
|
}
|
|
263
267
|
};
|
|
268
|
+
let interactiveSubmissionInFlight = false;
|
|
264
269
|
let rolloverConsumptionInFlight = false;
|
|
265
270
|
let rolloverRetryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
266
271
|
let rolloverRetryAttempt = 0;
|
|
@@ -287,7 +292,12 @@ export async function runInteractiveRuntime(project: ProjectContext, options: Ru
|
|
|
287
292
|
await consumeControlledRollover();
|
|
288
293
|
while (true) {
|
|
289
294
|
const input = await mode.getUserInput();
|
|
290
|
-
|
|
295
|
+
interactiveSubmissionInFlight = true;
|
|
296
|
+
try {
|
|
297
|
+
await omp.submitInteractiveInput(mode, runtime.session, input);
|
|
298
|
+
} finally {
|
|
299
|
+
interactiveSubmissionInFlight = false;
|
|
300
|
+
}
|
|
291
301
|
await consumeControlledRollover();
|
|
292
302
|
// The submitted turn may create a durable rollover request after the
|
|
293
303
|
// immediate check above returns. Poll once while the UI is waiting for the
|