@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 +1 -1
- package/plugins/devflow-core/extensions/devflow-core.ts +19 -10
- package/plugins/devflow-core/skills/workflow-orchestrator/SKILL.md +7 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +5 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +1 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_runtime.rb +6 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +3 -0
- package/plugins/devflow-core/skills/workflow-pipeline/SKILL.md +3 -1
- package/plugins/devflow-core/skills/workflow-project-context/scripts/validate_initialization.py +6 -1
- package/plugins/devflow-intel/agents/project-scout.md +4 -0
- package/plugins/devflow-intel/extensions/devflow-intel.ts +33 -9
- package/plugins/devflow-intel/skills/code-index-query/SKILL.md +14 -0
- package/plugins/devflow-quality/skills/code-review/SKILL.md +1 -1
- package/src/constants.ts +1 -1
- package/src/project-initializer.ts +15 -2
package/package.json
CHANGED
|
@@ -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
|
|
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")
|
package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml
CHANGED
|
@@ -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
|
-
|
|
112
|
+
在当前 `hard_gate_mode: chain` 下,pipeline 继续调用 `workflow-orchestrator` 获取步骤拓扑,但由本链路规则串联实际技能调用,不把 Core gate 的 `block` 结果当作自动停止条件:
|
|
111
113
|
|
|
112
114
|
```
|
|
113
115
|
/workflow-orchestrator
|
package/plugins/devflow-core/skills/workflow-project-context/scripts/validate_initialization.py
CHANGED
|
@@ -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
|
|
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",
|
|
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",
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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`
|
|
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
|
@@ -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
|
-
|
|
229
|
-
|
|
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 () => {};
|