@guanlin4924/devflow-pi 0.1.19 → 0.1.21
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 +19 -0
- package/package.json +3 -3
- package/plugins/devflow-core/extensions/devflow-core.ts +152 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +394 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_suite.rb +86 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +192 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/api_test_matrix.py +829 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +7 -5
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_state.rb +11 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +5 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +39 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph.rb +309 -23
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/tdd_evidence.py +311 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/test_command_policy.py +53 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/test_task_binding_check.py +254 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +109 -4
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/uat-remediation.yaml +21 -1
- package/plugins/devflow-core/skills/workflow-project-context/SKILL.md +17 -0
- package/plugins/devflow-design/skills/workflow-api-contract/SKILL.md +38 -0
- package/plugins/devflow-implementation/agents/implementation-worker.md +3 -0
- package/plugins/devflow-implementation/skills/workflow-change-plan/SKILL.md +7 -0
- package/plugins/devflow-implementation/skills/workflow-change-plan/references/output-template.md +2 -0
- package/plugins/devflow-implementation/skills/workflow-execute-phase/SKILL.md +3 -3
- package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +63 -0
- package/plugins/devflow-quality/skills/workflow-acceptance-test-design/agents/openai.yaml +4 -0
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/SKILL.md +7 -6
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/intermediate-format.md +3 -0
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/compile_dsl.py +49 -3
- package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/SKILL.md +48 -0
- package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/agents/openai.yaml +4 -0
- package/plugins/devflow-quality/skills/workflow-verify-work/SKILL.md +9 -3
- package/src/constants.ts +1 -1
- package/src/doctor.ts +6 -2
- package/src/project-context.ts +11 -1
- package/src/runtime.ts +7 -0
- package/src/usage-stats.ts +3 -3
package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/compile_dsl.py
CHANGED
|
@@ -531,14 +531,25 @@ def compile_case(case_desc: dict[str, Any]) -> dict[str, Any]:
|
|
|
531
531
|
}
|
|
532
532
|
|
|
533
533
|
|
|
534
|
-
def compile_all(payload: dict[str, Any]) -> dict[str, Any]:
|
|
534
|
+
def compile_all(payload: dict[str, Any], frozen_mainflow_tc_ids: set[str] | None = None) -> dict[str, Any]:
|
|
535
535
|
"""Compile all cases in the payload."""
|
|
536
536
|
metadata = dict(payload.get("metadata") or {})
|
|
537
|
-
|
|
537
|
+
descriptions = list(payload.get("cases", []))
|
|
538
|
+
tc_ids: list[str] = []
|
|
539
|
+
if frozen_mainflow_tc_ids is not None:
|
|
540
|
+
for index, description in enumerate(descriptions):
|
|
541
|
+
tc_id = description.get("tc_id") if isinstance(description, dict) else None
|
|
542
|
+
if not isinstance(tc_id, str) or tc_id not in frozen_mainflow_tc_ids:
|
|
543
|
+
raise ValueError(f"cases[{index}].tc_id must reference a frozen api/page TC that is positive")
|
|
544
|
+
tc_ids.append(tc_id)
|
|
545
|
+
if len(tc_ids) != len(set(tc_ids)):
|
|
546
|
+
raise ValueError("each frozen positive api/page TC may generate at most one mainflow case")
|
|
547
|
+
cases = [compile_case(c) for c in descriptions]
|
|
538
548
|
|
|
539
549
|
return {
|
|
540
550
|
"metadata": {
|
|
541
551
|
**metadata,
|
|
552
|
+
**({"frozen_tc_ids": tc_ids} if frozen_mainflow_tc_ids is not None else {}),
|
|
542
553
|
"compiled_at": datetime.now(timezone.utc).isoformat(),
|
|
543
554
|
"compiled_by": "compile_dsl.py (hard-constraint layer)",
|
|
544
555
|
},
|
|
@@ -546,6 +557,41 @@ def compile_all(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
546
557
|
}
|
|
547
558
|
|
|
548
559
|
|
|
560
|
+
def frozen_mainflow_tc_ids_from_cwd(cwd: Path) -> set[str] | None:
|
|
561
|
+
"""Return the frozen API/page suite for new workflows, else preserve legacy compilation."""
|
|
562
|
+
state_path = cwd / ".planning" / "STATE.md"
|
|
563
|
+
if not state_path.is_file():
|
|
564
|
+
return None
|
|
565
|
+
match = re.search(r"^current_feature:\s*['\"]?([^'\"\n]+)", state_path.read_text(encoding="utf-8"), re.MULTILINE)
|
|
566
|
+
if not match:
|
|
567
|
+
return None
|
|
568
|
+
suite_path = cwd / ".planning" / "requirements" / match.group(1).strip() / "tests.json"
|
|
569
|
+
if not suite_path.is_file():
|
|
570
|
+
return None
|
|
571
|
+
suite = json.loads(suite_path.read_text(encoding="utf-8"))
|
|
572
|
+
if suite.get("frozen") is not True:
|
|
573
|
+
raise ValueError("current TC suite exists but is not frozen and fresh")
|
|
574
|
+
eligible = {
|
|
575
|
+
node.get("id")
|
|
576
|
+
for node in suite.get("nodes", [])
|
|
577
|
+
if isinstance(node, dict)
|
|
578
|
+
and node.get("kind") == "test_case"
|
|
579
|
+
and node.get("status") == "confirmed"
|
|
580
|
+
and isinstance(node.get("data"), dict)
|
|
581
|
+
and node["data"].get("test_type") in {"api", "page"}
|
|
582
|
+
# A generated API TC with an error-code oracle is a negative/exception
|
|
583
|
+
# test. It belongs to run-acceptance-tests, never to the positive
|
|
584
|
+
# mainflow importer even though its broad test_type is "api".
|
|
585
|
+
and not (
|
|
586
|
+
isinstance(node["data"].get("generated_case_key"), str)
|
|
587
|
+
and isinstance(node["data"].get("execution"), dict)
|
|
588
|
+
and isinstance(node["data"]["execution"].get("expect"), dict)
|
|
589
|
+
and "error_code" in node["data"]["execution"]["expect"]
|
|
590
|
+
)
|
|
591
|
+
}
|
|
592
|
+
return {value for value in eligible if isinstance(value, str)}
|
|
593
|
+
|
|
594
|
+
|
|
549
595
|
# ---------------------------------------------------------------------------
|
|
550
596
|
# CLI
|
|
551
597
|
# ---------------------------------------------------------------------------
|
|
@@ -568,7 +614,7 @@ def main() -> int:
|
|
|
568
614
|
raw = {"metadata": {}, "cases": raw}
|
|
569
615
|
|
|
570
616
|
try:
|
|
571
|
-
result = compile_all(raw)
|
|
617
|
+
result = compile_all(raw, frozen_mainflow_tc_ids_from_cwd(Path.cwd()))
|
|
572
618
|
except ValueError as exc:
|
|
573
619
|
print(f"COMPILE ERROR: {exc}", file=__import__("sys").stderr)
|
|
574
620
|
return 1
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: workflow-run-acceptance-tests
|
|
3
|
+
description: "Execute the frozen TC acceptance suite after code review. Use the requirement baseline and frozen TC nodes for test intent; use current code only to bind execution adapters."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 冻结验收测试执行
|
|
7
|
+
|
|
8
|
+
此步骤在代码审查后、UAT 前运行。它的目标是执行冻结的 `TC-*`,而不是根据交付总结重新猜测要测什么。
|
|
9
|
+
|
|
10
|
+
## 输入边界
|
|
11
|
+
|
|
12
|
+
测试意图只能读取:
|
|
13
|
+
|
|
14
|
+
- `.planning/requirements/{feature}/tests.json` 中已冻结且未 stale 的 `TC-*`;
|
|
15
|
+
- 图谱中被 TC 引用的已确认 REQ / BR / BC / AC、契约与架构风险;
|
|
16
|
+
- 初始化发现的测试工具链能力。
|
|
17
|
+
|
|
18
|
+
可以读取当前源码和代码索引,但**仅**用来绑定入口、选择器、请求地址、测试数据与命令适配器。
|
|
19
|
+
|
|
20
|
+
禁止读取或使用以下内容来扩大、缩小或补造验收范围:
|
|
21
|
+
|
|
22
|
+
- `.planning/phases/{phase}/SUMMARY.md`;
|
|
23
|
+
- agent 的交付自述、代码 diff 或“已完成”结论;
|
|
24
|
+
- 未冻结的临时需求列表。
|
|
25
|
+
|
|
26
|
+
## 过程
|
|
27
|
+
|
|
28
|
+
1. 用 `devflow_acceptance_tests(feature)` 检查测试集已由 CP-3 冻结。若有 stale 或候选 TC,停止并回到需求/测试设计。
|
|
29
|
+
2. 读取冻结 TC 与其 oracle、前置数据、身份和自动化模式。
|
|
30
|
+
对 `api_refs`、`workload_refs` 和 `risk_refs` 指向的专项用例,保留其契约、性能/迁移或韧性义务;没有适配器或环境时必须写为 blocked,不能用普通主流程替代。
|
|
31
|
+
3. 读取代码审查报告、代码索引和实际源码,仅为每个 TC 绑定运行适配器;不得改变 TC 的断言、范围或预期。
|
|
32
|
+
4. 写入 `.planning/features/{feature}/acceptance-test-bindings.json`:
|
|
33
|
+
- 固定 `test_suite.graph_revision` 与 `fingerprint`;
|
|
34
|
+
- 每个 TC 恰好一条绑定;
|
|
35
|
+
- `mode` 必须与冻结 TC 的 `automation_mode` 一致;
|
|
36
|
+
- 通用自动化用例使用 `{ "kind": "command", "argv": [ ... ] }`,不使用 shell 字符串;
|
|
37
|
+
- Core 生成的 API TC 可直接使用 `{ "kind": "http", "base_url_env": "DEVFLOW_TEST_BASE_URL", "headers_env": { "Authorization": "DEVFLOW_TEST_AUTHORIZATION" } }`。请求、HTTP 状态和异常码断言来自冻结 TC 的 `execution`,绑定只能提供环境地址和鉴权,不得重写测试意图;若场景需要专用夹具,可额外配置 `{ "setup": { "argv": [ ... ] } }`,Core 会先记录并执行该受控命令,再发 HTTP 请求;
|
|
38
|
+
- 人工或环境阻塞用例必须有 owner 与原因/阻塞源。
|
|
39
|
+
5. 调用 `devflow_run_acceptance_tests(action="validate", feature=...)`。不通过时修正绑定,不得绕过。
|
|
40
|
+
6. 调用 `devflow_run_acceptance_tests(action="run", feature=...)`。该 Core 工具记录每个 TC 的命令哈希、退出码和输出摘要。
|
|
41
|
+
7. 失败、未执行或 blocked 的 TC 只能报告为 `blocked`/`failed`,不得在 CP-4 或 UAT 中宣称验收通过。
|
|
42
|
+
|
|
43
|
+
## 输出
|
|
44
|
+
|
|
45
|
+
- `.planning/features/{feature}/acceptance-test-bindings.json`
|
|
46
|
+
- `.planning/test-runs/{feature}/acceptance-results.json`
|
|
47
|
+
|
|
48
|
+
`acceptance-results.json` 是 Core-owned 的执行事实;Markdown 结论不能替代它。
|
|
@@ -72,18 +72,24 @@ Verification must include these gates when project artifacts exist:
|
|
|
72
72
|
- Run or require the commands declared by `.planning/toolchain.md`, `.planning/execution-contract.md`, `.context/workflow/verification.md`, or the change list.
|
|
73
73
|
- Include target tests, full tests, build, codegen/mock/DI checks, migration checks, and integration tests when applicable.
|
|
74
74
|
|
|
75
|
-
5.
|
|
75
|
+
5. **冻结验收测试门禁**
|
|
76
|
+
- 读取 `.planning/requirements/{feature}/tests.json`、`.planning/features/{feature}/acceptance-test-bindings.json` 和 `.planning/test-runs/{feature}/acceptance-results.json`。
|
|
77
|
+
- 调用 `devflow_run_acceptance_tests(action="validate_results", feature=...)`;它验证结果仍引用当前冻结 suite,且每个 `TC-*` 都已通过。
|
|
78
|
+
- 任一 TC 未执行、失败、人工待执行或因环境阻塞时,验证状态只能是 `blocked` 或 `failed`,CP-4 不得宣称验收通过。
|
|
79
|
+
- Markdown、UAT 口述或 agent 自述不能替代该 Core-owned 结果。
|
|
80
|
+
|
|
81
|
+
6. **Compatibility gate**
|
|
76
82
|
- Check interface, database, config, data migration, permission, and consumer compatibility risks.
|
|
77
83
|
- For frontend-visible API/VO/schema changes, record backward compatibility or migration handling.
|
|
78
84
|
- For database/model/config changes, record migration, rollback, and deployment ordering risk.
|
|
79
85
|
- Record accepted residual risks explicitly.
|
|
80
86
|
|
|
81
|
-
|
|
87
|
+
7. **Baseline decision gate**
|
|
82
88
|
- Compare implementation against `.planning/requirements/baseline.md`, especially `反向审视与用户拍板`, `待确认事项`, and `下游输入契约`.
|
|
83
89
|
- Confirm implemented behavior maps to `FP-*`, `BR-*`, `BC-*`, and confirmed `CR-*` decisions.
|
|
84
90
|
- Any implemented scope based on a pending decision is a verification failure unless the user explicitly approved it.
|
|
85
91
|
|
|
86
|
-
|
|
92
|
+
8. **Lessons extraction gate**
|
|
87
93
|
- After verification is complete, review the findings for lessons worth preserving.
|
|
88
94
|
- Candidate triggers for a lesson:
|
|
89
95
|
- A regression was caught early — what pattern prevented it?
|
package/src/constants.ts
CHANGED
package/src/doctor.ts
CHANGED
|
@@ -48,8 +48,12 @@ export async function runHostDoctor(project: ProjectContext): Promise<HostDoctor
|
|
|
48
48
|
const sdkManifest = ompSdkManifestPath();
|
|
49
49
|
let installedVersion = "missing";
|
|
50
50
|
if (await exists(sdkManifest)) {
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(await readFile(sdkManifest, "utf8")) as { version?: unknown };
|
|
53
|
+
installedVersion = typeof parsed.version === "string" && parsed.version ? parsed.version : "unknown";
|
|
54
|
+
} catch {
|
|
55
|
+
installedVersion = "invalid manifest";
|
|
56
|
+
}
|
|
53
57
|
}
|
|
54
58
|
add("omp-sdk", installedVersion === OMP_RUNTIME_VERSION, installedVersion);
|
|
55
59
|
add("agent-dir", true, devflowAgentDir());
|
package/src/project-context.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { access, lstat, mkdir, readFile, realpath, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { access, lstat, mkdir, readFile, realpath, readdir, rename, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
import { PROJECT_SCHEMA_VERSION } from "./constants.ts";
|
|
@@ -26,6 +26,15 @@ const CONTROL_BOUNDARY_PATHS = [
|
|
|
26
26
|
".planning/artifacts",
|
|
27
27
|
];
|
|
28
28
|
|
|
29
|
+
async function assertNoSymlinkDescendants(target: string): Promise<void> {
|
|
30
|
+
const entries = await readdir(target, { withFileTypes: true }).catch(() => []);
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const child = path.join(target, entry.name);
|
|
33
|
+
if (entry.isSymbolicLink()) throw new Error(`Refusing symlinked Devflow control path: ${child}`);
|
|
34
|
+
if (entry.isDirectory()) await assertNoSymlinkDescendants(child);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
async function exists(target: string): Promise<boolean> {
|
|
30
39
|
try {
|
|
31
40
|
await access(target);
|
|
@@ -112,6 +121,7 @@ export async function assertPlanningBoundary(root: string): Promise<string> {
|
|
|
112
121
|
if (!canonicalTarget.startsWith(`${canonicalRoot}${path.sep}`)) {
|
|
113
122
|
throw new Error(`Devflow control path escapes project boundary: ${canonicalTarget}`);
|
|
114
123
|
}
|
|
124
|
+
if (targetStat.isDirectory()) await assertNoSymlinkDescendants(target);
|
|
115
125
|
}
|
|
116
126
|
}
|
|
117
127
|
return planningRoot;
|
package/src/runtime.ts
CHANGED
|
@@ -16,6 +16,12 @@ export interface RuntimeOptions {
|
|
|
16
16
|
approvalMode?: "write" | "yolo";
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
const TOOL_CALL_SAFETY_PROMPT = `Devflow tool-call rules:
|
|
20
|
+
- Read always requires a non-empty string path; never call Read with {} or omit path.
|
|
21
|
+
- skill:// URLs identify one exact packaged resource. Never use Glob or Grep with a skill:// URL or wildcard; choose an exact resource and use Read.
|
|
22
|
+
- A generated project path such as .context/spec/frontend/index.md is not automatically a packaged skill template path.
|
|
23
|
+
- The project-context validator is a final gate. Do not run project-context-validate while files are still being planned or written.`;
|
|
24
|
+
|
|
19
25
|
const MANAGED_SETTINGS: Record<string, unknown> = {
|
|
20
26
|
modelRoleStorage: "user",
|
|
21
27
|
disabledProviders: ["claude", "codex", "cursor", "gemini", "github", "opencode"],
|
|
@@ -135,6 +141,7 @@ export async function createEmbeddedRuntime(project: ProjectContext, options: Ru
|
|
|
135
141
|
hasUI: options.hasUI ?? true,
|
|
136
142
|
enableMCP: options.enableMCP ?? false,
|
|
137
143
|
enableLsp: options.enableLsp ?? true,
|
|
144
|
+
appendSystemPrompt: TOOL_CALL_SAFETY_PROMPT,
|
|
138
145
|
});
|
|
139
146
|
return { omp, runtime, sessionDir };
|
|
140
147
|
}
|
package/src/usage-stats.ts
CHANGED
|
@@ -153,13 +153,11 @@ export async function aggregateUsageReport(
|
|
|
153
153
|
const perSession: Record<string, UsageTotals> = {};
|
|
154
154
|
const perSkill: Record<string, UsageTotals> = {};
|
|
155
155
|
const files = await jsonlFiles(sessionRoot);
|
|
156
|
-
totals.sessionFiles = files.length;
|
|
157
156
|
|
|
158
157
|
for (const file of files) {
|
|
159
158
|
const session = sessionLabel(sessionRoot, file);
|
|
160
159
|
if (range.session && range.session !== "all" && range.session !== session) continue;
|
|
161
|
-
|
|
162
|
-
sessionTotals.sessionFiles = 1;
|
|
160
|
+
let sessionTotals: UsageTotals | undefined;
|
|
163
161
|
let currentModel = "unknown";
|
|
164
162
|
const content = await readFile(file, "utf8").catch(() => "");
|
|
165
163
|
for (const line of content.split("\n")) {
|
|
@@ -181,6 +179,8 @@ export async function aggregateUsageReport(
|
|
|
181
179
|
if (!usage) continue;
|
|
182
180
|
const model = currentModel;
|
|
183
181
|
if (range.models?.length && !range.models.includes(model)) continue;
|
|
182
|
+
sessionTotals ??= (perSession[session] ??= { ...emptyUsage(), sessionFiles: 1 });
|
|
183
|
+
totals.sessionFiles = Math.max(totals.sessionFiles, Object.keys(perSession).length);
|
|
184
184
|
const modelTotals = (perModel[model] ??= emptyUsage());
|
|
185
185
|
addUsage(totals, usage);
|
|
186
186
|
addUsage(sessionTotals, usage);
|