@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanlin4924/devflow-pi",
3
- "version": "0.1.51",
3
+ "version": "0.1.53",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -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
- await enqueueControlledRolloverRequest(ctx.cwd, rolloverId, ctx.sessionManager.getSessionId());
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}`, "error");
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 = RequirementCoverage.validate(root: options[:root], feature: options[:feature])
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["verdict"] == "allow" ? 0 : 1)
207
+ exit(payload["ok"] ? 0 : 1)
173
208
  end
@@ -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
@@ -1,5 +1,5 @@
1
1
  export const PRODUCT_NAME = "Devflow Pi";
2
- export const PRODUCT_VERSION = "0.1.51";
2
+ export const PRODUCT_VERSION = "0.1.53";
3
3
  export const PROJECT_SCHEMA_VERSION = 1;
4
4
  export const OMP_RUNTIME_VERSION = "17.2.7";
5
5
 
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
- await omp.submitInteractiveInput(mode, runtime.session, input);
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