@guanlin4924/devflow-pi 0.1.89 → 0.1.92
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 +24 -11
- 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-intel/agents/project-scout.md +2 -0
- package/plugins/devflow-intel/extensions/devflow-intel.ts +33 -9
- package/plugins/devflow-intel/skills/code-index-query/SKILL.md +12 -0
- package/src/constants.ts +1 -1
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",
|
|
@@ -414,7 +420,7 @@ export function isRequirementDraftToolAllowed(toolName: string, input: unknown,
|
|
|
414
420
|
return normalized === "task" && taskAgents.length > 0 && taskAgents.every(agent => PLANNING_AGENTS.has(agent));
|
|
415
421
|
}
|
|
416
422
|
|
|
417
|
-
type InternalDevflowControlResource = "contract" | "state" | "mainflow" | "requirement_graph" | "requirement_coverage" | "project_context" | "skill_script";
|
|
423
|
+
type InternalDevflowControlResource = "contract" | "state" | "mainflow" | "requirement_graph" | "requirement_coverage" | "project_context" | "skill_script" | "code_index" | "review_preflight" | "review_run" | "review_submit";
|
|
418
424
|
|
|
419
425
|
/** Resolve an xdev target only when all supplied target aliases agree.
|
|
420
426
|
* A direct/eval caller can otherwise provide `path` for a real file plus an
|
|
@@ -443,6 +449,10 @@ export function internalDevflowControlResource(toolName: string, input: unknown)
|
|
|
443
449
|
if (target === "xd://devflow_requirement_coverage") return "requirement_coverage";
|
|
444
450
|
if (target === "xd://devflow_project_context") return "project_context";
|
|
445
451
|
if (target === "xd://devflow_skill_script") return "skill_script";
|
|
452
|
+
if (target === "xd://devflow_code_index") return "code_index";
|
|
453
|
+
if (target === "xd://devflow_review_preflight") return "review_preflight";
|
|
454
|
+
if (target === "xd://devflow_review_run") return "review_run";
|
|
455
|
+
if (target === "xd://devflow_review_submit") return "review_submit";
|
|
446
456
|
return undefined;
|
|
447
457
|
}
|
|
448
458
|
|
|
@@ -2778,7 +2788,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2778
2788
|
const branchContext = parallelBranchContext(event.input);
|
|
2779
2789
|
if (branchContext) parallelTaskContexts.set(event.toolCallId, branchContext);
|
|
2780
2790
|
}
|
|
2781
|
-
if (activeContract && controlState.phase === "executing" && !toolName.startsWith("devflow_") && !internalControlResource) {
|
|
2791
|
+
if (coreHardGatesEnabled(activeContract) && controlState.phase === "executing" && !toolName.startsWith("devflow_") && !internalControlResource) {
|
|
2782
2792
|
if (!activeContract.allowedTools.includes(toolName)) {
|
|
2783
2793
|
return { block: true, reason: `Active contract ${activeContract.contractId} does not allow tool: ${toolName}` };
|
|
2784
2794
|
}
|
|
@@ -2812,19 +2822,20 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2812
2822
|
return { block: true, reason: planningToolBlockReason(toolName) };
|
|
2813
2823
|
}
|
|
2814
2824
|
if (
|
|
2825
|
+
coreHardGatesEnabled(activeContract) &&
|
|
2815
2826
|
activeContract &&
|
|
2816
2827
|
controlState.phase === "executing" &&
|
|
2817
2828
|
!requested.names.every(agent => activeContract?.allowedAgents.includes(agent))
|
|
2818
2829
|
) {
|
|
2819
2830
|
return { block: true, reason: `Active contract ${activeContract.contractId} only allows agents: ${activeContract.allowedAgents.join(", ")}` };
|
|
2820
2831
|
}
|
|
2821
|
-
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) {
|
|
2822
2833
|
return { block: true, reason: `Active contract ${activeContract.contractId} exhausted its fixed agent budget (${activeContract.toolBudget}).` };
|
|
2823
2834
|
}
|
|
2824
|
-
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) {
|
|
2825
2836
|
return { block: true, reason: "Code review requires exactly one independent reviewer task." };
|
|
2826
2837
|
}
|
|
2827
|
-
if (activeContract && controlState.phase === "executing" && activeContract.step === "code-review") {
|
|
2838
|
+
if (coreHardGatesEnabled(activeContract) && activeContract && controlState.phase === "executing" && activeContract.step === "code-review") {
|
|
2828
2839
|
const preflight = await runProcess([
|
|
2829
2840
|
"python3", path.join(resolveKitRoot(ctx.cwd), "workflow-orchestrator/scripts/review_preflight.py"),
|
|
2830
2841
|
"--root", ctx.cwd,
|
|
@@ -2876,7 +2887,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2876
2887
|
"Devflow 状态迁移只能由运行时在用户明确批准已展示的步骤或 checkpoint 后执行;LLM 不得自行推进。",
|
|
2877
2888
|
};
|
|
2878
2889
|
}
|
|
2879
|
-
if (isUserConfirmedStepTransition && activeContract) {
|
|
2890
|
+
if (isUserConfirmedStepTransition && activeContract && coreHardGatesEnabled(activeContract)) {
|
|
2880
2891
|
const evidence = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/machine_evidence.rb", [
|
|
2881
2892
|
"--feature", activeContract.feature, "--step", activeContract.step, "--write", "--json",
|
|
2882
2893
|
]);
|
|
@@ -2888,7 +2899,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2888
2899
|
};
|
|
2889
2900
|
}
|
|
2890
2901
|
}
|
|
2891
|
-
if (activeWorkflowTurn && !parallelRunActive && stepScopedMutationOwner(toolName, event.input) !== undefined) {
|
|
2902
|
+
if (coreHardGatesEnabled(activeContract) && activeWorkflowTurn && !parallelRunActive && stepScopedMutationOwner(toolName, event.input) !== undefined) {
|
|
2892
2903
|
const inspected = await inspectWorkflow(ctx.cwd);
|
|
2893
2904
|
if (!inspected.result.ok || !inspected.snapshot) {
|
|
2894
2905
|
return {
|
|
@@ -2901,6 +2912,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2901
2912
|
if (reason) return { block: true, reason };
|
|
2902
2913
|
}
|
|
2903
2914
|
if (
|
|
2915
|
+
coreHardGatesEnabled(activeContract) &&
|
|
2904
2916
|
!toolName.startsWith("devflow_") &&
|
|
2905
2917
|
["write", "edit", "apply_patch", "notebook_edit", "bash", "shell", "exec_command"].includes(toolName) &&
|
|
2906
2918
|
inputPaths(event.input, ctx.cwd).some(isCoreOwnedGovernancePath)
|
|
@@ -2931,6 +2943,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2931
2943
|
}
|
|
2932
2944
|
if (toolName.startsWith("devflow_")) return;
|
|
2933
2945
|
if (
|
|
2946
|
+
coreHardGatesEnabled(activeContract) &&
|
|
2934
2947
|
activeContract &&
|
|
2935
2948
|
controlState.phase === "executing" &&
|
|
2936
2949
|
!internalControlResource &&
|
|
@@ -2944,7 +2957,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2944
2957
|
};
|
|
2945
2958
|
}
|
|
2946
2959
|
}
|
|
2947
|
-
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)) {
|
|
2948
2961
|
const command = String((event.input as Record<string, unknown> | undefined)?.command ?? (event.input as Record<string, unknown> | undefined)?.cmd ?? "");
|
|
2949
2962
|
const shell = shellMutationPaths(command, ctx.cwd);
|
|
2950
2963
|
if (shell.mutating && (shell.paths.length === 0 || shell.paths.some(filePath => !contractAllowsPath(activeContract!, filePath)))) {
|
|
@@ -3184,7 +3197,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3184
3197
|
if (params.expectedRevision === undefined || !params.delta) {
|
|
3185
3198
|
return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "propose requires expectedRevision and delta" });
|
|
3186
3199
|
}
|
|
3187
|
-
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") {
|
|
3188
3201
|
return renderResult({
|
|
3189
3202
|
ok: false,
|
|
3190
3203
|
exitCode: 1,
|
|
@@ -3197,7 +3210,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3197
3210
|
// explicitly asks for a preview; the normal approved proposal remains
|
|
3198
3211
|
// a Core-owned write.
|
|
3199
3212
|
if (params.write !== false) args.push("--write");
|
|
3200
|
-
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(","));
|
|
3201
3214
|
}
|
|
3202
3215
|
if (params.action === "confirm") {
|
|
3203
3216
|
if (params.expectedRevision === undefined || !params.nodeId || !params.evidence) {
|
|
@@ -3301,7 +3314,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3301
3314
|
}),
|
|
3302
3315
|
approval: args => args.action === "validate" ? "read" : "write",
|
|
3303
3316
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
3304
|
-
if (params.action !== "validate" && activeContract?.step !== "acceptance-test-design") {
|
|
3317
|
+
if (coreHardGatesEnabled(activeContract) && params.action !== "validate" && activeContract?.step !== "acceptance-test-design") {
|
|
3305
3318
|
return renderResult({ ok: false, exitCode: 1, stdout: "", stderr: "API test matrix compilation/import is allowed only during the active acceptance-test-design contract." });
|
|
3306
3319
|
}
|
|
3307
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
|
|
@@ -14,6 +14,8 @@ 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
|
+
|
|
17
19
|
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
20
|
|
|
19
21
|
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,18 @@ 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
|
+
|
|
10
22
|
## Required Flow
|
|
11
23
|
|
|
12
24
|
1. Check that `.planning/code-index/index.sqlite` or JSONL sidecars exist.
|
package/src/constants.ts
CHANGED