@guanlin4924/devflow-pi 0.1.91 → 0.1.93

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.91",
3
+ "version": "0.1.93",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -137,6 +137,7 @@ type ActiveExecutionContract = {
137
137
  allowedTools: string[];
138
138
  checkerIds: string[];
139
139
  toolBudget: number;
140
+ hardGateMode: "chain" | "core";
140
141
  frozenHash: string;
141
142
  contractPath?: string;
142
143
  acceptance: string[];
@@ -159,6 +160,7 @@ function contractFromRecord(record: Record<string, unknown>): ActiveExecutionCon
159
160
  allowedTools: strings("allowed_tools"),
160
161
  checkerIds: strings("checker_ids"),
161
162
  toolBudget: typeof record.tool_budget === "number" ? record.tool_budget : 0,
163
+ hardGateMode: record.hard_gate_mode === "core" ? "core" : "chain",
162
164
  frozenHash: String(record.frozen_hash ?? ""),
163
165
  acceptance: strings("acceptance"),
164
166
  allowedGraphDelta: strings("allowed_graph_delta"),
@@ -168,6 +170,10 @@ function contractFromRecord(record: Record<string, unknown>): ActiveExecutionCon
168
170
  return contract;
169
171
  }
170
172
 
173
+ function coreHardGatesEnabled(contract: ActiveExecutionContract | undefined): boolean {
174
+ return contract?.hardGateMode === "core";
175
+ }
176
+
171
177
  const READ_ONLY_TOOLS = new Set([
172
178
  "read",
173
179
  "grep",
@@ -2782,7 +2788,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2782
2788
  const branchContext = parallelBranchContext(event.input);
2783
2789
  if (branchContext) parallelTaskContexts.set(event.toolCallId, branchContext);
2784
2790
  }
2785
- if (activeContract && controlState.phase === "executing" && !toolName.startsWith("devflow_") && !internalControlResource) {
2791
+ if (coreHardGatesEnabled(activeContract) && controlState.phase === "executing" && !toolName.startsWith("devflow_") && !internalControlResource) {
2786
2792
  if (!activeContract.allowedTools.includes(toolName)) {
2787
2793
  return { block: true, reason: `Active contract ${activeContract.contractId} does not allow tool: ${toolName}` };
2788
2794
  }
@@ -2816,19 +2822,20 @@ export default function devflowCore(pi: ExtensionAPI) {
2816
2822
  return { block: true, reason: planningToolBlockReason(toolName) };
2817
2823
  }
2818
2824
  if (
2825
+ coreHardGatesEnabled(activeContract) &&
2819
2826
  activeContract &&
2820
2827
  controlState.phase === "executing" &&
2821
2828
  !requested.names.every(agent => activeContract?.allowedAgents.includes(agent))
2822
2829
  ) {
2823
2830
  return { block: true, reason: `Active contract ${activeContract.contractId} only allows agents: ${activeContract.allowedAgents.join(", ")}` };
2824
2831
  }
2825
- if (activeContract && controlState.phase === "executing" && contractAgentCalls + requested.names.length > activeContract.toolBudget) {
2832
+ if (coreHardGatesEnabled(activeContract) && activeContract && controlState.phase === "executing" && contractAgentCalls + requested.names.length > activeContract.toolBudget) {
2826
2833
  return { block: true, reason: `Active contract ${activeContract.contractId} exhausted its fixed agent budget (${activeContract.toolBudget}).` };
2827
2834
  }
2828
- if (activeContract && controlState.phase === "executing" && activeContract.step === "code-review" && requested.names.length !== 1) {
2835
+ if (coreHardGatesEnabled(activeContract) && activeContract && controlState.phase === "executing" && activeContract.step === "code-review" && requested.names.length !== 1) {
2829
2836
  return { block: true, reason: "Code review requires exactly one independent reviewer task." };
2830
2837
  }
2831
- if (activeContract && controlState.phase === "executing" && activeContract.step === "code-review") {
2838
+ if (coreHardGatesEnabled(activeContract) && activeContract && controlState.phase === "executing" && activeContract.step === "code-review") {
2832
2839
  const preflight = await runProcess([
2833
2840
  "python3", path.join(resolveKitRoot(ctx.cwd), "workflow-orchestrator/scripts/review_preflight.py"),
2834
2841
  "--root", ctx.cwd,
@@ -2880,7 +2887,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2880
2887
  "Devflow 状态迁移只能由运行时在用户明确批准已展示的步骤或 checkpoint 后执行;LLM 不得自行推进。",
2881
2888
  };
2882
2889
  }
2883
- if (isUserConfirmedStepTransition && activeContract) {
2890
+ if (isUserConfirmedStepTransition && activeContract && coreHardGatesEnabled(activeContract)) {
2884
2891
  const evidence = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/machine_evidence.rb", [
2885
2892
  "--feature", activeContract.feature, "--step", activeContract.step, "--write", "--json",
2886
2893
  ]);
@@ -2892,7 +2899,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2892
2899
  };
2893
2900
  }
2894
2901
  }
2895
- if (activeWorkflowTurn && !parallelRunActive && stepScopedMutationOwner(toolName, event.input) !== undefined) {
2902
+ if (coreHardGatesEnabled(activeContract) && activeWorkflowTurn && !parallelRunActive && stepScopedMutationOwner(toolName, event.input) !== undefined) {
2896
2903
  const inspected = await inspectWorkflow(ctx.cwd);
2897
2904
  if (!inspected.result.ok || !inspected.snapshot) {
2898
2905
  return {
@@ -2905,6 +2912,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2905
2912
  if (reason) return { block: true, reason };
2906
2913
  }
2907
2914
  if (
2915
+ coreHardGatesEnabled(activeContract) &&
2908
2916
  !toolName.startsWith("devflow_") &&
2909
2917
  ["write", "edit", "apply_patch", "notebook_edit", "bash", "shell", "exec_command"].includes(toolName) &&
2910
2918
  inputPaths(event.input, ctx.cwd).some(isCoreOwnedGovernancePath)
@@ -2935,6 +2943,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2935
2943
  }
2936
2944
  if (toolName.startsWith("devflow_")) return;
2937
2945
  if (
2946
+ coreHardGatesEnabled(activeContract) &&
2938
2947
  activeContract &&
2939
2948
  controlState.phase === "executing" &&
2940
2949
  !internalControlResource &&
@@ -2948,7 +2957,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2948
2957
  };
2949
2958
  }
2950
2959
  }
2951
- if (activeContract && controlState.phase === "executing" && ["bash", "shell", "exec_command"].includes(toolName)) {
2960
+ if (coreHardGatesEnabled(activeContract) && activeContract && controlState.phase === "executing" && ["bash", "shell", "exec_command"].includes(toolName)) {
2952
2961
  const command = String((event.input as Record<string, unknown> | undefined)?.command ?? (event.input as Record<string, unknown> | undefined)?.cmd ?? "");
2953
2962
  const shell = shellMutationPaths(command, ctx.cwd);
2954
2963
  if (shell.mutating && (shell.paths.length === 0 || shell.paths.some(filePath => !contractAllowsPath(activeContract!, filePath)))) {
@@ -3188,7 +3197,7 @@ export default function devflowCore(pi: ExtensionAPI) {
3188
3197
  if (params.expectedRevision === undefined || !params.delta) {
3189
3198
  return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "propose requires expectedRevision and delta" });
3190
3199
  }
3191
- if (params.delta.kind === "test_case" && activeContract?.step !== "acceptance-test-design") {
3200
+ if (coreHardGatesEnabled(activeContract) && params.delta.kind === "test_case" && activeContract?.step !== "acceptance-test-design") {
3192
3201
  return renderResult({
3193
3202
  ok: false,
3194
3203
  exitCode: 1,
@@ -3201,7 +3210,7 @@ export default function devflowCore(pi: ExtensionAPI) {
3201
3210
  // explicitly asks for a preview; the normal approved proposal remains
3202
3211
  // a Core-owned write.
3203
3212
  if (params.write !== false) args.push("--write");
3204
- if (activeContract?.allowedGraphDelta?.length) args.push("--allowed-kinds", activeContract.allowedGraphDelta.join(","));
3213
+ if (coreHardGatesEnabled(activeContract) && activeContract?.allowedGraphDelta?.length) args.push("--allowed-kinds", activeContract.allowedGraphDelta.join(","));
3205
3214
  }
3206
3215
  if (params.action === "confirm") {
3207
3216
  if (params.expectedRevision === undefined || !params.nodeId || !params.evidence) {
@@ -3305,7 +3314,7 @@ export default function devflowCore(pi: ExtensionAPI) {
3305
3314
  }),
3306
3315
  approval: args => args.action === "validate" ? "read" : "write",
3307
3316
  async execute(_id, params, _signal, _onUpdate, ctx) {
3308
- if (params.action !== "validate" && activeContract?.step !== "acceptance-test-design") {
3317
+ if (coreHardGatesEnabled(activeContract) && params.action !== "validate" && activeContract?.step !== "acceptance-test-design") {
3309
3318
  return renderResult({ ok: false, exitCode: 1, stdout: "", stderr: "API test matrix compilation/import is allowed only during the active acceptance-test-design contract." });
3310
3319
  }
3311
3320
  if (params.action === "validate") {
@@ -11,6 +11,12 @@ Control-plane entrypoint for project workflows. Reads `.planning/STATE.md`, `.pl
11
11
 
12
12
  This skill is the **single source of truth for all execution-time rules**: gate rules, CP protocol, failure branches, parallel windows, state writeback, lessons injection, and requirement writeback. Pipeline only handles entry (args + breakpoint).
13
13
 
14
+ ## Chain-skill mode (current)
15
+
16
+ The active workflow uses `governance.hard_gate_mode: chain`. In this mode the chain skill owns dispatch and sequencing. Core contract, checker, output, and review gates are diagnostic only and must not hard-block skill execution. Call the current step skill, verify its declared inputs and outputs, collect its result, and continue the chain; preserve missing facts and failed checks as structured findings for the next skill or the user. Do not invoke Core gate APIs to force a step through, and stop only for unsafe writes, corrupted control-plane state, missing required human input, or an explicit checkpoint.
17
+
18
+ This is a migration mode. Setting `hard_gate_mode: core` re-enables the former Core hard-gate behavior.
19
+
14
20
  ## Quick Start
15
21
 
16
22
  If the project has no generated control-plane files, initialize them through the single-writer runtime:
@@ -125,7 +131,7 @@ After each step completes, verify the step's gate before advancing:
125
131
  | integration-verify | integration-report.meta.json `merge_gate_status` / `traceability_status` are `passed` and all mismatch/blocking counters are 0 | Reject advance to change-plan |
126
132
  | change-plan | 变更清单.md review conclusion is "通过" AND 变更清单.meta.json traceability_status is `passed` with zero uncovered/orphan counts | Reject advance to execute |
127
133
 
128
- If a gate fails, the current step must be re-executed. Do not skip failing gates.
134
+ If `hard_gate_mode` is `core`, a gate failure requires re-execution. In the current `chain` mode, retain the failure as a diagnostic finding and let the chain skill decide whether the next skill can consume the artifact; do not use Core hard gates to stop the chain.
129
135
 
130
136
  ## Requirement Writeback Gate
131
137
 
@@ -84,6 +84,10 @@ def gate_contract_enabled?(workflow)
84
84
  workflow.dig("governance", "gate_contract_version").to_i >= 3
85
85
  end
86
86
 
87
+ def core_hard_gate_mode(workflow)
88
+ workflow.dig("governance", "hard_gate_mode").to_s.empty? ? "chain" : workflow.dig("governance", "hard_gate_mode").to_s
89
+ end
90
+
87
91
  # Build the runtime view from the existing workflow declaration. This keeps
88
92
  # governance.step_checkers as the single checker namespace and avoids creating
89
93
  # a second hand-maintained per-step transition table.
@@ -121,6 +125,7 @@ def transition_gate_contract(workflow, step)
121
125
  "required_inputs" => Array(step["inputs"]).select { |input| input.is_a?(Hash) && input["required_at_completion"] == true }
122
126
  },
123
127
  "quality" => { "checker_ids" => Array(workflow.dig("governance", "step_checkers", step["id"])) },
128
+ "hard_gate_mode" => core_hard_gate_mode(workflow),
124
129
  "concurrency" => { "project_lock" => "required", "optimistic_revision" => "required", "registry_view" => "candidate_overlay" },
125
130
  "recovery" => { "allowed" => ["sync_artifacts", "repair_outputs"], "max_attempts" => 2 }
126
131
  }
@@ -131,7 +131,7 @@ begin
131
131
  devflow_requirement_graph devflow_static_runtime_context
132
132
  devflow_architecture_constraint devflow_architecture_feasibility devflow_architecture_brief
133
133
  ],
134
- "tool_budget" => policy["budget"], "acceptance" => Array(graph["nodes"]).select { |node| node["kind"] == "acceptance" }.map { |node| node["id"] },
134
+ "tool_budget" => policy["budget"], "hard_gate_mode" => (workflow.dig("governance", "hard_gate_mode").to_s.empty? ? "chain" : workflow.dig("governance", "hard_gate_mode").to_s), "acceptance" => Array(graph["nodes"]).select { |node| node["kind"] == "acceptance" }.map { |node| node["id"] },
135
135
  "allowed_graph_delta" => Array(policy["allowed_graph_delta"]),
136
136
  "on_unknown" => "block_and_escalate"
137
137
  }
@@ -32,6 +32,12 @@ module GateRuntime
32
32
  gate_id = contract.fetch("gate_id")
33
33
  step_id = contract.fetch("step_id")
34
34
  base = { gate_id: gate_id, step_id: step_id, evidence: evidence.merge("collector" => source), revisions: { "state" => current_revision } }
35
+ # During the chain-skill migration Core is diagnostic-only. It still
36
+ # validates the contract shape above, but never blocks a transition on
37
+ # graph, artifact, output, checker, review, or topology observations.
38
+ if contract.fetch("hard_gate_mode", "core").to_s == "chain"
39
+ return GateResult.allow(**base.merge(evidence: base.fetch(:evidence).merge("core_hard_gate_mode" => "chain", "core_hard_gate_bypassed" => true)))
40
+ end
35
41
  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
36
42
  unless WORKFLOW_STATUSES.include?(workflow_state["status"].to_s)
37
43
  return GateResult.block(**base, code: "WORKFLOW_STATUS_INVALID", message: "workflow status is not recognized")
@@ -17,6 +17,9 @@ phase_scope:
17
17
  # files without governance_started_revision remain legacy_unattested and are
18
18
  # only reported until their next newly-started governed step.
19
19
  governance:
20
+ # Chain skill owns orchestration while Core remains an evidence/diagnostic
21
+ # service. Set to `core` only after the hard-gate migration is complete.
22
+ hard_gate_mode: chain
20
23
  gate_contract_version: 3
21
24
  requirement_graph_gate: enforced
22
25
  requirement_coverage_gate: enforced
@@ -6,6 +6,8 @@ description: Use when running or resuming the full .planning delivery pipeline f
6
6
  <objective>
7
7
  全链路研发交付入口。解析用户参数,先确保需求主链“入口分诊 → 探索地图 → 领域建模 → 可实现性审查”完成,再检测当前状态,然后委托 workflow-orchestrator 接管后续所有步骤调度。
8
8
  pipeline 本身不包含执行规则——所有门禁、CP 协议、失败分支、并行窗口、lessons 注入规则均由 orchestrator 维护。
9
+
10
+ 当前迁移阶段由本 skill 作为链路编排器:按 `next_step.skill` 依次调用各技能、传递上一技能的产物与诊断、再决定下一技能。Core 的 contract/checker/output/review 硬门禁暂不参与阻断;Core 结果只作为事实和风险提示。只有控制面损坏、危险写入或需要人工确认时才停止链路。
9
11
  </objective>
10
12
 
11
13
  <process>
@@ -107,7 +109,7 @@ devflow_state(action="inspect")
107
109
 
108
110
  ## 3. 委托 orchestrator
109
111
 
110
- 无论 inspect_state 返回什么 action,pipeline 本身的职责到此为止。后续所有调度逻辑由 `workflow-orchestrator` skill 全权处理:
112
+ 在当前 `hard_gate_mode: chain` 下,pipeline 继续调用 `workflow-orchestrator` 获取步骤拓扑,但由本链路规则串联实际技能调用,不把 Core gate 的 `block` 结果当作自动停止条件:
111
113
 
112
114
  ```
113
115
  /workflow-orchestrator
@@ -149,8 +149,13 @@ def validate_code_index(
149
149
  mode = manifest.get("mode")
150
150
  status = manifest.get("code_index_status")
151
151
  if mode == "full":
152
+ if status == "degraded":
153
+ reason = str(manifest.get("code_index_skip_reason", ""))
154
+ if reason != "code_index_publish_failed":
155
+ errors.append("degraded full mode requires code_index_publish_failed reason")
156
+ return
152
157
  if status != "complete":
153
- errors.append("full mode requires code_index_status=complete")
158
+ errors.append("full mode requires code_index_status=complete or degraded")
154
159
  if manifest.get("code_index_skip_reason") != "none":
155
160
  errors.append("full mode requires code_index_skip_reason=none")
156
161
  for relative_path in CODE_INDEX_FILES:
@@ -14,6 +14,10 @@ autoloadSkills:
14
14
 
15
15
  Act as a bounded read-only evidence collector, not a planner. Use the code index first, then make at most three targeted reads or searches needed to answer the assigned question. Do not inspect adjacent systems merely to be thorough.
16
16
 
17
+ Before using `read`, `grep`, or `devflow_code_index`, resolve the project root by walking upward from the runtime directory to the nearest `.devflow/project.json`, `.planning/code-index/index.sqlite`, or `.planning/PROJECT.md`. Treat nested directories such as `drama-ad/backend` as codebases, not Devflow project roots; `.planning` must always be resolved from the project root.
18
+
19
+ If the index is missing, stale, fails validation, or has no useful hit, continue with a bounded `grep`/`rg` or LSP fallback over the registered codebase roots. Return `degraded_index_coverage` and the exact fallback scope; never present fallback relations as index facts.
20
+
17
21
  If the answer needs architecture tradeoffs, requirements interpretation, a cross-module decision, or evidence remains insufficient after the bounded search, stop and return `STATUS: escalate`. Do not guess or continue exploring.
18
22
 
19
23
  Return exactly:
@@ -23,6 +23,29 @@ function resolveSkillRoot(cwd: string): string {
23
23
  throw new Error("Devflow intel skills not found");
24
24
  }
25
25
 
26
+ /**
27
+ * Resolve the Devflow project root from the runtime directory. A codebase
28
+ * (for example `drama-ad/backend`) may be nested below the project root;
29
+ * `.planning` belongs to the nearest ancestor project, never to the codebase
30
+ * directory itself.
31
+ */
32
+ function resolveProjectRoot(cwd: string): string {
33
+ let current = path.resolve(cwd);
34
+ while (true) {
35
+ if (
36
+ existsSync(path.join(current, ".devflow", "project.json")) ||
37
+ existsSync(path.join(current, ".planning", "code-index", "index.sqlite")) ||
38
+ existsSync(path.join(current, ".planning", "PROJECT.md"))
39
+ ) {
40
+ return current;
41
+ }
42
+ const parent = path.dirname(current);
43
+ if (parent === current) break;
44
+ current = parent;
45
+ }
46
+ return path.resolve(cwd);
47
+ }
48
+
26
49
  async function run(command: string[], cwd: string): Promise<ProcessResult> {
27
50
  const child = Bun.spawn(command, { cwd, stdout: "pipe", stderr: "pipe" });
28
51
  const [exitCode, stdout, stderr] = await Promise.all([
@@ -82,18 +105,19 @@ export default function devflowIntel(pi: ExtensionAPI) {
82
105
  return ["scan", "domain-import"].includes(action ?? "") ? "exec" : "read";
83
106
  },
84
107
  async execute(_id, params, _signal, _onUpdate, ctx) {
85
- const skillRoot = resolveSkillRoot(ctx.cwd);
108
+ const projectRoot = resolveProjectRoot(ctx.cwd);
109
+ const skillRoot = resolveSkillRoot(projectRoot);
86
110
  const script = path.join(skillRoot, "workflow-code-index/scripts/code_index.py");
87
111
  const args = ["python3", script, params.action];
88
112
  if (params.action === "scan") {
89
- args.push("--root", ctx.cwd, "--output", ".planning/code-index");
113
+ args.push("--root", projectRoot, "--output", ".planning/code-index");
90
114
  if (params.changedFiles.length > 0) args.push("--changed-files", ...params.changedFiles);
91
115
  } else if (params.action === "validate") {
92
116
  args.push("--index", ".planning/code-index");
93
117
  args.push("--level", params.validationLevel);
94
118
  if (params.snapshot) args.push("--snapshot", params.snapshot);
95
119
  } else if (params.action === "audit") {
96
- args.push("--root", ctx.cwd, "--index", ".planning/code-index", "--skill-root", skillRoot);
120
+ args.push("--root", projectRoot, "--index", ".planning/code-index", "--skill-root", skillRoot);
97
121
  args.push("--level", params.validationLevel === "fast" ? "publish" : params.validationLevel);
98
122
  if (params.snapshot) args.push("--snapshot", params.snapshot);
99
123
  } else if (params.action === "query") {
@@ -111,19 +135,19 @@ export default function devflowIntel(pi: ExtensionAPI) {
111
135
  if (params.repository) args.push("--repository", params.repository);
112
136
  if (params.pathPrefix) args.push("--path-prefix", params.pathPrefix);
113
137
  if (params.language) args.push("--language", params.language);
114
- return render(await run(args, ctx.cwd), { action: params.action });
138
+ return render(await run(args, projectRoot), { action: params.action, projectRoot });
115
139
  } else if (params.action === "resolve-domain") {
116
140
  if (!params.term) return render({ ok: false, exitCode: 2, stdout: "", stderr: "term is required" }, {});
117
141
  args.push("--index", ".planning/code-index", "--term", params.term, "--project-id", params.projectId, "--limit", String(params.limit));
118
142
  if (params.mappingRevision) args.push("--mapping-revision", params.mappingRevision);
119
- return render(await run(args, ctx.cwd), { action: params.action });
143
+ return render(await run(args, projectRoot), { action: params.action, projectRoot });
120
144
  } else if (params.action === "query-plan") {
121
145
  if (!params.plan) return render({ ok: false, exitCode: 2, stdout: "", stderr: "plan is required" }, {});
122
146
  const plan = { ...params.plan };
123
147
  if (plan.limit === undefined) plan.limit = params.limit;
124
148
  args.push("--index", ".planning/code-index", "--project-id", params.projectId, "--plan", JSON.stringify(plan));
125
149
  if (params.snapshot) args.push("--snapshot", params.snapshot);
126
- return render(await run(args, ctx.cwd), { action: params.action });
150
+ return render(await run(args, projectRoot), { action: params.action, projectRoot });
127
151
  } else if (params.action === "domain-import") {
128
152
  if (!params.bundle) return render({ ok: false, exitCode: 2, stdout: "", stderr: "bundle is required" }, {});
129
153
  const bundleDir = mkdtempSync(path.join(tmpdir(), "devflow-domain-"));
@@ -132,7 +156,7 @@ export default function devflowIntel(pi: ExtensionAPI) {
132
156
  writeFileSync(bundlePath, JSON.stringify(params.bundle), { encoding: "utf8", mode: 0o600 });
133
157
  args.push("--index", ".planning/code-index", "--project-id", params.projectId, "--bundle", bundlePath);
134
158
  if (params.expectedRevision) args.push("--expected-revision", params.expectedRevision);
135
- return render(await run(args, ctx.cwd), { action: params.action });
159
+ return render(await run(args, projectRoot), { action: params.action, projectRoot });
136
160
  } finally {
137
161
  rmSync(bundleDir, { recursive: true, force: true });
138
162
  }
@@ -142,7 +166,7 @@ export default function devflowIntel(pi: ExtensionAPI) {
142
166
  args.push("--index", ".planning/code-index", "--entity", params.entity, "--depth", String(params.depth), "--limit", String(params.limit));
143
167
  if (params.snapshot) args.push("--snapshot", params.snapshot);
144
168
  if (params.changeType) args.push("--change-type", params.changeType);
145
- return render(await run(args, ctx.cwd), { action: params.action });
169
+ return render(await run(args, projectRoot), { action: params.action, projectRoot });
146
170
  }
147
171
  if (!params.object) return render({ ok: false, exitCode: 2, stdout: "", stderr: "object is required" }, {});
148
172
  args.push("--index", ".planning/code-index", "--object", params.object, "--depth", String(params.depth), "--limit", String(params.limit));
@@ -150,7 +174,7 @@ export default function devflowIntel(pi: ExtensionAPI) {
150
174
  if (params.snapshot) args.push("--snapshot", params.snapshot);
151
175
  if (params.includeHeuristic) args.push("--include-heuristic");
152
176
  }
153
- return render(await run(args, ctx.cwd), { action: params.action });
177
+ return render(await run(args, projectRoot), { action: params.action, projectRoot });
154
178
  },
155
179
  });
156
180
 
@@ -7,6 +7,20 @@ description: Query a generated .planning/code-index machine index to answer code
7
7
 
8
8
  Use this skill to answer source-structure questions from `.planning/code-index/`.
9
9
 
10
+ ## Project-root resolution
11
+
12
+ The runtime directory may be a nested codebase such as `drama-ad/backend`. Before checking the index, resolve the project root from the runtime directory using the same rule as project initialization:
13
+
14
+ 1. Walk upward from the runtime directory and select the nearest ancestor containing `.devflow/project.json`.
15
+ 2. If no manifest exists, select the nearest ancestor containing `.planning/code-index/index.sqlite` or `.planning/PROJECT.md`.
16
+ 3. Only if neither exists, use the runtime directory and report that the project root is uninitialized.
17
+
18
+ All index checks and `devflow_code_index` calls must run with that resolved root as the working directory. Source paths remain relative to the resolved root, so a source file may be `drama-ad/backend/...` while the index remains `.planning/code-index/...`. Never append `.planning` to a selected source/codebase directory.
19
+
20
+ The tool returns the resolved `projectRoot` in its details. If the root cannot be resolved, stop and report the exact check command/path instead of probing `<source-dir>/.planning`.
21
+
22
+ If the index is missing, stale, fails validation, or returns no useful hit, do not block the source question. Record the machine failure and use a bounded `rg`/`grep` or LSP search within the registered codebase roots. Mark the result `degraded_index_coverage` and include the exact fallback scope; do not claim index-derived relations or complete impact coverage.
23
+
10
24
  ## Required Flow
11
25
 
12
26
  1. Check that `.planning/code-index/index.sqlite` or JSONL sidecars exist.
@@ -136,7 +136,7 @@ seam 标注: "只改 OrderWorkflow.process()"
136
136
 
137
137
  **对每一个目标文件**,按以下流程执行:
138
138
 
139
- 1. **⛔ Gate 前置:先查 code-index** — 在读取任何业务源码之前,必须先用 `code_index outline` 或 `code_index search` 获取目标文件的符号表。code-index 不可用时用 LSP `documentSymbol` 兜底。
139
+ 1. **⛔ Gate 前置:先查 code-index** — 在读取任何业务源码之前,必须先用 `code_index outline` 或 `code_index search` 获取目标文件的符号表。若 code-index 缺失、过期、校验失败或无有效命中,使用有界 `rg/grep` 或 LSP `documentSymbol` 兜底,并在报告中标记 `degraded_index_coverage` 和实际搜索范围;不得把 fallback 结果表述为索引关系事实。
140
140
  2. **用 code-index relations 查引用/调用/导入/测试覆盖** — 确认每个导出方法是否有调用方;code-index 缺失、过期或无命中时再用 Grep / LSP findReferences 兜底
141
141
  3. **回读源码 evidence** — 对用于审查结论的 relation,必须读取 `evidence_path:evidence_line` 附近源码。**此时可声明"基于 code-index 符号 [X] 读取源码"以通过 gate。**
142
142
  4. **逐条对照规范检查清单** — 详见 `references/` 目录下各层级的检查清单:
package/src/constants.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PRODUCT_NAME = "Devflow Pi";
2
- export const PRODUCT_VERSION = "0.1.91";
2
+ export const PRODUCT_VERSION = "0.1.93";
3
3
  export const PROJECT_SCHEMA_VERSION = 1;
4
4
  export const OMP_RUNTIME_VERSION = "17.2.7";
5
5
 
@@ -225,8 +225,20 @@ async function buildCodeIndex(context: ProjectContext, config: ScanConfigInput):
225
225
  "--max-file-size", String(config.maxFileSize ?? 2 * 1024 * 1024),
226
226
  ];
227
227
  await runProcess(["python3", script, "scan", "--root", context.root, "--output", output, ...scopeArgs, ...codebases], context.root, "code-index scan");
228
- await runProcess(["python3", script, "validate", "--index", output, "--level", "publish"], context.root, "code-index validate");
229
- await runProcess(["python3", script, "audit", "--root", context.root, "--index", output, "--skill-root", path.resolve(import.meta.dir, "../plugins/devflow-intel/skills"), "--level", "publish"], context.root, "code-index audit");
228
+ try {
229
+ await runProcess(["python3", script, "validate", "--index", output, "--level", "publish"], context.root, "code-index validate");
230
+ await runProcess(["python3", script, "audit", "--root", context.root, "--index", output, "--skill-root", path.resolve(import.meta.dir, "../plugins/devflow-intel/skills"), "--level", "publish"], context.root, "code-index audit");
231
+ } catch (error) {
232
+ const message = error instanceof Error ? error.message : String(error);
233
+ return {
234
+ path: ".planning/code-index",
235
+ stagingPath: output,
236
+ status: "degraded",
237
+ reason: "code_index_publish_failed",
238
+ fallback: "bounded_source_search",
239
+ diagnostics: [message],
240
+ };
241
+ }
230
242
  const snapshotPath = path.join(context.root, output, "SNAPSHOT.json");
231
243
  const bytes = await readFile(snapshotPath); const snapshot = JSON.parse(bytes.toString("utf8")) as Record<string, unknown>;
232
244
  if (!snapshot.generation || !snapshot.immutablePath) throw new Error("Code index did not publish an immutable generation");
@@ -235,6 +247,7 @@ async function buildCodeIndex(context: ProjectContext, config: ScanConfigInput):
235
247
  }
236
248
 
237
249
  async function promoteCodeIndex(context: ProjectContext, indexRef: Record<string, unknown>): Promise<() => Promise<void>> {
250
+ if (indexRef.status !== "complete") return async () => {};
238
251
  const stagingPath = typeof indexRef.stagingPath === "string" ? path.resolve(context.root, indexRef.stagingPath) : null;
239
252
  const target = path.resolve(context.root, ".planning/code-index");
240
253
  if (!stagingPath) return async () => {};