@guanlin4924/devflow-pi 0.1.96 → 0.1.97
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/README.md +11 -1
- package/package.json +1 -1
- package/plugins/devflow-core/extensions/devflow-core.ts +678 -91
- package/plugins/devflow-core/skills/wireframe-render/SKILL.md +1 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +18 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_suite.rb +6 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +64 -8
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_coverage.py +3 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_v1.py +52 -2
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +2 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/explore_preflight.py +58 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_workflow.rb +1 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/locator_map.py +156 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +1 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/playwright_adapter.py +1 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph.rb +50 -15
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/session_rollover.rb +60 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/startup_preflight.py +167 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +5 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_render.py +262 -56
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/regression-acceptance.yaml +126 -0
- package/plugins/devflow-core/skills/workflow-pipeline/SKILL.md +2 -0
- package/plugins/devflow-quality/runtime/playwright/devflow-test-dsl-v1.schema.json +103 -88
- package/plugins/devflow-quality/runtime/playwright/explore-host.mjs +879 -0
- package/plugins/devflow-quality/runtime/playwright/runner.spec.mjs +297 -15
- package/plugins/devflow-quality/runtime/playwright/runtime-manifest.json +3 -2
- package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +4 -0
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/SKILL.md +10 -2
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/intermediate-format.md +6 -2
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/project-dsl-contract.md +3 -1
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/ui-mainflow-rules.md +4 -2
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/compile_dsl.py +98 -15
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/locator_report.py +73 -0
- package/plugins/devflow-quality/skills/workflow-regression-test-design/SKILL.md +69 -0
- package/plugins/devflow-quality/skills/workflow-release-closeout/SKILL.md +1 -1
- package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/SKILL.md +1 -1
- package/src/constants.ts +1 -1
- package/src/startup-briefing.ts +109 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
3
|
-
import { appendFile, mkdir, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { appendFile, mkdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
@@ -15,6 +16,7 @@ import { loadProjectContext } from "../../../src/project-context.ts";
|
|
|
15
16
|
import { runProjectInitialization } from "../../../src/project-initializer.ts";
|
|
16
17
|
import { projectControlDatabasePath } from "../../../src/paths.ts";
|
|
17
18
|
import { generateProjectDocuments } from "../../../src/project-documents.ts";
|
|
19
|
+
import { describeStartupIssue, renderStartupBriefing } from "../../../src/startup-briefing.ts";
|
|
18
20
|
import { exportInitializationSnapshot, importLegacyInitialization, switchInitializationAuthority } from "../../../src/project-migration.ts";
|
|
19
21
|
import { migrateLegacyProjectLayout } from "../../../src/project-layout.ts";
|
|
20
22
|
import {
|
|
@@ -43,6 +45,67 @@ export type SessionControlState = {
|
|
|
43
45
|
label: string;
|
|
44
46
|
};
|
|
45
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Credentials entered in the control panel are deliberately session-local.
|
|
50
|
+
* Keeping them in a Map (instead of process.env) is important: every Agent
|
|
51
|
+
* child process inherits the extension environment, while only the Core
|
|
52
|
+
* acceptance runner is allowed to receive these values explicitly.
|
|
53
|
+
*/
|
|
54
|
+
export class TestCredentialStore {
|
|
55
|
+
private readonly values = new Map<string, string>();
|
|
56
|
+
|
|
57
|
+
set(name: string, value: string): void {
|
|
58
|
+
if (!name || !value) {
|
|
59
|
+
this.values.delete(name);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
this.values.set(name, value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
clear(name: string): void {
|
|
66
|
+
this.values.delete(name);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
clearAll(): void {
|
|
70
|
+
this.values.clear();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
has(name: string): boolean {
|
|
74
|
+
return this.values.has(name);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Return only values declared by the current project's profiles. */
|
|
78
|
+
forEnvironment(names: Iterable<string>): Record<string, string> {
|
|
79
|
+
const result: Record<string, string> = {};
|
|
80
|
+
for (const name of names) {
|
|
81
|
+
const value = this.values.get(name);
|
|
82
|
+
if (value !== undefined) result[name] = value;
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function declaredSecretEnvironmentNames(cwd: string): string[] {
|
|
89
|
+
const root = path.join(cwd, ".devflow", "planning", "test-environments");
|
|
90
|
+
if (!existsSync(root)) return [];
|
|
91
|
+
const names = new Set<string>();
|
|
92
|
+
for (const file of readdirSync(root)) {
|
|
93
|
+
if (!file.endsWith(".json")) continue;
|
|
94
|
+
try {
|
|
95
|
+
const profile = JSON.parse(readFileSync(path.join(root, file), "utf8")) as Record<string, unknown>;
|
|
96
|
+
const secretEnv = profile.secret_env;
|
|
97
|
+
if (!secretEnv || typeof secretEnv !== "object" || Array.isArray(secretEnv)) continue;
|
|
98
|
+
for (const value of Object.values(secretEnv as Record<string, unknown>)) {
|
|
99
|
+
if (typeof value === "string" && value) names.add(value);
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// Doctor owns profile validation; an invalid profile contributes no
|
|
103
|
+
// injectable names and therefore cannot widen the secret boundary.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return [...names].sort();
|
|
107
|
+
}
|
|
108
|
+
|
|
46
109
|
type ProjectInitializationMode = "plan" | "apply";
|
|
47
110
|
|
|
48
111
|
type ProjectInitializationRequest = {
|
|
@@ -540,6 +603,118 @@ type ProcessResult = {
|
|
|
540
603
|
stderr: string;
|
|
541
604
|
};
|
|
542
605
|
|
|
606
|
+
const EXPLORE_IDLE_MS = 10 * 60 * 1000;
|
|
607
|
+
const EXPLORE_TOTAL_MS = 60 * 60 * 1000;
|
|
608
|
+
|
|
609
|
+
export type ExploreHostRequest = {
|
|
610
|
+
id: number;
|
|
611
|
+
method: "start" | "observe" | "act" | "assert" | "draft" | "stop";
|
|
612
|
+
params?: Record<string, unknown>;
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
/** Small JSON-lines client. The host owns Playwright; Core owns its lifetime. */
|
|
616
|
+
export class ExploreHostManager {
|
|
617
|
+
private child: any;
|
|
618
|
+
private nextId = 1;
|
|
619
|
+
private readerTask?: Promise<void>;
|
|
620
|
+
private readonly pending = new Map<number, { resolve: (value: unknown) => void; reject: (error: Error) => void }>();
|
|
621
|
+
private idleTimer?: ReturnType<typeof setTimeout>;
|
|
622
|
+
private totalTimer?: ReturnType<typeof setTimeout>;
|
|
623
|
+
private lastActivity = 0;
|
|
624
|
+
|
|
625
|
+
get running(): boolean { return Boolean(this.child); }
|
|
626
|
+
|
|
627
|
+
async start(hostPath: string, cwd: string, environmentFile: string, secrets: Record<string, string>): Promise<void> {
|
|
628
|
+
if (this.child) throw new Error("an explore session is already active");
|
|
629
|
+
const inherited = process.env;
|
|
630
|
+
const env: Record<string, string> = {};
|
|
631
|
+
for (const name of ["PATH", "HOME", "TMPDIR", "LANG", "PLAYWRIGHT_BROWSERS_PATH"]) {
|
|
632
|
+
const value = inherited[name];
|
|
633
|
+
if (value !== undefined) env[name] = value;
|
|
634
|
+
}
|
|
635
|
+
env.DEVFLOW_EXPLORE_ENVIRONMENT_FILE = environmentFile;
|
|
636
|
+
for (const [name, value] of Object.entries(secrets)) env[name] = value;
|
|
637
|
+
this.child = Bun.spawn(["node", hostPath], { cwd, env, stdin: "pipe", stdout: "pipe", stderr: "pipe", detached: true });
|
|
638
|
+
void new Response(this.child.stderr).text();
|
|
639
|
+
this.lastActivity = Date.now();
|
|
640
|
+
this.readerTask = this.readResponses(this.child.stdout);
|
|
641
|
+
this.totalTimer = setTimeout(() => void this.stop("explore session exceeded the 60 minute limit"), EXPLORE_TOTAL_MS);
|
|
642
|
+
this.armIdleTimer();
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
async request(method: ExploreHostRequest["method"], params: Record<string, unknown> = {}): Promise<any> {
|
|
646
|
+
if (!this.child) throw new Error("no active explore session");
|
|
647
|
+
const id = this.nextId++;
|
|
648
|
+
const request = JSON.stringify({ id, method, params }) + "\n";
|
|
649
|
+
this.lastActivity = Date.now();
|
|
650
|
+
this.armIdleTimer();
|
|
651
|
+
const result = new Promise<unknown>((resolve, reject) => this.pending.set(id, { resolve, reject }));
|
|
652
|
+
try {
|
|
653
|
+
this.child.stdin.write(request);
|
|
654
|
+
} catch (error) {
|
|
655
|
+
this.pending.delete(id);
|
|
656
|
+
throw error;
|
|
657
|
+
}
|
|
658
|
+
return result;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
async stop(reason = "explore session stopped"): Promise<void> {
|
|
662
|
+
const child = this.child;
|
|
663
|
+
if (!child) return;
|
|
664
|
+
this.child = undefined;
|
|
665
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
666
|
+
if (this.totalTimer) clearTimeout(this.totalTimer);
|
|
667
|
+
for (const pending of this.pending.values()) pending.reject(new Error(reason));
|
|
668
|
+
this.pending.clear();
|
|
669
|
+
try { child.stdin?.end(); } catch { /* already closed */ }
|
|
670
|
+
const pid = typeof child.pid === "number" ? child.pid : undefined;
|
|
671
|
+
if (pid !== undefined) {
|
|
672
|
+
try { process.kill(-pid, "SIGTERM"); } catch { try { process.kill(pid, "SIGTERM"); } catch { /* exited */ } }
|
|
673
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
674
|
+
try { process.kill(-pid, "SIGKILL"); } catch { try { process.kill(pid, "SIGKILL"); } catch { /* exited */ } }
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
terminateNow(): void {
|
|
679
|
+
const pid = typeof this.child?.pid === "number" ? this.child.pid : undefined;
|
|
680
|
+
if (pid === undefined) return;
|
|
681
|
+
try { process.kill(-pid, "SIGKILL"); } catch { try { process.kill(pid, "SIGKILL"); } catch { /* exited */ } }
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
private armIdleTimer(): void {
|
|
685
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
686
|
+
const delay = Math.max(1, EXPLORE_IDLE_MS - (Date.now() - this.lastActivity));
|
|
687
|
+
this.idleTimer = setTimeout(() => void this.stop("explore session idle timeout"), delay);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
private async readResponses(stream: ReadableStream<Uint8Array>): Promise<void> {
|
|
691
|
+
const reader = stream.getReader();
|
|
692
|
+
const decoder = new TextDecoder();
|
|
693
|
+
let buffer = "";
|
|
694
|
+
try {
|
|
695
|
+
for (;;) {
|
|
696
|
+
const chunk = await reader.read();
|
|
697
|
+
if (chunk.done) break;
|
|
698
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
699
|
+
const lines = buffer.split("\n");
|
|
700
|
+
buffer = lines.pop() ?? "";
|
|
701
|
+
for (const line of lines) {
|
|
702
|
+
if (!line.trim()) continue;
|
|
703
|
+
let response: any;
|
|
704
|
+
try { response = JSON.parse(line); } catch { continue; }
|
|
705
|
+
const waiter = this.pending.get(response?.id);
|
|
706
|
+
if (!waiter) continue;
|
|
707
|
+
this.pending.delete(response.id);
|
|
708
|
+
if (response.ok === true) waiter.resolve(response.result);
|
|
709
|
+
else waiter.reject(new Error(String(response?.error?.message ?? "explore-host request failed")));
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
} finally {
|
|
713
|
+
if (this.child) await this.stop("explore-host exited unexpectedly");
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
543
718
|
type DoctorStatus = "pass" | "warn" | "fail";
|
|
544
719
|
|
|
545
720
|
type DoctorCheck = {
|
|
@@ -597,6 +772,7 @@ const REQUIRED_EXTENSION_TOOLS = [
|
|
|
597
772
|
"devflow_context_cursor",
|
|
598
773
|
"devflow_design_rule",
|
|
599
774
|
"devflow_mainflow",
|
|
775
|
+
"devflow_explore",
|
|
600
776
|
"devflow_approval",
|
|
601
777
|
];
|
|
602
778
|
|
|
@@ -608,6 +784,7 @@ const REQUIRED_SKILLS = [
|
|
|
608
784
|
"workflow-api-contract",
|
|
609
785
|
"workflow-api-supplement",
|
|
610
786
|
"workflow-acceptance-test-design",
|
|
787
|
+
"workflow-regression-test-design",
|
|
611
788
|
"workflow-backend-tech-spec",
|
|
612
789
|
"workflow-bug-fix",
|
|
613
790
|
"workflow-change-plan",
|
|
@@ -673,8 +850,10 @@ const REQUIRED_SCRIPTS = [
|
|
|
673
850
|
"workflow-orchestrator/scripts/acceptance_test_runner.py",
|
|
674
851
|
"workflow-orchestrator/scripts/machine_evidence.rb",
|
|
675
852
|
"workflow-orchestrator/scripts/review_preflight.py",
|
|
853
|
+
"workflow-orchestrator/scripts/startup_preflight.py",
|
|
676
854
|
"workflow-orchestrator/scripts/cross_track_consistency_check.rb",
|
|
677
855
|
"workflow-orchestrator/scripts/execution_contract.rb",
|
|
856
|
+
"workflow-orchestrator/scripts/explore_preflight.py",
|
|
678
857
|
"workflow-code-index/scripts/code_index.py",
|
|
679
858
|
"workflow-context-sync/scripts/context_sync_cursor.py",
|
|
680
859
|
];
|
|
@@ -947,6 +1126,13 @@ function renderResult(result: ProcessResult, details: Record<string, unknown> =
|
|
|
947
1126
|
};
|
|
948
1127
|
}
|
|
949
1128
|
|
|
1129
|
+
function redactCredentialOutput(result: ProcessResult, values: Record<string, string>): ProcessResult {
|
|
1130
|
+
const secrets = Object.values(values).filter(value => value.length > 0);
|
|
1131
|
+
if (secrets.length === 0) return result;
|
|
1132
|
+
const redact = (text: string) => secrets.reduce((current, secret) => current.split(secret).join("[REDACTED]"), text);
|
|
1133
|
+
return { ...result, stdout: redact(result.stdout), stderr: redact(result.stderr) };
|
|
1134
|
+
}
|
|
1135
|
+
|
|
950
1136
|
function registeredNames(value: unknown): Set<string> {
|
|
951
1137
|
if (Array.isArray(value)) {
|
|
952
1138
|
return new Set(
|
|
@@ -995,6 +1181,16 @@ function parseJsonResult(result: ProcessResult): Record<string, unknown> | null
|
|
|
995
1181
|
}
|
|
996
1182
|
}
|
|
997
1183
|
|
|
1184
|
+
function parseJsonOutput(result: ProcessResult): Record<string, unknown> | null {
|
|
1185
|
+
if (!result.stdout) return null;
|
|
1186
|
+
try {
|
|
1187
|
+
const parsed = JSON.parse(result.stdout);
|
|
1188
|
+
return parsed && typeof parsed === "object" ? parsed as Record<string, unknown> : null;
|
|
1189
|
+
} catch {
|
|
1190
|
+
return null;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
998
1194
|
export function canonicalReviewJson(value: unknown): string {
|
|
999
1195
|
if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
|
|
1000
1196
|
if (typeof value === "number" && Number.isFinite(value)) return JSON.stringify(value);
|
|
@@ -1115,7 +1311,7 @@ export function workflowResumeDisposition(snapshot: Record<string, unknown>): Wo
|
|
|
1115
1311
|
return "blocked";
|
|
1116
1312
|
}
|
|
1117
1313
|
|
|
1118
|
-
function workflowDirective(snapshot: Record<string, unknown>, source: "start" | "approval", requirementDraft?: string): string {
|
|
1314
|
+
function workflowDirective(snapshot: Record<string, unknown>, source: "start" | "approval", requirementDraft?: string, briefing?: string): string {
|
|
1119
1315
|
const selectedStep = selectedWorkflowStep(snapshot);
|
|
1120
1316
|
const reviewInstructions = selectedStep?.id === "code-review"
|
|
1121
1317
|
? [
|
|
@@ -1153,8 +1349,12 @@ function workflowDirective(snapshot: Record<string, unknown>, source: "start" |
|
|
|
1153
1349
|
: [];
|
|
1154
1350
|
return [
|
|
1155
1351
|
"<devflow-runtime-directive>",
|
|
1156
|
-
|
|
1157
|
-
|
|
1352
|
+
briefing
|
|
1353
|
+
? "Devflow Runtime 已完成开工预检(环境、交接、状态、合同、输入),以下开工简报中的事实已经核实;执行合同已激活,不要再次调用 devflow_state inspect 或 devflow_contract activate。读取 skill://workflow-pipeline,并只执行当前项目的一个工作流步骤。"
|
|
1354
|
+
: "Devflow Runtime 已完成环境与状态检查。读取 skill://workflow-pipeline,并只执行当前项目的一个工作流步骤。",
|
|
1355
|
+
...(briefing
|
|
1356
|
+
? ["<startup-briefing>", briefing, "</startup-briefing>"]
|
|
1357
|
+
: ["先调用 devflow_state(action=\"inspect\") 重新确认 revision,再调用 devflow_contract(action=\"status\");若没有匹配当前 feature/current_step 的 fresh contract,立即调用 devflow_contract(action=\"activate\", feature=<current_feature>, step=<current_step>),激活成功后才能执行或写入。"]),
|
|
1158
1358
|
"只按 inspector 返回的当前 step 加载所需 Skill:step.skill 是精确 ID,必须读取 skill://<step.skill>,不得自行添加 workflow- 前缀或改写名称;本回合严禁读取或写入 xd://devflow_transition,严禁调用 devflow_transition,严禁执行 Skill 中的 complete-step/confirm-checkpoint,不得推进下一步骤;状态写回和会话切换由 Runtime 在用户确认后独占处理。",
|
|
1159
1359
|
...reviewInstructions,
|
|
1160
1360
|
...(parallelInstructions.length > 0 ? parallelInstructions : [
|
|
@@ -1584,6 +1784,27 @@ export function approvalReviewText(proposal: ProposalRecord): string {
|
|
|
1584
1784
|
|
|
1585
1785
|
export default function devflowCore(pi: ExtensionAPI) {
|
|
1586
1786
|
const { z } = pi.zod;
|
|
1787
|
+
const testCredentials = new TestCredentialStore();
|
|
1788
|
+
const exploreHost = new ExploreHostManager();
|
|
1789
|
+
process.once("exit", () => exploreHost.terminateNow());
|
|
1790
|
+
|
|
1791
|
+
const redactExploreValue = (value: unknown, secrets: Record<string, string>): unknown => {
|
|
1792
|
+
const values = Object.values(secrets).filter(Boolean);
|
|
1793
|
+
if (typeof value === "string") return values.reduce((text, secret) => text.split(secret).join("[REDACTED]"), value);
|
|
1794
|
+
if (Array.isArray(value)) return value.map(item => redactExploreValue(item, secrets));
|
|
1795
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactExploreValue(item, secrets)]));
|
|
1796
|
+
return value;
|
|
1797
|
+
};
|
|
1798
|
+
|
|
1799
|
+
const exploreHostPath = (): string => {
|
|
1800
|
+
const candidates = [
|
|
1801
|
+
path.join(resolvePluginRoot(), "devflow-quality", "runtime", "playwright", "explore-host.mjs"),
|
|
1802
|
+
path.join(resolvePluginRoot(), "devflow-quality", "runtime", "playwright", "dist", "explore-host.mjs"),
|
|
1803
|
+
];
|
|
1804
|
+
const found = candidates.find(candidate => existsSync(candidate));
|
|
1805
|
+
if (!found) throw new Error("explore-host.mjs is not installed; build the Devflow Playwright Runtime first");
|
|
1806
|
+
return found;
|
|
1807
|
+
};
|
|
1587
1808
|
pi.registerTool({
|
|
1588
1809
|
name: "devflow_project_context",
|
|
1589
1810
|
label: "Devflow Project Context",
|
|
@@ -1690,10 +1911,13 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1690
1911
|
// during extension construction and never becomes model-controlled state.
|
|
1691
1912
|
const cliSessionResumeRequested = process.env.DEVFLOW_CLI_SESSION_RESUME === "1";
|
|
1692
1913
|
delete process.env.DEVFLOW_CLI_SESSION_RESUME;
|
|
1693
|
-
let sessionId = randomUUID();
|
|
1914
|
+
let sessionId: string = randomUUID();
|
|
1694
1915
|
let controlState: SessionControlState = initialControlState();
|
|
1695
1916
|
let activeContract: ActiveExecutionContract | undefined;
|
|
1696
1917
|
let contractAgentCalls = 0;
|
|
1918
|
+
let stepUserNotes: string[] = [];
|
|
1919
|
+
let lastAgentReport = "";
|
|
1920
|
+
let sessionBootstrapContext = "";
|
|
1697
1921
|
const reviewBindingKey = randomBytes(32).toString("hex");
|
|
1698
1922
|
const pendingReviewTasks = new Map<string, { feature: string; contractId: string }>();
|
|
1699
1923
|
const completedReviewTasks = new Map<string, { feature: string; contractId: string; report: Record<string, unknown> }>();
|
|
@@ -1932,6 +2156,63 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1932
2156
|
return undefined;
|
|
1933
2157
|
}
|
|
1934
2158
|
|
|
2159
|
+
async function activateExecutionContract(
|
|
2160
|
+
ctx: {
|
|
2161
|
+
cwd: string;
|
|
2162
|
+
ui: {
|
|
2163
|
+
confirm(title: string, body: string): Promise<boolean>;
|
|
2164
|
+
setStatus(key: string, text: string | undefined): void;
|
|
2165
|
+
setWidget?(key: string, content: unknown, options?: { placement?: "aboveEditor" | "belowEditor" }): void;
|
|
2166
|
+
};
|
|
2167
|
+
},
|
|
2168
|
+
feature: string,
|
|
2169
|
+
step: string,
|
|
2170
|
+
): Promise<{ ok: true; contract: ActiveExecutionContract } | { ok: false; reason: string }> {
|
|
2171
|
+
const inspected = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/execution_contract.rb", [
|
|
2172
|
+
"inspect", "--feature", feature, "--step", step, "--json",
|
|
2173
|
+
]);
|
|
2174
|
+
const payload = parseJsonResult(inspected);
|
|
2175
|
+
const raw = payload?.contract;
|
|
2176
|
+
if (!controlPlaneSucceeded(inspected) || !raw || typeof raw !== "object") {
|
|
2177
|
+
return { ok: false, reason: inspected.stderr || inspected.stdout || "Core returned no execution contract." };
|
|
2178
|
+
}
|
|
2179
|
+
const contract = contractFromRecord(raw as Record<string, unknown>);
|
|
2180
|
+
if (!contract) return { ok: false, reason: "Core returned an invalid execution contract." };
|
|
2181
|
+
const confirmed = await ctx.ui.confirm(
|
|
2182
|
+
"激活 Devflow 执行合同",
|
|
2183
|
+
[
|
|
2184
|
+
`合同:${contract.contractId}`,
|
|
2185
|
+
`Feature:${contract.feature}`,
|
|
2186
|
+
`步骤:${contract.step}`,
|
|
2187
|
+
`STATE revision:${contract.stateRevision ?? "未初始化"}`,
|
|
2188
|
+
`图谱 revision:${contract.graphRevision ?? 0}`,
|
|
2189
|
+
`可写路径:${contract.allowedPaths.join(", ")}`,
|
|
2190
|
+
`允许 Agent:${contract.allowedAgents.join(", ")}`,
|
|
2191
|
+
`固定检查器:${contract.checkerIds.join(", ") || "无"}`,
|
|
2192
|
+
`冻结哈希:${contract.frozenHash}`,
|
|
2193
|
+
"确认后低成本模型只能在以上范围执行;未知项必须阻塞升级。",
|
|
2194
|
+
].join("\n"),
|
|
2195
|
+
);
|
|
2196
|
+
if (!confirmed) return { ok: false, reason: "User declined execution contract activation." };
|
|
2197
|
+
try {
|
|
2198
|
+
const createArgs = ["create", "--feature", feature, "--step", step, "--write", "--json"];
|
|
2199
|
+
if (contract.stateRevision !== undefined) createArgs.push("--expected-state-revision", String(contract.stateRevision));
|
|
2200
|
+
if (contract.graphRevision !== undefined) createArgs.push("--expected-graph-revision", String(contract.graphRevision));
|
|
2201
|
+
const written = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/execution_contract.rb", createArgs);
|
|
2202
|
+
if (!controlPlaneSucceeded(written)) {
|
|
2203
|
+
return { ok: false, reason: written.stderr || written.stdout || "Execution contract activation failed." };
|
|
2204
|
+
}
|
|
2205
|
+
const writtenPayload = parseJsonResult(written);
|
|
2206
|
+
contract.contractPath = typeof writtenPayload?.path === "string" ? writtenPayload.path : ".devflow/planning/contracts/";
|
|
2207
|
+
activeContract = contract;
|
|
2208
|
+
contractAgentCalls = 0;
|
|
2209
|
+
setControlState(ctx, executingState(`合同执行:${contract.contractId}`));
|
|
2210
|
+
return { ok: true, contract };
|
|
2211
|
+
} catch (error) {
|
|
2212
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
|
|
1935
2216
|
async function collectProjectInitialization(ctx: {
|
|
1936
2217
|
cwd: string;
|
|
1937
2218
|
ui: {
|
|
@@ -2055,12 +2336,13 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2055
2336
|
});
|
|
2056
2337
|
}
|
|
2057
2338
|
|
|
2058
|
-
async function runPython(cwd: string, relativeScript: string, args: string[]) {
|
|
2339
|
+
async function runPython(cwd: string, relativeScript: string, args: string[], extraEnv: Record<string, string> = {}) {
|
|
2059
2340
|
assertRuntimeProjectBoundary(cwd);
|
|
2060
2341
|
const kitRoot = resolveKitRoot(cwd);
|
|
2061
2342
|
return runProcess(["python3", path.join(kitRoot, relativeScript), ...args], cwd, {
|
|
2062
2343
|
DEVFLOW_KIT_ROOT: kitRoot,
|
|
2063
2344
|
OMP_PROJECT_DIR: cwd,
|
|
2345
|
+
...extraEnv,
|
|
2064
2346
|
});
|
|
2065
2347
|
}
|
|
2066
2348
|
|
|
@@ -2248,6 +2530,114 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2248
2530
|
return { ok: checks.every(check => check.status !== "fail"), checks, models };
|
|
2249
2531
|
}
|
|
2250
2532
|
|
|
2533
|
+
async function runStartupPreflight(
|
|
2534
|
+
ctx: DoctorContext & {
|
|
2535
|
+
ui: {
|
|
2536
|
+
confirm(title: string, body: string): Promise<boolean>;
|
|
2537
|
+
notify(message: string, level: "info" | "error"): void;
|
|
2538
|
+
setStatus(key: string, text: string | undefined): void;
|
|
2539
|
+
setWidget?(key: string, content: unknown, options?: { placement?: "aboveEditor" | "belowEditor" }): void;
|
|
2540
|
+
};
|
|
2541
|
+
},
|
|
2542
|
+
snapshot: Record<string, unknown>,
|
|
2543
|
+
options: { doctorAlreadyPassed?: boolean; handoffPath?: string } = {},
|
|
2544
|
+
): Promise<{ ok: true; briefing?: string } | { ok: false }> {
|
|
2545
|
+
if (options.doctorAlreadyPassed !== true) {
|
|
2546
|
+
const report = await runDoctor(ctx);
|
|
2547
|
+
if (!report.ok) {
|
|
2548
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检失败" });
|
|
2549
|
+
ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), "error");
|
|
2550
|
+
return { ok: false };
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
if (workflowResumeDisposition(snapshot) !== "execute" || snapshot.action === "run_parallel_window") {
|
|
2554
|
+
return { ok: true };
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
try {
|
|
2558
|
+
const feature = typeof snapshot.current_feature === "string" ? snapshot.current_feature : "";
|
|
2559
|
+
const step = String(selectedWorkflowStep(snapshot)?.id ?? "");
|
|
2560
|
+
if (!feature || !step) {
|
|
2561
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检未通过" });
|
|
2562
|
+
ctx.ui.notify("开工预检未通过:当前 Feature 或步骤不可用。", "error");
|
|
2563
|
+
return { ok: false };
|
|
2564
|
+
}
|
|
2565
|
+
let contract = activeContract;
|
|
2566
|
+
if (!contract || contract.feature !== feature || contract.step !== step || !(await contractIsFresh(ctx.cwd, contract))) {
|
|
2567
|
+
const activated = await activateExecutionContract(ctx, feature, step);
|
|
2568
|
+
if (!activated.ok) {
|
|
2569
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检:执行合同未激活" });
|
|
2570
|
+
ctx.ui.notify(`开工预检:执行合同未激活。${activated.reason}`, "error");
|
|
2571
|
+
return { ok: false };
|
|
2572
|
+
}
|
|
2573
|
+
contract = activated.contract;
|
|
2574
|
+
}
|
|
2575
|
+
if (!contract.contractPath) {
|
|
2576
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检:执行合同未激活" });
|
|
2577
|
+
ctx.ui.notify("开工预检:执行合同未激活。合同冻结路径不可用。", "error");
|
|
2578
|
+
return { ok: false };
|
|
2579
|
+
}
|
|
2580
|
+
const contractAbsolute = path.resolve(ctx.cwd, contract.contractPath);
|
|
2581
|
+
const contractRelative = path.relative(ctx.cwd, contractAbsolute).split(path.sep).join("/");
|
|
2582
|
+
if (!contractRelative || contractRelative === ".." || contractRelative.startsWith("../")) {
|
|
2583
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检:执行合同未激活" });
|
|
2584
|
+
ctx.ui.notify("开工预检:执行合同未激活。合同冻结路径越出项目。", "error");
|
|
2585
|
+
return { ok: false };
|
|
2586
|
+
}
|
|
2587
|
+
const preflight = await runPython(ctx.cwd, "workflow-orchestrator/scripts/startup_preflight.py", [
|
|
2588
|
+
"--root", ctx.cwd,
|
|
2589
|
+
"--feature", feature,
|
|
2590
|
+
"--step", step,
|
|
2591
|
+
"--contract", contractRelative,
|
|
2592
|
+
...(options.handoffPath ? ["--handoff", options.handoffPath] : []),
|
|
2593
|
+
]);
|
|
2594
|
+
const payload = parseJsonOutput(preflight);
|
|
2595
|
+
if (!payload || payload.status !== "ready") {
|
|
2596
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检未通过" });
|
|
2597
|
+
const issues = Array.isArray(payload?.issues) ? payload.issues : [];
|
|
2598
|
+
const details = issues.length > 0
|
|
2599
|
+
? issues.map(issue => `- ${describeStartupIssue(issue && typeof issue === "object" ? issue as Record<string, unknown> : {})}`).join("\n")
|
|
2600
|
+
: "- 开工预检输出无法解析";
|
|
2601
|
+
ctx.ui.notify(`开工预检未通过:\n${details}`, "error");
|
|
2602
|
+
return { ok: false };
|
|
2603
|
+
}
|
|
2604
|
+
const facts = payload.facts && typeof payload.facts === "object" ? payload.facts as Record<string, unknown> : {};
|
|
2605
|
+
const handoff = payload.handoff && typeof payload.handoff === "object" ? payload.handoff as Record<string, unknown> : {};
|
|
2606
|
+
return {
|
|
2607
|
+
ok: true,
|
|
2608
|
+
briefing: renderStartupBriefing(facts, handoff, {
|
|
2609
|
+
revision: snapshot.revision,
|
|
2610
|
+
handoffProvided: Boolean(options.handoffPath),
|
|
2611
|
+
}),
|
|
2612
|
+
};
|
|
2613
|
+
} catch (error) {
|
|
2614
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检未通过" });
|
|
2615
|
+
ctx.ui.notify(`开工预检未通过:${error instanceof Error ? error.message : String(error)}`, "error");
|
|
2616
|
+
return { ok: false };
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
async function startWorkflowAfterPreflight(
|
|
2621
|
+
ctx: DoctorContext & {
|
|
2622
|
+
ui: {
|
|
2623
|
+
confirm(title: string, body: string): Promise<boolean>;
|
|
2624
|
+
notify(message: string, level: "info" | "error"): void;
|
|
2625
|
+
setStatus(key: string, text: string | undefined): void;
|
|
2626
|
+
setWidget?(key: string, content: unknown, options?: { placement?: "aboveEditor" | "belowEditor" }): void;
|
|
2627
|
+
};
|
|
2628
|
+
},
|
|
2629
|
+
snapshot: Record<string, unknown>,
|
|
2630
|
+
source: "start" | "approval",
|
|
2631
|
+
options: { doctorAlreadyPassed?: boolean; handoffPath?: string; executingLabel?: string } = {},
|
|
2632
|
+
): Promise<void> {
|
|
2633
|
+
const preflight = await runStartupPreflight(ctx, snapshot, options);
|
|
2634
|
+
if (!preflight.ok) return;
|
|
2635
|
+
setControlState(ctx, executingState(options.executingLabel));
|
|
2636
|
+
if (preflight.briefing) await triggerWorkflow(snapshot, source, undefined, preflight.briefing);
|
|
2637
|
+
else if (source === "start") await triggerWorkflow(snapshot, "start");
|
|
2638
|
+
else await triggerWorkflow(snapshot, "approval");
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2251
2641
|
async function inspectWorkflow(cwd: string): Promise<{ result: ProcessResult; snapshot: Record<string, unknown> | null }> {
|
|
2252
2642
|
const result = await runRuby(cwd, "workflow-orchestrator/scripts/inspect_state.rb", ["--json"]);
|
|
2253
2643
|
return { result, snapshot: parseJsonResult(result) };
|
|
@@ -2294,26 +2684,33 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2294
2684
|
async function requestControlledRollover(ctx: { cwd: string; sessionManager: { getSessionId(): string } }, snapshot: Record<string, unknown>) {
|
|
2295
2685
|
const revision = typeof snapshot.revision === "number" ? snapshot.revision : undefined;
|
|
2296
2686
|
if (revision === undefined) throw new Error("Cannot prepare session rollover without a STATE revision.");
|
|
2297
|
-
const
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2687
|
+
const carryPath = path.join(tmpdir(), `devflow-rollover-carry-${randomUUID()}.json`);
|
|
2688
|
+
try {
|
|
2689
|
+
await writeFile(carryPath, JSON.stringify({ user_notes: stepUserNotes, agent_report: lastAgentReport }), "utf8");
|
|
2690
|
+
const prepared = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/session_rollover.rb", [
|
|
2691
|
+
"prepare", "--write", "--json", "--expected-revision", String(revision),
|
|
2692
|
+
"--source-session-id", ctx.sessionManager.getSessionId(), "--reason", "workflow_step_boundary",
|
|
2693
|
+
"--carry-file", carryPath,
|
|
2694
|
+
]);
|
|
2695
|
+
const payload = parseJsonResult(prepared);
|
|
2696
|
+
if (!prepared.ok || !payload || typeof payload.revision !== "number") {
|
|
2697
|
+
throw new Error(prepared.stderr || prepared.stdout || "rollover prepare failed");
|
|
2698
|
+
}
|
|
2699
|
+
const rollover = payload.rollover as Record<string, unknown> | undefined;
|
|
2700
|
+
const rolloverId = typeof rollover?.id === "string" ? rollover.id : undefined;
|
|
2701
|
+
if (!rolloverId) throw new Error("rollover prepare returned no rollover id");
|
|
2702
|
+
const synced = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/sync_artifacts.rb", [
|
|
2703
|
+
"--write", "--no-preserve", "--json",
|
|
2704
|
+
]);
|
|
2705
|
+
if (!controlPlaneSucceeded(synced)) {
|
|
2706
|
+
throw new Error(synced.stderr || synced.stdout || "artifact registry sync after rollover prepare failed");
|
|
2707
|
+
}
|
|
2708
|
+
// The runtime can observe the request immediately; arm the switch handler first.
|
|
2709
|
+
pendingRollover = { snapshot, preparedRevision: payload.revision, rolloverId };
|
|
2710
|
+
await enqueueControlledRolloverRequest(ctx.cwd, rolloverId, ctx.sessionManager.getSessionId());
|
|
2711
|
+
} finally {
|
|
2712
|
+
await unlink(carryPath).catch(() => undefined);
|
|
2313
2713
|
}
|
|
2314
|
-
// The runtime can observe the request immediately; arm the switch handler first.
|
|
2315
|
-
pendingRollover = { snapshot, preparedRevision: payload.revision, rolloverId };
|
|
2316
|
-
await enqueueControlledRolloverRequest(ctx.cwd, rolloverId, ctx.sessionManager.getSessionId());
|
|
2317
2714
|
}
|
|
2318
2715
|
|
|
2319
2716
|
function parallelBranches(snapshot: Record<string, unknown>): Array<{ workflow_step: string; track: string }> {
|
|
@@ -2361,7 +2758,9 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2361
2758
|
return { ...snapshot, parallel_run: payload.run };
|
|
2362
2759
|
}
|
|
2363
2760
|
|
|
2364
|
-
async function triggerWorkflow(snapshot: Record<string, unknown>, source: "start" | "approval", requirementDraft?: string) {
|
|
2761
|
+
async function triggerWorkflow(snapshot: Record<string, unknown>, source: "start" | "approval", requirementDraft?: string, briefing?: string) {
|
|
2762
|
+
stepUserNotes = [];
|
|
2763
|
+
lastAgentReport = "";
|
|
2365
2764
|
if (snapshot.action !== "run_parallel_window") {
|
|
2366
2765
|
parallelRunActive = false;
|
|
2367
2766
|
parallelContinuationQueued = false;
|
|
@@ -2372,26 +2771,35 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2372
2771
|
}
|
|
2373
2772
|
const preparedSnapshot = await prepareParallelRun(projectRoot ?? process.cwd(), snapshot);
|
|
2374
2773
|
activeWorkflowTurn = true;
|
|
2375
|
-
await pi.sendUserMessage(workflowDirective(preparedSnapshot, source, requirementDraft));
|
|
2774
|
+
await pi.sendUserMessage(workflowDirective(preparedSnapshot, source, requirementDraft, briefing));
|
|
2376
2775
|
}
|
|
2377
2776
|
|
|
2378
2777
|
async function resumeInspectedWorkflow(
|
|
2379
2778
|
ctx: {
|
|
2380
2779
|
cwd: string;
|
|
2780
|
+
models: { resolve(spec: string): ModelLike | undefined };
|
|
2381
2781
|
ui: {
|
|
2782
|
+
confirm(title: string, body: string): Promise<boolean>;
|
|
2382
2783
|
notify(message: string, level: "info" | "error"): void;
|
|
2383
2784
|
setStatus(key: string, text: string | undefined): void;
|
|
2384
2785
|
setWidget?(key: string, content: string[] | undefined, options?: { placement?: "aboveEditor" | "belowEditor" }): void;
|
|
2385
2786
|
};
|
|
2386
2787
|
},
|
|
2387
2788
|
snapshot: Record<string, unknown>,
|
|
2789
|
+
options: { doctorAlreadyPassed: boolean },
|
|
2388
2790
|
): Promise<WorkflowResumeDisposition> {
|
|
2389
2791
|
const disposition = workflowResumeDisposition(snapshot);
|
|
2390
2792
|
const stepId = String(selectedWorkflowStep(snapshot)?.id ?? "当前步骤");
|
|
2391
2793
|
if (disposition === "execute") {
|
|
2392
|
-
setControlState(ctx, executingState(
|
|
2393
|
-
ctx.ui.notify(`Devflow 已恢复到 ${stepId}
|
|
2394
|
-
|
|
2794
|
+
setControlState(ctx, executingState("开工预检中"));
|
|
2795
|
+
ctx.ui.notify(`Devflow 已恢复到 ${stepId},当前步骤产物尚未完成,正在完成开工预检。`, "info");
|
|
2796
|
+
void startWorkflowAfterPreflight(ctx, snapshot, "start", {
|
|
2797
|
+
doctorAlreadyPassed: options.doctorAlreadyPassed,
|
|
2798
|
+
executingLabel: `恢复并执行 ${stepId}`,
|
|
2799
|
+
}).catch(error => {
|
|
2800
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检未通过" });
|
|
2801
|
+
ctx.ui.notify(`开工预检未通过:${String(error)}`, "error");
|
|
2802
|
+
});
|
|
2395
2803
|
return disposition;
|
|
2396
2804
|
}
|
|
2397
2805
|
if (disposition === "review") {
|
|
@@ -2488,6 +2896,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2488
2896
|
|
|
2489
2897
|
type ProjectControlCommandContext = {
|
|
2490
2898
|
cwd: string;
|
|
2899
|
+
models: DoctorContext["models"];
|
|
2491
2900
|
sessionManager: { getSessionId(): string };
|
|
2492
2901
|
ui: ProjectControlUi & {
|
|
2493
2902
|
select(title: string, options: string[]): Promise<string | undefined>;
|
|
@@ -2668,6 +3077,45 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2668
3077
|
}
|
|
2669
3078
|
}
|
|
2670
3079
|
|
|
3080
|
+
async function manageTestCredentials(ctx: ProjectControlCommandContext): Promise<void> {
|
|
3081
|
+
const names = declaredSecretEnvironmentNames(ctx.cwd);
|
|
3082
|
+
if (names.length === 0) {
|
|
3083
|
+
ctx.ui.notify("当前项目没有声明测试凭据(secret_env)。", "info");
|
|
3084
|
+
return;
|
|
3085
|
+
}
|
|
3086
|
+
for (;;) {
|
|
3087
|
+
const labels = names.map(name => `${name}(${testCredentials.has(name) || process.env[name] !== undefined ? "已设置" : "未设置"})`);
|
|
3088
|
+
const selected = await ctx.ui.select("测试凭据(输入内容会显示在屏幕上,不会持久化)", [
|
|
3089
|
+
...labels,
|
|
3090
|
+
"清除全部",
|
|
3091
|
+
"返回",
|
|
3092
|
+
]);
|
|
3093
|
+
if (!selected || selected === "返回") return;
|
|
3094
|
+
if (selected === "清除全部") {
|
|
3095
|
+
if (await ctx.ui.confirm("清除全部测试凭据", "只清除本次 devflow-pi 会话内存中的面板凭据;不会修改启动环境。继续吗?")) {
|
|
3096
|
+
testCredentials.clearAll();
|
|
3097
|
+
ctx.ui.notify("已清除本次会话的面板凭据。", "info");
|
|
3098
|
+
}
|
|
3099
|
+
continue;
|
|
3100
|
+
}
|
|
3101
|
+
const index = labels.indexOf(selected);
|
|
3102
|
+
const name = names[index];
|
|
3103
|
+
if (!name) continue;
|
|
3104
|
+
const action = await ctx.ui.select(`测试凭据:${name}`, ["设置/覆盖", "清除面板值", "返回"]);
|
|
3105
|
+
if (!action || action === "返回") continue;
|
|
3106
|
+
if (action === "清除面板值") {
|
|
3107
|
+
testCredentials.clear(name);
|
|
3108
|
+
ctx.ui.notify(`已清除 ${name} 的面板值;启动环境值仍可能存在。`, "info");
|
|
3109
|
+
continue;
|
|
3110
|
+
}
|
|
3111
|
+
const value = await ctx.ui.input(`输入测试凭据:${name}`, "输入值(会显示在屏幕上,不会持久化)");
|
|
3112
|
+
if (value === undefined) continue;
|
|
3113
|
+
if (!value) testCredentials.clear(name);
|
|
3114
|
+
else testCredentials.set(name, value);
|
|
3115
|
+
ctx.ui.notify(`${name} 已${value ? "设置" : "清除"}(值不会显示)。`, "info");
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
|
|
2671
3119
|
async function openProjectControlPanel(ctx: ProjectControlCommandContext): Promise<void> {
|
|
2672
3120
|
for (;;) {
|
|
2673
3121
|
// The control panel is a read-only projection. Each action below reads a
|
|
@@ -2683,6 +3131,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2683
3131
|
if (snapshot.action === "await_user_confirmation") options.push("审查并批准推进…");
|
|
2684
3132
|
if (hasSafeRecoveryPlan(snapshot)) options.push("执行安全恢复…");
|
|
2685
3133
|
if (typeof snapshot.current_feature === "string" && !["completed", "cancelled", "failed"].includes(String(snapshot.status))) options.push("取消当前功能…");
|
|
3134
|
+
options.push("测试凭据…");
|
|
2686
3135
|
const approvalCount = await pendingApprovalCount(ctx.cwd);
|
|
2687
3136
|
const approvalOption = approvalCount === null ? null : `待审批(${approvalCount})…`;
|
|
2688
3137
|
if (approvalOption) options.push(approvalOption);
|
|
@@ -2701,7 +3150,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2701
3150
|
ctx.ui.notify(`启动前状态读取失败:${fresh.result.stderr || fresh.result.stdout}`, "error");
|
|
2702
3151
|
return;
|
|
2703
3152
|
}
|
|
2704
|
-
await resumeInspectedWorkflow(ctx, fresh.snapshot);
|
|
3153
|
+
await resumeInspectedWorkflow(ctx, fresh.snapshot, { doctorAlreadyPassed: false });
|
|
2705
3154
|
return;
|
|
2706
3155
|
}
|
|
2707
3156
|
if (selected === "审查并批准推进…") {
|
|
@@ -2716,6 +3165,10 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2716
3165
|
await cancelFeatureFromPanel(ctx);
|
|
2717
3166
|
return;
|
|
2718
3167
|
}
|
|
3168
|
+
if (selected === "测试凭据…") {
|
|
3169
|
+
await manageTestCredentials(ctx);
|
|
3170
|
+
continue;
|
|
3171
|
+
}
|
|
2719
3172
|
if (approvalOption && selected === approvalOption) {
|
|
2720
3173
|
await reviewPendingApprovalsFromPanel(ctx);
|
|
2721
3174
|
continue;
|
|
@@ -2797,6 +3250,36 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2797
3250
|
|
|
2798
3251
|
pi.setLabel("Devflow Project Runtime");
|
|
2799
3252
|
|
|
3253
|
+
async function refreshSessionBootstrapContext(
|
|
3254
|
+
ctx: { cwd: string },
|
|
3255
|
+
source: "startup" | "rollover",
|
|
3256
|
+
): Promise<{ ok: true } | { ok: false; reason: string }> {
|
|
3257
|
+
sessionBootstrapContext = "";
|
|
3258
|
+
try {
|
|
3259
|
+
const result = await runHook(ctx.cwd, "start.py", {
|
|
3260
|
+
session_id: sessionId,
|
|
3261
|
+
cwd: ctx.cwd,
|
|
3262
|
+
source,
|
|
3263
|
+
hook_event_name: "SessionStart",
|
|
3264
|
+
});
|
|
3265
|
+
if (!result.ok) return { ok: false, reason: result.stderr || result.stdout || "start.py did not complete successfully" };
|
|
3266
|
+
const bootstrap = parseJsonResult(result);
|
|
3267
|
+
const hookOutput = bootstrap?.hookSpecificOutput;
|
|
3268
|
+
const additionalContext = hookOutput && typeof hookOutput === "object"
|
|
3269
|
+
? (hookOutput as Record<string, unknown>).additionalContext
|
|
3270
|
+
: undefined;
|
|
3271
|
+
if (typeof additionalContext === "string" && additionalContext) {
|
|
3272
|
+
sessionBootstrapContext = additionalContext.length > 8000
|
|
3273
|
+
? `${additionalContext.slice(0, 7999)}…`
|
|
3274
|
+
: additionalContext;
|
|
3275
|
+
}
|
|
3276
|
+
return { ok: true };
|
|
3277
|
+
} catch (error) {
|
|
3278
|
+
sessionBootstrapContext = "";
|
|
3279
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
|
|
2800
3283
|
pi.on("session_start", async (_event, ctx) => {
|
|
2801
3284
|
projectRoot = ctx.cwd;
|
|
2802
3285
|
sessionId = randomUUID();
|
|
@@ -2806,14 +3289,9 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2806
3289
|
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
2807
3290
|
return;
|
|
2808
3291
|
}
|
|
2809
|
-
const
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
source: "startup",
|
|
2813
|
-
hook_event_name: "SessionStart",
|
|
2814
|
-
});
|
|
2815
|
-
if (!result.ok) {
|
|
2816
|
-
ctx.ui.notify(`Devflow bootstrap failed: ${result.stderr || result.stdout}`, "error");
|
|
3292
|
+
const bootstrap = await refreshSessionBootstrapContext(ctx, "startup");
|
|
3293
|
+
if (!bootstrap.ok) {
|
|
3294
|
+
ctx.ui.notify(`Devflow bootstrap failed: ${bootstrap.reason}`, "error");
|
|
2817
3295
|
return;
|
|
2818
3296
|
}
|
|
2819
3297
|
ctx.ui.setStatus("devflow", "Devflow ready");
|
|
@@ -2866,8 +3344,15 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2866
3344
|
ctx.ui.notify(`会话已恢复,但工作流校验失败,未启动 AI:${String(after.snapshot.reason ?? "state validation failed")}`, "error");
|
|
2867
3345
|
return;
|
|
2868
3346
|
}
|
|
2869
|
-
setControlState(ctx, executingState("
|
|
2870
|
-
|
|
3347
|
+
setControlState(ctx, executingState("开工预检中"));
|
|
3348
|
+
void startWorkflowAfterPreflight(ctx, after.snapshot, "approval", {
|
|
3349
|
+
handoffPath: typeof rollover.handoff_path === "string" ? rollover.handoff_path : undefined,
|
|
3350
|
+
executingLabel: "恢复已认领的干净会话",
|
|
3351
|
+
}).catch(error => {
|
|
3352
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检未通过" });
|
|
3353
|
+
ctx.ui.notify(`开工预检未通过:${String(error)}`, "error");
|
|
3354
|
+
});
|
|
3355
|
+
return;
|
|
2871
3356
|
} else if (!consumed.ok || !synced || !controlPlaneSucceeded(synced)) {
|
|
2872
3357
|
ctx.ui.notify(`已认领的会话交接无法恢复;工作流保持冻结:${consumed.stderr || consumed.stdout}`, "error");
|
|
2873
3358
|
}
|
|
@@ -2883,7 +3368,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2883
3368
|
ctx.ui.notify(`Devflow resume failed: ${inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON"}`, "error");
|
|
2884
3369
|
return;
|
|
2885
3370
|
}
|
|
2886
|
-
await resumeInspectedWorkflow(ctx, inspected.snapshot);
|
|
3371
|
+
await resumeInspectedWorkflow(ctx, inspected.snapshot, { doctorAlreadyPassed: true });
|
|
2887
3372
|
}
|
|
2888
3373
|
});
|
|
2889
3374
|
|
|
@@ -2925,6 +3410,8 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2925
3410
|
ctx.ui.notify(`Devflow 会话交接消费或 registry 同步失败;工作流保持冻结:${consumed.stderr || consumed.stdout || synced?.stderr || synced?.stdout}`, "error");
|
|
2926
3411
|
return;
|
|
2927
3412
|
}
|
|
3413
|
+
sessionId = targetSessionId;
|
|
3414
|
+
await refreshSessionBootstrapContext(ctx, "rollover");
|
|
2928
3415
|
pendingRollover = undefined;
|
|
2929
3416
|
activeContract = undefined;
|
|
2930
3417
|
contractAgentCalls = 0;
|
|
@@ -2934,8 +3421,15 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2934
3421
|
ctx.ui.notify(`会话已切换,但工作流校验失败,未启动 AI:${String(next.snapshot?.reason ?? next.result.stderr ?? next.result.stdout ?? "state validation failed")}`, "error");
|
|
2935
3422
|
return;
|
|
2936
3423
|
}
|
|
2937
|
-
|
|
2938
|
-
|
|
3424
|
+
const claimedRollover = claimedPayload.rollover as Record<string, unknown> | undefined;
|
|
3425
|
+
setControlState(ctx, executingState("开工预检中"));
|
|
3426
|
+
void startWorkflowAfterPreflight(ctx, next.snapshot, "approval", {
|
|
3427
|
+
handoffPath: typeof claimedRollover?.handoff_path === "string" ? claimedRollover.handoff_path : undefined,
|
|
3428
|
+
executingLabel: "在干净会话中执行下一工作流步骤",
|
|
3429
|
+
}).catch(error => {
|
|
3430
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检未通过" });
|
|
3431
|
+
ctx.ui.notify(`开工预检未通过:${String(error)}`, "error");
|
|
3432
|
+
});
|
|
2939
3433
|
});
|
|
2940
3434
|
|
|
2941
3435
|
pi.on("before_agent_start", async event => {
|
|
@@ -2943,11 +3437,25 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2943
3437
|
await appendEngineEvidence(projectRoot, "ai_runtime_initialized", { source: "runtime_before_agent_start" });
|
|
2944
3438
|
await appendEngineEvidence(projectRoot, "ai_invocation", { source: "runtime_before_agent_start" });
|
|
2945
3439
|
}
|
|
2946
|
-
return {
|
|
3440
|
+
return {
|
|
3441
|
+
systemPrompt: [
|
|
3442
|
+
...event.systemPrompt,
|
|
3443
|
+
...(sessionBootstrapContext ? [sessionBootstrapContext] : []),
|
|
3444
|
+
DEVFLOW_CHINESE_LANGUAGE_POLICY,
|
|
3445
|
+
],
|
|
3446
|
+
};
|
|
2947
3447
|
});
|
|
2948
3448
|
|
|
2949
3449
|
pi.on("agent_end", async (event, ctx) => {
|
|
2950
3450
|
if (!activeWorkflowTurn || event.willContinue) return;
|
|
3451
|
+
const assistant = [...event.messages].reverse().find(message => (
|
|
3452
|
+
message && typeof message === "object" && (message as { role?: unknown }).role === "assistant"
|
|
3453
|
+
));
|
|
3454
|
+
const content = assistant && typeof assistant === "object" ? (assistant as { content?: unknown }).content : undefined;
|
|
3455
|
+
lastAgentReport = Array.isArray(content)
|
|
3456
|
+
? content.flatMap(part => part && typeof part === "object" && (part as { type?: unknown }).type === "text"
|
|
3457
|
+
&& typeof (part as { text?: unknown }).text === "string" ? [(part as { text: string }).text] : []).join("")
|
|
3458
|
+
: "";
|
|
2951
3459
|
activeWorkflowTurn = false;
|
|
2952
3460
|
if (parallelRunActive) {
|
|
2953
3461
|
parallelContinuationQueued = false;
|
|
@@ -2983,6 +3491,10 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2983
3491
|
pi.on("input", async (event, ctx) => {
|
|
2984
3492
|
if (event.source !== "interactive") return;
|
|
2985
3493
|
const controlAction = classifyNaturalControlAction(event.text);
|
|
3494
|
+
const stepNote = event.text.trim();
|
|
3495
|
+
if (controlState.intent === "workflow-execute" && !controlAction && stepNote && !stepNote.startsWith("/")) {
|
|
3496
|
+
stepUserNotes.push(stepNote);
|
|
3497
|
+
}
|
|
2986
3498
|
if (controlState.intent === "requirement-draft" && controlState.phase === "planning") {
|
|
2987
3499
|
const requirementDraft = event.text.trim();
|
|
2988
3500
|
if (!requirementDraft) return { handled: true };
|
|
@@ -3471,6 +3983,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3471
3983
|
});
|
|
3472
3984
|
|
|
3473
3985
|
pi.on("session_stop", async (_event, ctx) => {
|
|
3986
|
+
await exploreHost.stop("Devflow session stopped");
|
|
3474
3987
|
const result = await runHook(ctx.cwd, "context_sync_gate.py", {
|
|
3475
3988
|
session_id: sessionId,
|
|
3476
3989
|
cwd: ctx.cwd,
|
|
@@ -3775,6 +4288,99 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3775
4288
|
},
|
|
3776
4289
|
});
|
|
3777
4290
|
|
|
4291
|
+
pi.registerTool({
|
|
4292
|
+
name: "devflow_explore",
|
|
4293
|
+
label: "Devflow Explore",
|
|
4294
|
+
description: [
|
|
4295
|
+
"在已批准的 environment_profile 内启动结构化页面探索会话;探索结果不是验收结论,draft 仍必须经过编译、校验和用户批准。",
|
|
4296
|
+
"凭据只按 profile.secret_env 注入 explore-host 子进程,永不写入 process.env、日志或工具输出。页面文本可能包含个人信息,本工具不做页面级个人信息脱敏。",
|
|
4297
|
+
].join(" "),
|
|
4298
|
+
parameters: z.object({
|
|
4299
|
+
action: z.enum(["start", "observe", "act", "assert", "draft", "stop"]),
|
|
4300
|
+
feature: z.string().min(1).optional(),
|
|
4301
|
+
environment_profile: z.string().min(1).optional(),
|
|
4302
|
+
url: z.string().min(1).optional(),
|
|
4303
|
+
scope: z.string().optional(),
|
|
4304
|
+
full: z.boolean().optional(),
|
|
4305
|
+
table_text: z.boolean().optional(),
|
|
4306
|
+
ref: z.string().min(1).optional(),
|
|
4307
|
+
operation: z.enum(["click", "fill", "check", "uncheck", "select", "press"]).optional(),
|
|
4308
|
+
action_name: z.string().optional(),
|
|
4309
|
+
value: z.string().optional(),
|
|
4310
|
+
value_from_secret: z.string().optional(),
|
|
4311
|
+
kind: z.string().optional(),
|
|
4312
|
+
match: z.string().optional(),
|
|
4313
|
+
expected: z.unknown().optional(),
|
|
4314
|
+
name: z.string().optional(),
|
|
4315
|
+
tc_id: z.string().optional(),
|
|
4316
|
+
}),
|
|
4317
|
+
approval: args => args.action === "observe" || args.action === "stop" ? "read" : "write",
|
|
4318
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
4319
|
+
const fail = (message: string, code = "invalid_request") => ({
|
|
4320
|
+
content: [{ type: "text" as const, text: message }],
|
|
4321
|
+
details: { code },
|
|
4322
|
+
isError: true,
|
|
4323
|
+
});
|
|
4324
|
+
const response = (result: unknown, secrets: Record<string, string> = {}) => ({
|
|
4325
|
+
content: [{ type: "text" as const, text: JSON.stringify(redactExploreValue(result, secrets), null, 2) }],
|
|
4326
|
+
details: redactExploreValue(result, secrets),
|
|
4327
|
+
});
|
|
4328
|
+
try {
|
|
4329
|
+
if (activeContract && !["regression-test-design", "regression-dsl", "generate-mainflow-cases"].includes(activeContract.step)) {
|
|
4330
|
+
return fail("devflow_explore is only available while idle or during regression-test-design/DSL generation", "tool_not_allowed");
|
|
4331
|
+
}
|
|
4332
|
+
if (params.action === "stop") {
|
|
4333
|
+
await exploreHost.stop();
|
|
4334
|
+
return response({ ok: true, stopped: true });
|
|
4335
|
+
}
|
|
4336
|
+
if (params.action === "start") {
|
|
4337
|
+
if (!params.feature || !params.environment_profile || !params.url) return fail("start requires feature, environment_profile and url");
|
|
4338
|
+
assertRuntimeProjectBoundary(ctx.cwd);
|
|
4339
|
+
const preflight = await runPython(ctx.cwd, "workflow-orchestrator/scripts/explore_preflight.py", [
|
|
4340
|
+
"--root", ctx.cwd, "--feature", params.feature, "--environment-profile", params.environment_profile,
|
|
4341
|
+
]);
|
|
4342
|
+
const checked = parseJsonResult(preflight);
|
|
4343
|
+
if (!preflight.ok || checked?.ok !== true) {
|
|
4344
|
+
return renderResult({ ...preflight, ok: false, exitCode: preflight.exitCode || 1 }, { code: checked?.reason ?? "environment_profile_not_approved" });
|
|
4345
|
+
}
|
|
4346
|
+
const secretNames = Array.isArray(checked.secret_names) ? checked.secret_names.filter((name): name is string => typeof name === "string") : [];
|
|
4347
|
+
const panelSecrets = testCredentials.forEnvironment(secretNames);
|
|
4348
|
+
const childSecrets: Record<string, string> = {};
|
|
4349
|
+
const missing: string[] = [];
|
|
4350
|
+
for (const name of secretNames) {
|
|
4351
|
+
const value = panelSecrets[name] ?? process.env[name];
|
|
4352
|
+
if (value === undefined || value === "") missing.push(name);
|
|
4353
|
+
else childSecrets[name] = value;
|
|
4354
|
+
}
|
|
4355
|
+
if (missing.length) return fail(`environment secret is missing: ${missing.join(", ")}`, "environment_secret_missing");
|
|
4356
|
+
await exploreHost.start(exploreHostPath(), ctx.cwd, String(checked.profile_path), childSecrets);
|
|
4357
|
+
const result = await exploreHost.request("start", {
|
|
4358
|
+
feature: params.feature, environment_profile: params.environment_profile, url: params.url,
|
|
4359
|
+
});
|
|
4360
|
+
return response(result, childSecrets);
|
|
4361
|
+
}
|
|
4362
|
+
if (!exploreHost.running) return fail("no active explore session", "session_missing");
|
|
4363
|
+
const requestParams: Record<string, unknown> = {};
|
|
4364
|
+
if (params.action === "observe") Object.assign(requestParams, { scope: params.scope, full: params.full, table_text: params.table_text });
|
|
4365
|
+
else if (params.action === "act") {
|
|
4366
|
+
const operation = params.operation ?? params.action_name;
|
|
4367
|
+
if (!params.ref || !operation) return fail("act requires ref and operation");
|
|
4368
|
+
if (!["click", "fill", "check", "uncheck", "select", "press"].includes(operation)) return fail("act operation is invalid");
|
|
4369
|
+
Object.assign(requestParams, { ref: params.ref, action: operation, value: params.value, value_from_secret: params.value_from_secret });
|
|
4370
|
+
} else if (params.action === "assert") {
|
|
4371
|
+
if (!params.ref || !params.kind || !["text", "visible", "value", "each_text"].includes(params.kind)) return fail("assert requires ref and a supported kind");
|
|
4372
|
+
Object.assign(requestParams, { ref: params.ref, kind: params.kind, match: params.match, expected: params.expected });
|
|
4373
|
+
} else if (params.action === "draft") {
|
|
4374
|
+
if (!params.name) return fail("draft requires name");
|
|
4375
|
+
Object.assign(requestParams, { name: params.name, tc_id: params.tc_id });
|
|
4376
|
+
}
|
|
4377
|
+
return response(await exploreHost.request(params.action, requestParams));
|
|
4378
|
+
} catch (error) {
|
|
4379
|
+
return fail(error instanceof Error ? error.message : String(error), "explore_failed");
|
|
4380
|
+
}
|
|
4381
|
+
},
|
|
4382
|
+
});
|
|
4383
|
+
|
|
3778
4384
|
pi.registerTool({
|
|
3779
4385
|
name: "devflow_run_acceptance_tests",
|
|
3780
4386
|
label: "Devflow Run Acceptance Tests",
|
|
@@ -3795,10 +4401,17 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3795
4401
|
approval: args => args.action === "run" || (args.action === "gc_runs" && args.write === true) ? "write" : "read",
|
|
3796
4402
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
3797
4403
|
const args = acceptanceRunnerArgs(params);
|
|
3798
|
-
|
|
4404
|
+
// Only the Core-owned acceptance execution receives panel credentials.
|
|
4405
|
+
// validate/validate_results and every other subprocess stay on the
|
|
4406
|
+
// inherited startup environment and never receive Map values.
|
|
4407
|
+
const credentialEnv = params.action === "run"
|
|
4408
|
+
? testCredentials.forEnvironment(declaredSecretEnvironmentNames(ctx.cwd))
|
|
4409
|
+
: {};
|
|
4410
|
+
const process = await runPython(ctx.cwd, "workflow-orchestrator/scripts/acceptance_test_runner.py", args, credentialEnv);
|
|
4411
|
+
const safeProcess = redactCredentialOutput(process, credentialEnv);
|
|
3799
4412
|
const payload = parseJsonResult(process);
|
|
3800
4413
|
const failed = payload && typeof payload === "object" && (payload as { ok?: unknown }).ok === false;
|
|
3801
|
-
return renderResult(failed ? { ...
|
|
4414
|
+
return renderResult(failed ? { ...safeProcess, ok: false, exitCode: safeProcess.exitCode || 1 } : safeProcess, {
|
|
3802
4415
|
action: params.action,
|
|
3803
4416
|
feature: params.feature,
|
|
3804
4417
|
});
|
|
@@ -4164,51 +4777,25 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
4164
4777
|
};
|
|
4165
4778
|
}
|
|
4166
4779
|
if (!input.feature || !input.step) return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "feature and step are required; Core derives all control fields." });
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4780
|
+
if (input.action === "inspect") {
|
|
4781
|
+
const inspected = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/execution_contract.rb", ["inspect", "--feature", input.feature, "--step", input.step, "--json"]);
|
|
4782
|
+
const payload = parseJsonResult(inspected);
|
|
4783
|
+
const raw = payload?.contract;
|
|
4784
|
+
if (!controlPlaneSucceeded(inspected) || !raw || typeof raw !== "object") return renderResult(inspected, { action: input.action });
|
|
4785
|
+
const contract = contractFromRecord(raw as Record<string, unknown>);
|
|
4786
|
+
if (!contract) return renderResult({ ok: false, exitCode: 1, stdout: "", stderr: "Core returned an invalid execution contract." });
|
|
4787
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(contract, null, 2) }], details: { contract } };
|
|
4788
|
+
}
|
|
4174
4789
|
if (!contractActivationPhaseAllowed(controlState.phase, activeWorkflowTurn)) {
|
|
4175
4790
|
return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "Contracts may only be activated from planning/review, or for the exact current step during a runtime-triggered workflow turn." });
|
|
4176
4791
|
}
|
|
4177
|
-
const
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
`图谱 revision:${contract.graphRevision ?? 0}`,
|
|
4185
|
-
`可写路径:${contract.allowedPaths.join(", ")}`,
|
|
4186
|
-
`允许 Agent:${contract.allowedAgents.join(", ")}`,
|
|
4187
|
-
`固定检查器:${contract.checkerIds.join(", ") || "无"}`,
|
|
4188
|
-
`冻结哈希:${contract.frozenHash}`,
|
|
4189
|
-
"确认后低成本模型只能在以上范围执行;未知项必须阻塞升级。",
|
|
4190
|
-
].join("\n"),
|
|
4191
|
-
);
|
|
4192
|
-
if (!confirmed) return renderResult({ ok: false, exitCode: 1, stdout: "", stderr: "User declined execution contract activation." });
|
|
4193
|
-
try {
|
|
4194
|
-
const createArgs = ["create", "--feature", input.feature, "--step", input.step, "--write", "--json"];
|
|
4195
|
-
if (contract.stateRevision !== undefined) createArgs.push("--expected-state-revision", String(contract.stateRevision));
|
|
4196
|
-
if (contract.graphRevision !== undefined) createArgs.push("--expected-graph-revision", String(contract.graphRevision));
|
|
4197
|
-
const written = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/execution_contract.rb", createArgs);
|
|
4198
|
-
if (!controlPlaneSucceeded(written)) return renderResult(written, { action: input.action });
|
|
4199
|
-
const writtenPayload = parseJsonResult(written);
|
|
4200
|
-
const destination = typeof writtenPayload?.path === "string" ? writtenPayload.path : ".devflow/planning/contracts/";
|
|
4201
|
-
contract.contractPath = destination;
|
|
4202
|
-
activeContract = contract;
|
|
4203
|
-
contractAgentCalls = 0;
|
|
4204
|
-
setControlState(ctx, executingState(`合同执行:${contract.contractId}`));
|
|
4205
|
-
return {
|
|
4206
|
-
content: [{ type: "text" as const, text: `Execution contract activated: ${destination}` }],
|
|
4207
|
-
details: { contract, destination },
|
|
4208
|
-
};
|
|
4209
|
-
} catch (error) {
|
|
4210
|
-
return renderResult({ ok: false, exitCode: 1, stdout: "", stderr: error instanceof Error ? error.message : String(error) });
|
|
4211
|
-
}
|
|
4792
|
+
const activated = await activateExecutionContract(ctx, input.feature, input.step);
|
|
4793
|
+
if (!activated.ok) return renderResult({ ok: false, exitCode: 1, stdout: "", stderr: activated.reason });
|
|
4794
|
+
const destination = activated.contract.contractPath ?? ".devflow/planning/contracts/";
|
|
4795
|
+
return {
|
|
4796
|
+
content: [{ type: "text" as const, text: `Execution contract activated: ${destination}` }],
|
|
4797
|
+
details: { contract: activated.contract, destination },
|
|
4798
|
+
};
|
|
4212
4799
|
},
|
|
4213
4800
|
});
|
|
4214
4801
|
|
|
@@ -4256,7 +4843,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
4256
4843
|
parameters: z.object({
|
|
4257
4844
|
feature: z.string().min(1),
|
|
4258
4845
|
phase: z.string().min(1).optional(),
|
|
4259
|
-
flow: z.enum(["feature-development", "acceptance-remediation"]).default("feature-development"),
|
|
4846
|
+
flow: z.enum(["feature-development", "acceptance-remediation", "regression-acceptance"]).default("feature-development"),
|
|
4260
4847
|
force: z.boolean().default(false),
|
|
4261
4848
|
}),
|
|
4262
4849
|
approval: "write",
|
|
@@ -4793,7 +5380,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
4793
5380
|
ctx.ui.notify("Devflow 已恢复待完成的受控会话交接;当前回合结束后将继续切换。", "info");
|
|
4794
5381
|
return;
|
|
4795
5382
|
}
|
|
4796
|
-
await resumeInspectedWorkflow(ctx, inspected.snapshot);
|
|
5383
|
+
await resumeInspectedWorkflow(ctx, inspected.snapshot, { doctorAlreadyPassed: true });
|
|
4797
5384
|
return;
|
|
4798
5385
|
}
|
|
4799
5386
|
if (trimmed.startsWith("start ")) {
|