@guanlin4924/devflow-pi 0.1.96 → 0.1.98
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 +697 -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 +1015 -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 +12 -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 +72 -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,132 @@ 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
|
+
class ExploreHostError extends Error {
|
|
616
|
+
constructor(message: string, readonly code?: string) {
|
|
617
|
+
super(message);
|
|
618
|
+
this.name = "ExploreHostError";
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** Small JSON-lines client. The host owns Playwright; Core owns its lifetime. */
|
|
623
|
+
export class ExploreHostManager {
|
|
624
|
+
private child: any;
|
|
625
|
+
private nextId = 1;
|
|
626
|
+
private readerTask?: Promise<void>;
|
|
627
|
+
private readonly pending = new Map<number, { resolve: (value: unknown) => void; reject: (error: Error) => void }>();
|
|
628
|
+
private idleTimer?: ReturnType<typeof setTimeout>;
|
|
629
|
+
private totalTimer?: ReturnType<typeof setTimeout>;
|
|
630
|
+
private lastActivity = 0;
|
|
631
|
+
private exploreSecrets: Record<string, string> = {};
|
|
632
|
+
|
|
633
|
+
get running(): boolean { return Boolean(this.child); }
|
|
634
|
+
get secrets(): Record<string, string> { return { ...this.exploreSecrets }; }
|
|
635
|
+
|
|
636
|
+
async start(hostPath: string, cwd: string, environmentFile: string, secrets: Record<string, string>): Promise<void> {
|
|
637
|
+
if (this.child) throw new Error("an explore session is already active");
|
|
638
|
+
const inherited = process.env;
|
|
639
|
+
const env: Record<string, string> = {};
|
|
640
|
+
for (const name of ["PATH", "HOME", "TMPDIR", "LANG", "PLAYWRIGHT_BROWSERS_PATH"]) {
|
|
641
|
+
const value = inherited[name];
|
|
642
|
+
if (value !== undefined) env[name] = value;
|
|
643
|
+
}
|
|
644
|
+
env.DEVFLOW_EXPLORE_ENVIRONMENT_FILE = environmentFile;
|
|
645
|
+
for (const [name, value] of Object.entries(secrets)) env[name] = value;
|
|
646
|
+
this.child = Bun.spawn(["node", hostPath], { cwd, env, stdin: "pipe", stdout: "pipe", stderr: "pipe", detached: true });
|
|
647
|
+
this.exploreSecrets = { ...secrets };
|
|
648
|
+
void new Response(this.child.stderr).text();
|
|
649
|
+
this.lastActivity = Date.now();
|
|
650
|
+
this.readerTask = this.readResponses(this.child.stdout);
|
|
651
|
+
this.totalTimer = setTimeout(() => void this.stop("explore session exceeded the 60 minute limit"), EXPLORE_TOTAL_MS);
|
|
652
|
+
this.armIdleTimer();
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
async request(method: ExploreHostRequest["method"], params: Record<string, unknown> = {}): Promise<any> {
|
|
656
|
+
if (!this.child) throw new Error("no active explore session");
|
|
657
|
+
const id = this.nextId++;
|
|
658
|
+
const request = JSON.stringify({ id, method, params }) + "\n";
|
|
659
|
+
this.lastActivity = Date.now();
|
|
660
|
+
this.armIdleTimer();
|
|
661
|
+
const result = new Promise<unknown>((resolve, reject) => this.pending.set(id, { resolve, reject }));
|
|
662
|
+
try {
|
|
663
|
+
this.child.stdin.write(request);
|
|
664
|
+
} catch (error) {
|
|
665
|
+
this.pending.delete(id);
|
|
666
|
+
throw error;
|
|
667
|
+
}
|
|
668
|
+
return result;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async stop(reason = "explore session stopped"): Promise<void> {
|
|
672
|
+
const child = this.child;
|
|
673
|
+
if (!child) return;
|
|
674
|
+
this.child = undefined;
|
|
675
|
+
this.exploreSecrets = {};
|
|
676
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
677
|
+
if (this.totalTimer) clearTimeout(this.totalTimer);
|
|
678
|
+
for (const pending of this.pending.values()) pending.reject(new Error(reason));
|
|
679
|
+
this.pending.clear();
|
|
680
|
+
try { child.stdin?.end(); } catch { /* already closed */ }
|
|
681
|
+
const pid = typeof child.pid === "number" ? child.pid : undefined;
|
|
682
|
+
if (pid !== undefined) {
|
|
683
|
+
try { process.kill(-pid, "SIGTERM"); } catch { try { process.kill(pid, "SIGTERM"); } catch { /* exited */ } }
|
|
684
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
685
|
+
try { process.kill(-pid, "SIGKILL"); } catch { try { process.kill(pid, "SIGKILL"); } catch { /* exited */ } }
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
terminateNow(): void {
|
|
690
|
+
const pid = typeof this.child?.pid === "number" ? this.child.pid : undefined;
|
|
691
|
+
if (pid === undefined) return;
|
|
692
|
+
try { process.kill(-pid, "SIGKILL"); } catch { try { process.kill(pid, "SIGKILL"); } catch { /* exited */ } }
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
private armIdleTimer(): void {
|
|
696
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
697
|
+
const delay = Math.max(1, EXPLORE_IDLE_MS - (Date.now() - this.lastActivity));
|
|
698
|
+
this.idleTimer = setTimeout(() => void this.stop("explore session idle timeout"), delay);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
private async readResponses(stream: ReadableStream<Uint8Array>): Promise<void> {
|
|
702
|
+
const reader = stream.getReader();
|
|
703
|
+
const decoder = new TextDecoder();
|
|
704
|
+
let buffer = "";
|
|
705
|
+
try {
|
|
706
|
+
for (;;) {
|
|
707
|
+
const chunk = await reader.read();
|
|
708
|
+
if (chunk.done) break;
|
|
709
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
710
|
+
const lines = buffer.split("\n");
|
|
711
|
+
buffer = lines.pop() ?? "";
|
|
712
|
+
for (const line of lines) {
|
|
713
|
+
if (!line.trim()) continue;
|
|
714
|
+
let response: any;
|
|
715
|
+
try { response = JSON.parse(line); } catch { continue; }
|
|
716
|
+
const waiter = this.pending.get(response?.id);
|
|
717
|
+
if (!waiter) continue;
|
|
718
|
+
this.pending.delete(response.id);
|
|
719
|
+
if (response.ok === true) waiter.resolve(response.result);
|
|
720
|
+
else waiter.reject(new ExploreHostError(
|
|
721
|
+
String(response?.error?.message ?? "explore-host request failed"),
|
|
722
|
+
typeof response?.error?.code === "string" ? response.error.code : undefined,
|
|
723
|
+
));
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
} finally {
|
|
727
|
+
if (this.child) await this.stop("explore-host exited unexpectedly");
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
543
732
|
type DoctorStatus = "pass" | "warn" | "fail";
|
|
544
733
|
|
|
545
734
|
type DoctorCheck = {
|
|
@@ -597,6 +786,7 @@ const REQUIRED_EXTENSION_TOOLS = [
|
|
|
597
786
|
"devflow_context_cursor",
|
|
598
787
|
"devflow_design_rule",
|
|
599
788
|
"devflow_mainflow",
|
|
789
|
+
"devflow_explore",
|
|
600
790
|
"devflow_approval",
|
|
601
791
|
];
|
|
602
792
|
|
|
@@ -608,6 +798,7 @@ const REQUIRED_SKILLS = [
|
|
|
608
798
|
"workflow-api-contract",
|
|
609
799
|
"workflow-api-supplement",
|
|
610
800
|
"workflow-acceptance-test-design",
|
|
801
|
+
"workflow-regression-test-design",
|
|
611
802
|
"workflow-backend-tech-spec",
|
|
612
803
|
"workflow-bug-fix",
|
|
613
804
|
"workflow-change-plan",
|
|
@@ -673,8 +864,10 @@ const REQUIRED_SCRIPTS = [
|
|
|
673
864
|
"workflow-orchestrator/scripts/acceptance_test_runner.py",
|
|
674
865
|
"workflow-orchestrator/scripts/machine_evidence.rb",
|
|
675
866
|
"workflow-orchestrator/scripts/review_preflight.py",
|
|
867
|
+
"workflow-orchestrator/scripts/startup_preflight.py",
|
|
676
868
|
"workflow-orchestrator/scripts/cross_track_consistency_check.rb",
|
|
677
869
|
"workflow-orchestrator/scripts/execution_contract.rb",
|
|
870
|
+
"workflow-orchestrator/scripts/explore_preflight.py",
|
|
678
871
|
"workflow-code-index/scripts/code_index.py",
|
|
679
872
|
"workflow-context-sync/scripts/context_sync_cursor.py",
|
|
680
873
|
];
|
|
@@ -947,6 +1140,13 @@ function renderResult(result: ProcessResult, details: Record<string, unknown> =
|
|
|
947
1140
|
};
|
|
948
1141
|
}
|
|
949
1142
|
|
|
1143
|
+
function redactCredentialOutput(result: ProcessResult, values: Record<string, string>): ProcessResult {
|
|
1144
|
+
const secrets = Object.values(values).filter(value => value.length > 0);
|
|
1145
|
+
if (secrets.length === 0) return result;
|
|
1146
|
+
const redact = (text: string) => secrets.reduce((current, secret) => current.split(secret).join("[REDACTED]"), text);
|
|
1147
|
+
return { ...result, stdout: redact(result.stdout), stderr: redact(result.stderr) };
|
|
1148
|
+
}
|
|
1149
|
+
|
|
950
1150
|
function registeredNames(value: unknown): Set<string> {
|
|
951
1151
|
if (Array.isArray(value)) {
|
|
952
1152
|
return new Set(
|
|
@@ -995,6 +1195,16 @@ function parseJsonResult(result: ProcessResult): Record<string, unknown> | null
|
|
|
995
1195
|
}
|
|
996
1196
|
}
|
|
997
1197
|
|
|
1198
|
+
function parseJsonOutput(result: ProcessResult): Record<string, unknown> | null {
|
|
1199
|
+
if (!result.stdout) return null;
|
|
1200
|
+
try {
|
|
1201
|
+
const parsed = JSON.parse(result.stdout);
|
|
1202
|
+
return parsed && typeof parsed === "object" ? parsed as Record<string, unknown> : null;
|
|
1203
|
+
} catch {
|
|
1204
|
+
return null;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
998
1208
|
export function canonicalReviewJson(value: unknown): string {
|
|
999
1209
|
if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
|
|
1000
1210
|
if (typeof value === "number" && Number.isFinite(value)) return JSON.stringify(value);
|
|
@@ -1115,7 +1325,7 @@ export function workflowResumeDisposition(snapshot: Record<string, unknown>): Wo
|
|
|
1115
1325
|
return "blocked";
|
|
1116
1326
|
}
|
|
1117
1327
|
|
|
1118
|
-
function workflowDirective(snapshot: Record<string, unknown>, source: "start" | "approval", requirementDraft?: string): string {
|
|
1328
|
+
function workflowDirective(snapshot: Record<string, unknown>, source: "start" | "approval", requirementDraft?: string, briefing?: string): string {
|
|
1119
1329
|
const selectedStep = selectedWorkflowStep(snapshot);
|
|
1120
1330
|
const reviewInstructions = selectedStep?.id === "code-review"
|
|
1121
1331
|
? [
|
|
@@ -1153,8 +1363,12 @@ function workflowDirective(snapshot: Record<string, unknown>, source: "start" |
|
|
|
1153
1363
|
: [];
|
|
1154
1364
|
return [
|
|
1155
1365
|
"<devflow-runtime-directive>",
|
|
1156
|
-
|
|
1157
|
-
|
|
1366
|
+
briefing
|
|
1367
|
+
? "Devflow Runtime 已完成开工预检(环境、交接、状态、合同、输入),以下开工简报中的事实已经核实;执行合同已激活,不要再次调用 devflow_state inspect 或 devflow_contract activate。读取 skill://workflow-pipeline,并只执行当前项目的一个工作流步骤。"
|
|
1368
|
+
: "Devflow Runtime 已完成环境与状态检查。读取 skill://workflow-pipeline,并只执行当前项目的一个工作流步骤。",
|
|
1369
|
+
...(briefing
|
|
1370
|
+
? ["<startup-briefing>", briefing, "</startup-briefing>"]
|
|
1371
|
+
: ["先调用 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
1372
|
"只按 inspector 返回的当前 step 加载所需 Skill:step.skill 是精确 ID,必须读取 skill://<step.skill>,不得自行添加 workflow- 前缀或改写名称;本回合严禁读取或写入 xd://devflow_transition,严禁调用 devflow_transition,严禁执行 Skill 中的 complete-step/confirm-checkpoint,不得推进下一步骤;状态写回和会话切换由 Runtime 在用户确认后独占处理。",
|
|
1159
1373
|
...reviewInstructions,
|
|
1160
1374
|
...(parallelInstructions.length > 0 ? parallelInstructions : [
|
|
@@ -1584,6 +1798,28 @@ export function approvalReviewText(proposal: ProposalRecord): string {
|
|
|
1584
1798
|
|
|
1585
1799
|
export default function devflowCore(pi: ExtensionAPI) {
|
|
1586
1800
|
const { z } = pi.zod;
|
|
1801
|
+
const testCredentials = new TestCredentialStore();
|
|
1802
|
+
const exploreHost = new ExploreHostManager();
|
|
1803
|
+
process.once("exit", () => exploreHost.terminateNow());
|
|
1804
|
+
|
|
1805
|
+
const redactExploreValue = (value: unknown, secrets: Record<string, string>): unknown => {
|
|
1806
|
+
const values = Object.values(secrets).filter(Boolean);
|
|
1807
|
+
if (typeof value === "string") return values.reduce((text, secret) => text.split(secret).join("[REDACTED]"), value);
|
|
1808
|
+
if (Array.isArray(value)) return value.map(item => redactExploreValue(item, secrets));
|
|
1809
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactExploreValue(item, secrets)]));
|
|
1810
|
+
return value;
|
|
1811
|
+
};
|
|
1812
|
+
const exploreScopePattern = /^(?:e\d+|aria-ref=e\d+|role=[^:\s]+(?::.+)?|testid=.+)$/;
|
|
1813
|
+
|
|
1814
|
+
const exploreHostPath = (): string => {
|
|
1815
|
+
const candidates = [
|
|
1816
|
+
path.join(resolvePluginRoot(), "devflow-quality", "runtime", "playwright", "explore-host.mjs"),
|
|
1817
|
+
path.join(resolvePluginRoot(), "devflow-quality", "runtime", "playwright", "dist", "explore-host.mjs"),
|
|
1818
|
+
];
|
|
1819
|
+
const found = candidates.find(candidate => existsSync(candidate));
|
|
1820
|
+
if (!found) throw new Error("explore-host.mjs is not installed; build the Devflow Playwright Runtime first");
|
|
1821
|
+
return found;
|
|
1822
|
+
};
|
|
1587
1823
|
pi.registerTool({
|
|
1588
1824
|
name: "devflow_project_context",
|
|
1589
1825
|
label: "Devflow Project Context",
|
|
@@ -1690,10 +1926,13 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1690
1926
|
// during extension construction and never becomes model-controlled state.
|
|
1691
1927
|
const cliSessionResumeRequested = process.env.DEVFLOW_CLI_SESSION_RESUME === "1";
|
|
1692
1928
|
delete process.env.DEVFLOW_CLI_SESSION_RESUME;
|
|
1693
|
-
let sessionId = randomUUID();
|
|
1929
|
+
let sessionId: string = randomUUID();
|
|
1694
1930
|
let controlState: SessionControlState = initialControlState();
|
|
1695
1931
|
let activeContract: ActiveExecutionContract | undefined;
|
|
1696
1932
|
let contractAgentCalls = 0;
|
|
1933
|
+
let stepUserNotes: string[] = [];
|
|
1934
|
+
let lastAgentReport = "";
|
|
1935
|
+
let sessionBootstrapContext = "";
|
|
1697
1936
|
const reviewBindingKey = randomBytes(32).toString("hex");
|
|
1698
1937
|
const pendingReviewTasks = new Map<string, { feature: string; contractId: string }>();
|
|
1699
1938
|
const completedReviewTasks = new Map<string, { feature: string; contractId: string; report: Record<string, unknown> }>();
|
|
@@ -1932,6 +2171,63 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1932
2171
|
return undefined;
|
|
1933
2172
|
}
|
|
1934
2173
|
|
|
2174
|
+
async function activateExecutionContract(
|
|
2175
|
+
ctx: {
|
|
2176
|
+
cwd: string;
|
|
2177
|
+
ui: {
|
|
2178
|
+
confirm(title: string, body: string): Promise<boolean>;
|
|
2179
|
+
setStatus(key: string, text: string | undefined): void;
|
|
2180
|
+
setWidget?(key: string, content: unknown, options?: { placement?: "aboveEditor" | "belowEditor" }): void;
|
|
2181
|
+
};
|
|
2182
|
+
},
|
|
2183
|
+
feature: string,
|
|
2184
|
+
step: string,
|
|
2185
|
+
): Promise<{ ok: true; contract: ActiveExecutionContract } | { ok: false; reason: string }> {
|
|
2186
|
+
const inspected = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/execution_contract.rb", [
|
|
2187
|
+
"inspect", "--feature", feature, "--step", step, "--json",
|
|
2188
|
+
]);
|
|
2189
|
+
const payload = parseJsonResult(inspected);
|
|
2190
|
+
const raw = payload?.contract;
|
|
2191
|
+
if (!controlPlaneSucceeded(inspected) || !raw || typeof raw !== "object") {
|
|
2192
|
+
return { ok: false, reason: inspected.stderr || inspected.stdout || "Core returned no execution contract." };
|
|
2193
|
+
}
|
|
2194
|
+
const contract = contractFromRecord(raw as Record<string, unknown>);
|
|
2195
|
+
if (!contract) return { ok: false, reason: "Core returned an invalid execution contract." };
|
|
2196
|
+
const confirmed = await ctx.ui.confirm(
|
|
2197
|
+
"激活 Devflow 执行合同",
|
|
2198
|
+
[
|
|
2199
|
+
`合同:${contract.contractId}`,
|
|
2200
|
+
`Feature:${contract.feature}`,
|
|
2201
|
+
`步骤:${contract.step}`,
|
|
2202
|
+
`STATE revision:${contract.stateRevision ?? "未初始化"}`,
|
|
2203
|
+
`图谱 revision:${contract.graphRevision ?? 0}`,
|
|
2204
|
+
`可写路径:${contract.allowedPaths.join(", ")}`,
|
|
2205
|
+
`允许 Agent:${contract.allowedAgents.join(", ")}`,
|
|
2206
|
+
`固定检查器:${contract.checkerIds.join(", ") || "无"}`,
|
|
2207
|
+
`冻结哈希:${contract.frozenHash}`,
|
|
2208
|
+
"确认后低成本模型只能在以上范围执行;未知项必须阻塞升级。",
|
|
2209
|
+
].join("\n"),
|
|
2210
|
+
);
|
|
2211
|
+
if (!confirmed) return { ok: false, reason: "User declined execution contract activation." };
|
|
2212
|
+
try {
|
|
2213
|
+
const createArgs = ["create", "--feature", feature, "--step", step, "--write", "--json"];
|
|
2214
|
+
if (contract.stateRevision !== undefined) createArgs.push("--expected-state-revision", String(contract.stateRevision));
|
|
2215
|
+
if (contract.graphRevision !== undefined) createArgs.push("--expected-graph-revision", String(contract.graphRevision));
|
|
2216
|
+
const written = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/execution_contract.rb", createArgs);
|
|
2217
|
+
if (!controlPlaneSucceeded(written)) {
|
|
2218
|
+
return { ok: false, reason: written.stderr || written.stdout || "Execution contract activation failed." };
|
|
2219
|
+
}
|
|
2220
|
+
const writtenPayload = parseJsonResult(written);
|
|
2221
|
+
contract.contractPath = typeof writtenPayload?.path === "string" ? writtenPayload.path : ".devflow/planning/contracts/";
|
|
2222
|
+
activeContract = contract;
|
|
2223
|
+
contractAgentCalls = 0;
|
|
2224
|
+
setControlState(ctx, executingState(`合同执行:${contract.contractId}`));
|
|
2225
|
+
return { ok: true, contract };
|
|
2226
|
+
} catch (error) {
|
|
2227
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
|
|
1935
2231
|
async function collectProjectInitialization(ctx: {
|
|
1936
2232
|
cwd: string;
|
|
1937
2233
|
ui: {
|
|
@@ -2055,12 +2351,13 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2055
2351
|
});
|
|
2056
2352
|
}
|
|
2057
2353
|
|
|
2058
|
-
async function runPython(cwd: string, relativeScript: string, args: string[]) {
|
|
2354
|
+
async function runPython(cwd: string, relativeScript: string, args: string[], extraEnv: Record<string, string> = {}) {
|
|
2059
2355
|
assertRuntimeProjectBoundary(cwd);
|
|
2060
2356
|
const kitRoot = resolveKitRoot(cwd);
|
|
2061
2357
|
return runProcess(["python3", path.join(kitRoot, relativeScript), ...args], cwd, {
|
|
2062
2358
|
DEVFLOW_KIT_ROOT: kitRoot,
|
|
2063
2359
|
OMP_PROJECT_DIR: cwd,
|
|
2360
|
+
...extraEnv,
|
|
2064
2361
|
});
|
|
2065
2362
|
}
|
|
2066
2363
|
|
|
@@ -2248,6 +2545,114 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2248
2545
|
return { ok: checks.every(check => check.status !== "fail"), checks, models };
|
|
2249
2546
|
}
|
|
2250
2547
|
|
|
2548
|
+
async function runStartupPreflight(
|
|
2549
|
+
ctx: DoctorContext & {
|
|
2550
|
+
ui: {
|
|
2551
|
+
confirm(title: string, body: string): Promise<boolean>;
|
|
2552
|
+
notify(message: string, level: "info" | "error"): void;
|
|
2553
|
+
setStatus(key: string, text: string | undefined): void;
|
|
2554
|
+
setWidget?(key: string, content: unknown, options?: { placement?: "aboveEditor" | "belowEditor" }): void;
|
|
2555
|
+
};
|
|
2556
|
+
},
|
|
2557
|
+
snapshot: Record<string, unknown>,
|
|
2558
|
+
options: { doctorAlreadyPassed?: boolean; handoffPath?: string } = {},
|
|
2559
|
+
): Promise<{ ok: true; briefing?: string } | { ok: false }> {
|
|
2560
|
+
if (options.doctorAlreadyPassed !== true) {
|
|
2561
|
+
const report = await runDoctor(ctx);
|
|
2562
|
+
if (!report.ok) {
|
|
2563
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检失败" });
|
|
2564
|
+
ctx.ui.notify(formatDoctorReport(report).slice(0, 10000), "error");
|
|
2565
|
+
return { ok: false };
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
if (workflowResumeDisposition(snapshot) !== "execute" || snapshot.action === "run_parallel_window") {
|
|
2569
|
+
return { ok: true };
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
try {
|
|
2573
|
+
const feature = typeof snapshot.current_feature === "string" ? snapshot.current_feature : "";
|
|
2574
|
+
const step = String(selectedWorkflowStep(snapshot)?.id ?? "");
|
|
2575
|
+
if (!feature || !step) {
|
|
2576
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检未通过" });
|
|
2577
|
+
ctx.ui.notify("开工预检未通过:当前 Feature 或步骤不可用。", "error");
|
|
2578
|
+
return { ok: false };
|
|
2579
|
+
}
|
|
2580
|
+
let contract = activeContract;
|
|
2581
|
+
if (!contract || contract.feature !== feature || contract.step !== step || !(await contractIsFresh(ctx.cwd, contract))) {
|
|
2582
|
+
const activated = await activateExecutionContract(ctx, feature, step);
|
|
2583
|
+
if (!activated.ok) {
|
|
2584
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检:执行合同未激活" });
|
|
2585
|
+
ctx.ui.notify(`开工预检:执行合同未激活。${activated.reason}`, "error");
|
|
2586
|
+
return { ok: false };
|
|
2587
|
+
}
|
|
2588
|
+
contract = activated.contract;
|
|
2589
|
+
}
|
|
2590
|
+
if (!contract.contractPath) {
|
|
2591
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检:执行合同未激活" });
|
|
2592
|
+
ctx.ui.notify("开工预检:执行合同未激活。合同冻结路径不可用。", "error");
|
|
2593
|
+
return { ok: false };
|
|
2594
|
+
}
|
|
2595
|
+
const contractAbsolute = path.resolve(ctx.cwd, contract.contractPath);
|
|
2596
|
+
const contractRelative = path.relative(ctx.cwd, contractAbsolute).split(path.sep).join("/");
|
|
2597
|
+
if (!contractRelative || contractRelative === ".." || contractRelative.startsWith("../")) {
|
|
2598
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检:执行合同未激活" });
|
|
2599
|
+
ctx.ui.notify("开工预检:执行合同未激活。合同冻结路径越出项目。", "error");
|
|
2600
|
+
return { ok: false };
|
|
2601
|
+
}
|
|
2602
|
+
const preflight = await runPython(ctx.cwd, "workflow-orchestrator/scripts/startup_preflight.py", [
|
|
2603
|
+
"--root", ctx.cwd,
|
|
2604
|
+
"--feature", feature,
|
|
2605
|
+
"--step", step,
|
|
2606
|
+
"--contract", contractRelative,
|
|
2607
|
+
...(options.handoffPath ? ["--handoff", options.handoffPath] : []),
|
|
2608
|
+
]);
|
|
2609
|
+
const payload = parseJsonOutput(preflight);
|
|
2610
|
+
if (!payload || payload.status !== "ready") {
|
|
2611
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检未通过" });
|
|
2612
|
+
const issues = Array.isArray(payload?.issues) ? payload.issues : [];
|
|
2613
|
+
const details = issues.length > 0
|
|
2614
|
+
? issues.map(issue => `- ${describeStartupIssue(issue && typeof issue === "object" ? issue as Record<string, unknown> : {})}`).join("\n")
|
|
2615
|
+
: "- 开工预检输出无法解析";
|
|
2616
|
+
ctx.ui.notify(`开工预检未通过:\n${details}`, "error");
|
|
2617
|
+
return { ok: false };
|
|
2618
|
+
}
|
|
2619
|
+
const facts = payload.facts && typeof payload.facts === "object" ? payload.facts as Record<string, unknown> : {};
|
|
2620
|
+
const handoff = payload.handoff && typeof payload.handoff === "object" ? payload.handoff as Record<string, unknown> : {};
|
|
2621
|
+
return {
|
|
2622
|
+
ok: true,
|
|
2623
|
+
briefing: renderStartupBriefing(facts, handoff, {
|
|
2624
|
+
revision: snapshot.revision,
|
|
2625
|
+
handoffProvided: Boolean(options.handoffPath),
|
|
2626
|
+
}),
|
|
2627
|
+
};
|
|
2628
|
+
} catch (error) {
|
|
2629
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检未通过" });
|
|
2630
|
+
ctx.ui.notify(`开工预检未通过:${error instanceof Error ? error.message : String(error)}`, "error");
|
|
2631
|
+
return { ok: false };
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
async function startWorkflowAfterPreflight(
|
|
2636
|
+
ctx: DoctorContext & {
|
|
2637
|
+
ui: {
|
|
2638
|
+
confirm(title: string, body: string): Promise<boolean>;
|
|
2639
|
+
notify(message: string, level: "info" | "error"): void;
|
|
2640
|
+
setStatus(key: string, text: string | undefined): void;
|
|
2641
|
+
setWidget?(key: string, content: unknown, options?: { placement?: "aboveEditor" | "belowEditor" }): void;
|
|
2642
|
+
};
|
|
2643
|
+
},
|
|
2644
|
+
snapshot: Record<string, unknown>,
|
|
2645
|
+
source: "start" | "approval",
|
|
2646
|
+
options: { doctorAlreadyPassed?: boolean; handoffPath?: string; executingLabel?: string } = {},
|
|
2647
|
+
): Promise<void> {
|
|
2648
|
+
const preflight = await runStartupPreflight(ctx, snapshot, options);
|
|
2649
|
+
if (!preflight.ok) return;
|
|
2650
|
+
setControlState(ctx, executingState(options.executingLabel));
|
|
2651
|
+
if (preflight.briefing) await triggerWorkflow(snapshot, source, undefined, preflight.briefing);
|
|
2652
|
+
else if (source === "start") await triggerWorkflow(snapshot, "start");
|
|
2653
|
+
else await triggerWorkflow(snapshot, "approval");
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2251
2656
|
async function inspectWorkflow(cwd: string): Promise<{ result: ProcessResult; snapshot: Record<string, unknown> | null }> {
|
|
2252
2657
|
const result = await runRuby(cwd, "workflow-orchestrator/scripts/inspect_state.rb", ["--json"]);
|
|
2253
2658
|
return { result, snapshot: parseJsonResult(result) };
|
|
@@ -2294,26 +2699,33 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2294
2699
|
async function requestControlledRollover(ctx: { cwd: string; sessionManager: { getSessionId(): string } }, snapshot: Record<string, unknown>) {
|
|
2295
2700
|
const revision = typeof snapshot.revision === "number" ? snapshot.revision : undefined;
|
|
2296
2701
|
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
|
-
|
|
2702
|
+
const carryPath = path.join(tmpdir(), `devflow-rollover-carry-${randomUUID()}.json`);
|
|
2703
|
+
try {
|
|
2704
|
+
await writeFile(carryPath, JSON.stringify({ user_notes: stepUserNotes, agent_report: lastAgentReport }), "utf8");
|
|
2705
|
+
const prepared = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/session_rollover.rb", [
|
|
2706
|
+
"prepare", "--write", "--json", "--expected-revision", String(revision),
|
|
2707
|
+
"--source-session-id", ctx.sessionManager.getSessionId(), "--reason", "workflow_step_boundary",
|
|
2708
|
+
"--carry-file", carryPath,
|
|
2709
|
+
]);
|
|
2710
|
+
const payload = parseJsonResult(prepared);
|
|
2711
|
+
if (!prepared.ok || !payload || typeof payload.revision !== "number") {
|
|
2712
|
+
throw new Error(prepared.stderr || prepared.stdout || "rollover prepare failed");
|
|
2713
|
+
}
|
|
2714
|
+
const rollover = payload.rollover as Record<string, unknown> | undefined;
|
|
2715
|
+
const rolloverId = typeof rollover?.id === "string" ? rollover.id : undefined;
|
|
2716
|
+
if (!rolloverId) throw new Error("rollover prepare returned no rollover id");
|
|
2717
|
+
const synced = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/sync_artifacts.rb", [
|
|
2718
|
+
"--write", "--no-preserve", "--json",
|
|
2719
|
+
]);
|
|
2720
|
+
if (!controlPlaneSucceeded(synced)) {
|
|
2721
|
+
throw new Error(synced.stderr || synced.stdout || "artifact registry sync after rollover prepare failed");
|
|
2722
|
+
}
|
|
2723
|
+
// The runtime can observe the request immediately; arm the switch handler first.
|
|
2724
|
+
pendingRollover = { snapshot, preparedRevision: payload.revision, rolloverId };
|
|
2725
|
+
await enqueueControlledRolloverRequest(ctx.cwd, rolloverId, ctx.sessionManager.getSessionId());
|
|
2726
|
+
} finally {
|
|
2727
|
+
await unlink(carryPath).catch(() => undefined);
|
|
2313
2728
|
}
|
|
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
2729
|
}
|
|
2318
2730
|
|
|
2319
2731
|
function parallelBranches(snapshot: Record<string, unknown>): Array<{ workflow_step: string; track: string }> {
|
|
@@ -2361,7 +2773,9 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2361
2773
|
return { ...snapshot, parallel_run: payload.run };
|
|
2362
2774
|
}
|
|
2363
2775
|
|
|
2364
|
-
async function triggerWorkflow(snapshot: Record<string, unknown>, source: "start" | "approval", requirementDraft?: string) {
|
|
2776
|
+
async function triggerWorkflow(snapshot: Record<string, unknown>, source: "start" | "approval", requirementDraft?: string, briefing?: string) {
|
|
2777
|
+
stepUserNotes = [];
|
|
2778
|
+
lastAgentReport = "";
|
|
2365
2779
|
if (snapshot.action !== "run_parallel_window") {
|
|
2366
2780
|
parallelRunActive = false;
|
|
2367
2781
|
parallelContinuationQueued = false;
|
|
@@ -2372,26 +2786,35 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2372
2786
|
}
|
|
2373
2787
|
const preparedSnapshot = await prepareParallelRun(projectRoot ?? process.cwd(), snapshot);
|
|
2374
2788
|
activeWorkflowTurn = true;
|
|
2375
|
-
await pi.sendUserMessage(workflowDirective(preparedSnapshot, source, requirementDraft));
|
|
2789
|
+
await pi.sendUserMessage(workflowDirective(preparedSnapshot, source, requirementDraft, briefing));
|
|
2376
2790
|
}
|
|
2377
2791
|
|
|
2378
2792
|
async function resumeInspectedWorkflow(
|
|
2379
2793
|
ctx: {
|
|
2380
2794
|
cwd: string;
|
|
2795
|
+
models: { resolve(spec: string): ModelLike | undefined };
|
|
2381
2796
|
ui: {
|
|
2797
|
+
confirm(title: string, body: string): Promise<boolean>;
|
|
2382
2798
|
notify(message: string, level: "info" | "error"): void;
|
|
2383
2799
|
setStatus(key: string, text: string | undefined): void;
|
|
2384
2800
|
setWidget?(key: string, content: string[] | undefined, options?: { placement?: "aboveEditor" | "belowEditor" }): void;
|
|
2385
2801
|
};
|
|
2386
2802
|
},
|
|
2387
2803
|
snapshot: Record<string, unknown>,
|
|
2804
|
+
options: { doctorAlreadyPassed: boolean },
|
|
2388
2805
|
): Promise<WorkflowResumeDisposition> {
|
|
2389
2806
|
const disposition = workflowResumeDisposition(snapshot);
|
|
2390
2807
|
const stepId = String(selectedWorkflowStep(snapshot)?.id ?? "当前步骤");
|
|
2391
2808
|
if (disposition === "execute") {
|
|
2392
|
-
setControlState(ctx, executingState(
|
|
2393
|
-
ctx.ui.notify(`Devflow 已恢复到 ${stepId}
|
|
2394
|
-
|
|
2809
|
+
setControlState(ctx, executingState("开工预检中"));
|
|
2810
|
+
ctx.ui.notify(`Devflow 已恢复到 ${stepId},当前步骤产物尚未完成,正在完成开工预检。`, "info");
|
|
2811
|
+
void startWorkflowAfterPreflight(ctx, snapshot, "start", {
|
|
2812
|
+
doctorAlreadyPassed: options.doctorAlreadyPassed,
|
|
2813
|
+
executingLabel: `恢复并执行 ${stepId}`,
|
|
2814
|
+
}).catch(error => {
|
|
2815
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检未通过" });
|
|
2816
|
+
ctx.ui.notify(`开工预检未通过:${String(error)}`, "error");
|
|
2817
|
+
});
|
|
2395
2818
|
return disposition;
|
|
2396
2819
|
}
|
|
2397
2820
|
if (disposition === "review") {
|
|
@@ -2488,6 +2911,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2488
2911
|
|
|
2489
2912
|
type ProjectControlCommandContext = {
|
|
2490
2913
|
cwd: string;
|
|
2914
|
+
models: DoctorContext["models"];
|
|
2491
2915
|
sessionManager: { getSessionId(): string };
|
|
2492
2916
|
ui: ProjectControlUi & {
|
|
2493
2917
|
select(title: string, options: string[]): Promise<string | undefined>;
|
|
@@ -2668,6 +3092,45 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2668
3092
|
}
|
|
2669
3093
|
}
|
|
2670
3094
|
|
|
3095
|
+
async function manageTestCredentials(ctx: ProjectControlCommandContext): Promise<void> {
|
|
3096
|
+
const names = declaredSecretEnvironmentNames(ctx.cwd);
|
|
3097
|
+
if (names.length === 0) {
|
|
3098
|
+
ctx.ui.notify("当前项目没有声明测试凭据(secret_env)。", "info");
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
3101
|
+
for (;;) {
|
|
3102
|
+
const labels = names.map(name => `${name}(${testCredentials.has(name) || process.env[name] !== undefined ? "已设置" : "未设置"})`);
|
|
3103
|
+
const selected = await ctx.ui.select("测试凭据(输入内容会显示在屏幕上,不会持久化)", [
|
|
3104
|
+
...labels,
|
|
3105
|
+
"清除全部",
|
|
3106
|
+
"返回",
|
|
3107
|
+
]);
|
|
3108
|
+
if (!selected || selected === "返回") return;
|
|
3109
|
+
if (selected === "清除全部") {
|
|
3110
|
+
if (await ctx.ui.confirm("清除全部测试凭据", "只清除本次 devflow-pi 会话内存中的面板凭据;不会修改启动环境。继续吗?")) {
|
|
3111
|
+
testCredentials.clearAll();
|
|
3112
|
+
ctx.ui.notify("已清除本次会话的面板凭据。", "info");
|
|
3113
|
+
}
|
|
3114
|
+
continue;
|
|
3115
|
+
}
|
|
3116
|
+
const index = labels.indexOf(selected);
|
|
3117
|
+
const name = names[index];
|
|
3118
|
+
if (!name) continue;
|
|
3119
|
+
const action = await ctx.ui.select(`测试凭据:${name}`, ["设置/覆盖", "清除面板值", "返回"]);
|
|
3120
|
+
if (!action || action === "返回") continue;
|
|
3121
|
+
if (action === "清除面板值") {
|
|
3122
|
+
testCredentials.clear(name);
|
|
3123
|
+
ctx.ui.notify(`已清除 ${name} 的面板值;启动环境值仍可能存在。`, "info");
|
|
3124
|
+
continue;
|
|
3125
|
+
}
|
|
3126
|
+
const value = await ctx.ui.input(`输入测试凭据:${name}`, "输入值(会显示在屏幕上,不会持久化)");
|
|
3127
|
+
if (value === undefined) continue;
|
|
3128
|
+
if (!value) testCredentials.clear(name);
|
|
3129
|
+
else testCredentials.set(name, value);
|
|
3130
|
+
ctx.ui.notify(`${name} 已${value ? "设置" : "清除"}(值不会显示)。`, "info");
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
|
|
2671
3134
|
async function openProjectControlPanel(ctx: ProjectControlCommandContext): Promise<void> {
|
|
2672
3135
|
for (;;) {
|
|
2673
3136
|
// The control panel is a read-only projection. Each action below reads a
|
|
@@ -2683,6 +3146,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2683
3146
|
if (snapshot.action === "await_user_confirmation") options.push("审查并批准推进…");
|
|
2684
3147
|
if (hasSafeRecoveryPlan(snapshot)) options.push("执行安全恢复…");
|
|
2685
3148
|
if (typeof snapshot.current_feature === "string" && !["completed", "cancelled", "failed"].includes(String(snapshot.status))) options.push("取消当前功能…");
|
|
3149
|
+
options.push("测试凭据…");
|
|
2686
3150
|
const approvalCount = await pendingApprovalCount(ctx.cwd);
|
|
2687
3151
|
const approvalOption = approvalCount === null ? null : `待审批(${approvalCount})…`;
|
|
2688
3152
|
if (approvalOption) options.push(approvalOption);
|
|
@@ -2701,7 +3165,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2701
3165
|
ctx.ui.notify(`启动前状态读取失败:${fresh.result.stderr || fresh.result.stdout}`, "error");
|
|
2702
3166
|
return;
|
|
2703
3167
|
}
|
|
2704
|
-
await resumeInspectedWorkflow(ctx, fresh.snapshot);
|
|
3168
|
+
await resumeInspectedWorkflow(ctx, fresh.snapshot, { doctorAlreadyPassed: false });
|
|
2705
3169
|
return;
|
|
2706
3170
|
}
|
|
2707
3171
|
if (selected === "审查并批准推进…") {
|
|
@@ -2716,6 +3180,10 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2716
3180
|
await cancelFeatureFromPanel(ctx);
|
|
2717
3181
|
return;
|
|
2718
3182
|
}
|
|
3183
|
+
if (selected === "测试凭据…") {
|
|
3184
|
+
await manageTestCredentials(ctx);
|
|
3185
|
+
continue;
|
|
3186
|
+
}
|
|
2719
3187
|
if (approvalOption && selected === approvalOption) {
|
|
2720
3188
|
await reviewPendingApprovalsFromPanel(ctx);
|
|
2721
3189
|
continue;
|
|
@@ -2797,6 +3265,36 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2797
3265
|
|
|
2798
3266
|
pi.setLabel("Devflow Project Runtime");
|
|
2799
3267
|
|
|
3268
|
+
async function refreshSessionBootstrapContext(
|
|
3269
|
+
ctx: { cwd: string },
|
|
3270
|
+
source: "startup" | "rollover",
|
|
3271
|
+
): Promise<{ ok: true } | { ok: false; reason: string }> {
|
|
3272
|
+
sessionBootstrapContext = "";
|
|
3273
|
+
try {
|
|
3274
|
+
const result = await runHook(ctx.cwd, "start.py", {
|
|
3275
|
+
session_id: sessionId,
|
|
3276
|
+
cwd: ctx.cwd,
|
|
3277
|
+
source,
|
|
3278
|
+
hook_event_name: "SessionStart",
|
|
3279
|
+
});
|
|
3280
|
+
if (!result.ok) return { ok: false, reason: result.stderr || result.stdout || "start.py did not complete successfully" };
|
|
3281
|
+
const bootstrap = parseJsonResult(result);
|
|
3282
|
+
const hookOutput = bootstrap?.hookSpecificOutput;
|
|
3283
|
+
const additionalContext = hookOutput && typeof hookOutput === "object"
|
|
3284
|
+
? (hookOutput as Record<string, unknown>).additionalContext
|
|
3285
|
+
: undefined;
|
|
3286
|
+
if (typeof additionalContext === "string" && additionalContext) {
|
|
3287
|
+
sessionBootstrapContext = additionalContext.length > 8000
|
|
3288
|
+
? `${additionalContext.slice(0, 7999)}…`
|
|
3289
|
+
: additionalContext;
|
|
3290
|
+
}
|
|
3291
|
+
return { ok: true };
|
|
3292
|
+
} catch (error) {
|
|
3293
|
+
sessionBootstrapContext = "";
|
|
3294
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
|
|
2800
3298
|
pi.on("session_start", async (_event, ctx) => {
|
|
2801
3299
|
projectRoot = ctx.cwd;
|
|
2802
3300
|
sessionId = randomUUID();
|
|
@@ -2806,14 +3304,9 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2806
3304
|
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
2807
3305
|
return;
|
|
2808
3306
|
}
|
|
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");
|
|
3307
|
+
const bootstrap = await refreshSessionBootstrapContext(ctx, "startup");
|
|
3308
|
+
if (!bootstrap.ok) {
|
|
3309
|
+
ctx.ui.notify(`Devflow bootstrap failed: ${bootstrap.reason}`, "error");
|
|
2817
3310
|
return;
|
|
2818
3311
|
}
|
|
2819
3312
|
ctx.ui.setStatus("devflow", "Devflow ready");
|
|
@@ -2866,8 +3359,15 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2866
3359
|
ctx.ui.notify(`会话已恢复,但工作流校验失败,未启动 AI:${String(after.snapshot.reason ?? "state validation failed")}`, "error");
|
|
2867
3360
|
return;
|
|
2868
3361
|
}
|
|
2869
|
-
setControlState(ctx, executingState("
|
|
2870
|
-
|
|
3362
|
+
setControlState(ctx, executingState("开工预检中"));
|
|
3363
|
+
void startWorkflowAfterPreflight(ctx, after.snapshot, "approval", {
|
|
3364
|
+
handoffPath: typeof rollover.handoff_path === "string" ? rollover.handoff_path : undefined,
|
|
3365
|
+
executingLabel: "恢复已认领的干净会话",
|
|
3366
|
+
}).catch(error => {
|
|
3367
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检未通过" });
|
|
3368
|
+
ctx.ui.notify(`开工预检未通过:${String(error)}`, "error");
|
|
3369
|
+
});
|
|
3370
|
+
return;
|
|
2871
3371
|
} else if (!consumed.ok || !synced || !controlPlaneSucceeded(synced)) {
|
|
2872
3372
|
ctx.ui.notify(`已认领的会话交接无法恢复;工作流保持冻结:${consumed.stderr || consumed.stdout}`, "error");
|
|
2873
3373
|
}
|
|
@@ -2883,7 +3383,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2883
3383
|
ctx.ui.notify(`Devflow resume failed: ${inspected.result.stderr || inspected.result.stdout || "invalid inspector JSON"}`, "error");
|
|
2884
3384
|
return;
|
|
2885
3385
|
}
|
|
2886
|
-
await resumeInspectedWorkflow(ctx, inspected.snapshot);
|
|
3386
|
+
await resumeInspectedWorkflow(ctx, inspected.snapshot, { doctorAlreadyPassed: true });
|
|
2887
3387
|
}
|
|
2888
3388
|
});
|
|
2889
3389
|
|
|
@@ -2925,6 +3425,8 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2925
3425
|
ctx.ui.notify(`Devflow 会话交接消费或 registry 同步失败;工作流保持冻结:${consumed.stderr || consumed.stdout || synced?.stderr || synced?.stdout}`, "error");
|
|
2926
3426
|
return;
|
|
2927
3427
|
}
|
|
3428
|
+
sessionId = targetSessionId;
|
|
3429
|
+
await refreshSessionBootstrapContext(ctx, "rollover");
|
|
2928
3430
|
pendingRollover = undefined;
|
|
2929
3431
|
activeContract = undefined;
|
|
2930
3432
|
contractAgentCalls = 0;
|
|
@@ -2934,8 +3436,15 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2934
3436
|
ctx.ui.notify(`会话已切换,但工作流校验失败,未启动 AI:${String(next.snapshot?.reason ?? next.result.stderr ?? next.result.stdout ?? "state validation failed")}`, "error");
|
|
2935
3437
|
return;
|
|
2936
3438
|
}
|
|
2937
|
-
|
|
2938
|
-
|
|
3439
|
+
const claimedRollover = claimedPayload.rollover as Record<string, unknown> | undefined;
|
|
3440
|
+
setControlState(ctx, executingState("开工预检中"));
|
|
3441
|
+
void startWorkflowAfterPreflight(ctx, next.snapshot, "approval", {
|
|
3442
|
+
handoffPath: typeof claimedRollover?.handoff_path === "string" ? claimedRollover.handoff_path : undefined,
|
|
3443
|
+
executingLabel: "在干净会话中执行下一工作流步骤",
|
|
3444
|
+
}).catch(error => {
|
|
3445
|
+
setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "开工预检未通过" });
|
|
3446
|
+
ctx.ui.notify(`开工预检未通过:${String(error)}`, "error");
|
|
3447
|
+
});
|
|
2939
3448
|
});
|
|
2940
3449
|
|
|
2941
3450
|
pi.on("before_agent_start", async event => {
|
|
@@ -2943,11 +3452,25 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2943
3452
|
await appendEngineEvidence(projectRoot, "ai_runtime_initialized", { source: "runtime_before_agent_start" });
|
|
2944
3453
|
await appendEngineEvidence(projectRoot, "ai_invocation", { source: "runtime_before_agent_start" });
|
|
2945
3454
|
}
|
|
2946
|
-
return {
|
|
3455
|
+
return {
|
|
3456
|
+
systemPrompt: [
|
|
3457
|
+
...event.systemPrompt,
|
|
3458
|
+
...(sessionBootstrapContext ? [sessionBootstrapContext] : []),
|
|
3459
|
+
DEVFLOW_CHINESE_LANGUAGE_POLICY,
|
|
3460
|
+
],
|
|
3461
|
+
};
|
|
2947
3462
|
});
|
|
2948
3463
|
|
|
2949
3464
|
pi.on("agent_end", async (event, ctx) => {
|
|
2950
3465
|
if (!activeWorkflowTurn || event.willContinue) return;
|
|
3466
|
+
const assistant = [...event.messages].reverse().find(message => (
|
|
3467
|
+
message && typeof message === "object" && (message as { role?: unknown }).role === "assistant"
|
|
3468
|
+
));
|
|
3469
|
+
const content = assistant && typeof assistant === "object" ? (assistant as { content?: unknown }).content : undefined;
|
|
3470
|
+
lastAgentReport = Array.isArray(content)
|
|
3471
|
+
? content.flatMap(part => part && typeof part === "object" && (part as { type?: unknown }).type === "text"
|
|
3472
|
+
&& typeof (part as { text?: unknown }).text === "string" ? [(part as { text: string }).text] : []).join("")
|
|
3473
|
+
: "";
|
|
2951
3474
|
activeWorkflowTurn = false;
|
|
2952
3475
|
if (parallelRunActive) {
|
|
2953
3476
|
parallelContinuationQueued = false;
|
|
@@ -2983,6 +3506,10 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2983
3506
|
pi.on("input", async (event, ctx) => {
|
|
2984
3507
|
if (event.source !== "interactive") return;
|
|
2985
3508
|
const controlAction = classifyNaturalControlAction(event.text);
|
|
3509
|
+
const stepNote = event.text.trim();
|
|
3510
|
+
if (controlState.intent === "workflow-execute" && !controlAction && stepNote && !stepNote.startsWith("/")) {
|
|
3511
|
+
stepUserNotes.push(stepNote);
|
|
3512
|
+
}
|
|
2986
3513
|
if (controlState.intent === "requirement-draft" && controlState.phase === "planning") {
|
|
2987
3514
|
const requirementDraft = event.text.trim();
|
|
2988
3515
|
if (!requirementDraft) return { handled: true };
|
|
@@ -3471,6 +3998,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3471
3998
|
});
|
|
3472
3999
|
|
|
3473
4000
|
pi.on("session_stop", async (_event, ctx) => {
|
|
4001
|
+
await exploreHost.stop("Devflow session stopped");
|
|
3474
4002
|
const result = await runHook(ctx.cwd, "context_sync_gate.py", {
|
|
3475
4003
|
session_id: sessionId,
|
|
3476
4004
|
cwd: ctx.cwd,
|
|
@@ -3775,6 +4303,103 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3775
4303
|
},
|
|
3776
4304
|
});
|
|
3777
4305
|
|
|
4306
|
+
pi.registerTool({
|
|
4307
|
+
name: "devflow_explore",
|
|
4308
|
+
label: "Devflow Explore",
|
|
4309
|
+
description: [
|
|
4310
|
+
"在已批准的 environment_profile 内启动结构化页面探索会话;探索结果不是验收结论,draft 仍必须经过编译、校验和用户批准。",
|
|
4311
|
+
"草稿到执行唯一经过:人工检查 review_items → regression-test-design 冻结 TC 并生成 tests.json → regression-dsl 使用 compile_dsl.py <草稿或 case-descriptions 路径>(无 --draft)编译、校验并提交 execution_plan 批准 → run-acceptance-tests 执行;草稿不能直接执行,也不能用内置浏览器重放代替验收执行。冻结 TC 若需激活合同,请通过 /devflow control → 启动当前可执行步骤进入。DSL 的 screenshot 只供用户在本地查看。",
|
|
4312
|
+
"凭据只按 profile.secret_env 注入 explore-host 子进程,永不写入 process.env、日志或工具输出。页面文本可能包含个人信息,本工具不做页面级个人信息脱敏。",
|
|
4313
|
+
].join(" "),
|
|
4314
|
+
parameters: z.object({
|
|
4315
|
+
action: z.enum(["start", "observe", "act", "assert", "draft", "stop"]),
|
|
4316
|
+
feature: z.string().min(1).optional(),
|
|
4317
|
+
environment_profile: z.string().min(1).optional(),
|
|
4318
|
+
url: z.string().min(1).optional(),
|
|
4319
|
+
scope: z.string().regex(exploreScopePattern, "scope must be a ref, role selector, or testid selector").optional(),
|
|
4320
|
+
full: z.boolean().optional(),
|
|
4321
|
+
table_text: z.boolean().optional(),
|
|
4322
|
+
ref: z.string().min(1).optional(),
|
|
4323
|
+
operation: z.enum(["click", "fill", "check", "uncheck", "select", "press"]).optional(),
|
|
4324
|
+
action_name: z.string().optional(),
|
|
4325
|
+
value: z.string().optional(),
|
|
4326
|
+
value_from_secret: z.string().optional(),
|
|
4327
|
+
kind: z.string().optional(),
|
|
4328
|
+
match: z.string().optional(),
|
|
4329
|
+
expected: z.unknown().optional(),
|
|
4330
|
+
name: z.string().optional(),
|
|
4331
|
+
tc_id: z.string().optional(),
|
|
4332
|
+
}),
|
|
4333
|
+
approval: args => args.action === "observe" || args.action === "stop" ? "read" : "write",
|
|
4334
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
4335
|
+
const currentSecrets = () => exploreHost.secrets;
|
|
4336
|
+
const fail = (message: string, code = "invalid_request") => ({
|
|
4337
|
+
content: [{ type: "text" as const, text: String(redactExploreValue(message, currentSecrets())) }],
|
|
4338
|
+
details: { code },
|
|
4339
|
+
isError: true,
|
|
4340
|
+
});
|
|
4341
|
+
const response = (result: unknown) => ({
|
|
4342
|
+
content: [{ type: "text" as const, text: JSON.stringify(redactExploreValue(result, currentSecrets()), null, 2) }],
|
|
4343
|
+
details: redactExploreValue(result, currentSecrets()),
|
|
4344
|
+
});
|
|
4345
|
+
try {
|
|
4346
|
+
if (activeContract && !["regression-test-design", "regression-dsl", "generate-mainflow-cases"].includes(activeContract.step)) {
|
|
4347
|
+
return fail("devflow_explore is only available while idle or during regression-test-design/DSL generation", "tool_not_allowed");
|
|
4348
|
+
}
|
|
4349
|
+
if (params.action === "stop") {
|
|
4350
|
+
await exploreHost.stop();
|
|
4351
|
+
return response({ ok: true, stopped: true });
|
|
4352
|
+
}
|
|
4353
|
+
if (params.action === "start") {
|
|
4354
|
+
if (!params.feature || !params.environment_profile || !params.url) return fail("start requires feature, environment_profile and url");
|
|
4355
|
+
assertRuntimeProjectBoundary(ctx.cwd);
|
|
4356
|
+
const preflight = await runPython(ctx.cwd, "workflow-orchestrator/scripts/explore_preflight.py", [
|
|
4357
|
+
"--root", ctx.cwd, "--feature", params.feature, "--environment-profile", params.environment_profile,
|
|
4358
|
+
]);
|
|
4359
|
+
const checked = parseJsonResult(preflight);
|
|
4360
|
+
if (!preflight.ok || checked?.ok !== true) {
|
|
4361
|
+
return renderResult({ ...preflight, ok: false, exitCode: preflight.exitCode || 1 }, { code: checked?.reason ?? "environment_profile_not_approved" });
|
|
4362
|
+
}
|
|
4363
|
+
const secretNames = Array.isArray(checked.secret_names) ? checked.secret_names.filter((name): name is string => typeof name === "string") : [];
|
|
4364
|
+
const panelSecrets = testCredentials.forEnvironment(secretNames);
|
|
4365
|
+
const childSecrets: Record<string, string> = {};
|
|
4366
|
+
const missing: string[] = [];
|
|
4367
|
+
for (const name of secretNames) {
|
|
4368
|
+
const value = panelSecrets[name] ?? process.env[name];
|
|
4369
|
+
if (value === undefined || value === "") missing.push(name);
|
|
4370
|
+
else childSecrets[name] = value;
|
|
4371
|
+
}
|
|
4372
|
+
if (missing.length) return fail(`environment secret is missing: ${missing.join(", ")}`, "environment_secret_missing");
|
|
4373
|
+
await exploreHost.start(exploreHostPath(), ctx.cwd, String(checked.profile_path), childSecrets);
|
|
4374
|
+
const result = await exploreHost.request("start", {
|
|
4375
|
+
feature: params.feature, environment_profile: params.environment_profile, url: params.url,
|
|
4376
|
+
});
|
|
4377
|
+
return response(result);
|
|
4378
|
+
}
|
|
4379
|
+
if (!exploreHost.running) return fail("no active explore session", "session_missing");
|
|
4380
|
+
const requestParams: Record<string, unknown> = {};
|
|
4381
|
+
if (params.action === "observe") Object.assign(requestParams, { scope: params.scope, full: params.full, table_text: params.table_text });
|
|
4382
|
+
else if (params.action === "act") {
|
|
4383
|
+
if (!params.ref || !params.operation) return fail("act requires ref and operation");
|
|
4384
|
+
if (!["click", "fill", "check", "uncheck", "select", "press"].includes(params.operation)) return fail("act operation is invalid");
|
|
4385
|
+
Object.assign(requestParams, { ref: params.ref, action: params.operation, value: params.value, value_from_secret: params.value_from_secret });
|
|
4386
|
+
} else if (params.action === "assert") {
|
|
4387
|
+
if (!params.ref || !params.kind || !["text", "visible", "value", "each_text"].includes(params.kind)) return fail("assert requires ref and a supported kind");
|
|
4388
|
+
Object.assign(requestParams, { ref: params.ref, kind: params.kind, match: params.match, expected: params.expected });
|
|
4389
|
+
} else if (params.action === "draft") {
|
|
4390
|
+
if (!params.name) return fail("draft requires name");
|
|
4391
|
+
Object.assign(requestParams, { name: params.name, tc_id: params.tc_id });
|
|
4392
|
+
}
|
|
4393
|
+
return response(await exploreHost.request(params.action, requestParams));
|
|
4394
|
+
} catch (error) {
|
|
4395
|
+
if (params.action === "start") await exploreHost.stop("explore start failed");
|
|
4396
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4397
|
+
const code = error instanceof ExploreHostError && error.code ? error.code : "explore_failed";
|
|
4398
|
+
return fail(`[${code}] ${message}`, code);
|
|
4399
|
+
}
|
|
4400
|
+
},
|
|
4401
|
+
});
|
|
4402
|
+
|
|
3778
4403
|
pi.registerTool({
|
|
3779
4404
|
name: "devflow_run_acceptance_tests",
|
|
3780
4405
|
label: "Devflow Run Acceptance Tests",
|
|
@@ -3795,10 +4420,17 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3795
4420
|
approval: args => args.action === "run" || (args.action === "gc_runs" && args.write === true) ? "write" : "read",
|
|
3796
4421
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
3797
4422
|
const args = acceptanceRunnerArgs(params);
|
|
3798
|
-
|
|
4423
|
+
// Only the Core-owned acceptance execution receives panel credentials.
|
|
4424
|
+
// validate/validate_results and every other subprocess stay on the
|
|
4425
|
+
// inherited startup environment and never receive Map values.
|
|
4426
|
+
const credentialEnv = params.action === "run"
|
|
4427
|
+
? testCredentials.forEnvironment(declaredSecretEnvironmentNames(ctx.cwd))
|
|
4428
|
+
: {};
|
|
4429
|
+
const process = await runPython(ctx.cwd, "workflow-orchestrator/scripts/acceptance_test_runner.py", args, credentialEnv);
|
|
4430
|
+
const safeProcess = redactCredentialOutput(process, credentialEnv);
|
|
3799
4431
|
const payload = parseJsonResult(process);
|
|
3800
4432
|
const failed = payload && typeof payload === "object" && (payload as { ok?: unknown }).ok === false;
|
|
3801
|
-
return renderResult(failed ? { ...
|
|
4433
|
+
return renderResult(failed ? { ...safeProcess, ok: false, exitCode: safeProcess.exitCode || 1 } : safeProcess, {
|
|
3802
4434
|
action: params.action,
|
|
3803
4435
|
feature: params.feature,
|
|
3804
4436
|
});
|
|
@@ -4164,51 +4796,25 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
4164
4796
|
};
|
|
4165
4797
|
}
|
|
4166
4798
|
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
|
-
|
|
4799
|
+
if (input.action === "inspect") {
|
|
4800
|
+
const inspected = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/execution_contract.rb", ["inspect", "--feature", input.feature, "--step", input.step, "--json"]);
|
|
4801
|
+
const payload = parseJsonResult(inspected);
|
|
4802
|
+
const raw = payload?.contract;
|
|
4803
|
+
if (!controlPlaneSucceeded(inspected) || !raw || typeof raw !== "object") return renderResult(inspected, { action: input.action });
|
|
4804
|
+
const contract = contractFromRecord(raw as Record<string, unknown>);
|
|
4805
|
+
if (!contract) return renderResult({ ok: false, exitCode: 1, stdout: "", stderr: "Core returned an invalid execution contract." });
|
|
4806
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(contract, null, 2) }], details: { contract } };
|
|
4807
|
+
}
|
|
4174
4808
|
if (!contractActivationPhaseAllowed(controlState.phase, activeWorkflowTurn)) {
|
|
4175
4809
|
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
4810
|
}
|
|
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
|
-
}
|
|
4811
|
+
const activated = await activateExecutionContract(ctx, input.feature, input.step);
|
|
4812
|
+
if (!activated.ok) return renderResult({ ok: false, exitCode: 1, stdout: "", stderr: activated.reason });
|
|
4813
|
+
const destination = activated.contract.contractPath ?? ".devflow/planning/contracts/";
|
|
4814
|
+
return {
|
|
4815
|
+
content: [{ type: "text" as const, text: `Execution contract activated: ${destination}` }],
|
|
4816
|
+
details: { contract: activated.contract, destination },
|
|
4817
|
+
};
|
|
4212
4818
|
},
|
|
4213
4819
|
});
|
|
4214
4820
|
|
|
@@ -4256,7 +4862,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
4256
4862
|
parameters: z.object({
|
|
4257
4863
|
feature: z.string().min(1),
|
|
4258
4864
|
phase: z.string().min(1).optional(),
|
|
4259
|
-
flow: z.enum(["feature-development", "acceptance-remediation"]).default("feature-development"),
|
|
4865
|
+
flow: z.enum(["feature-development", "acceptance-remediation", "regression-acceptance"]).default("feature-development"),
|
|
4260
4866
|
force: z.boolean().default(false),
|
|
4261
4867
|
}),
|
|
4262
4868
|
approval: "write",
|
|
@@ -4793,7 +5399,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
4793
5399
|
ctx.ui.notify("Devflow 已恢复待完成的受控会话交接;当前回合结束后将继续切换。", "info");
|
|
4794
5400
|
return;
|
|
4795
5401
|
}
|
|
4796
|
-
await resumeInspectedWorkflow(ctx, inspected.snapshot);
|
|
5402
|
+
await resumeInspectedWorkflow(ctx, inspected.snapshot, { doctorAlreadyPassed: true });
|
|
4797
5403
|
return;
|
|
4798
5404
|
}
|
|
4799
5405
|
if (trimmed.startsWith("start ")) {
|