@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.
Files changed (36) hide show
  1. package/README.md +19 -0
  2. package/package.json +3 -3
  3. package/plugins/devflow-core/extensions/devflow-core.ts +152 -3
  4. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +394 -0
  5. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_suite.rb +86 -0
  6. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +192 -1
  7. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/api_test_matrix.py +829 -0
  8. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +7 -5
  9. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_state.rb +11 -1
  10. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +5 -1
  11. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +39 -3
  12. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph.rb +309 -23
  13. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/tdd_evidence.py +311 -0
  14. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/test_command_policy.py +53 -0
  15. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/test_task_binding_check.py +254 -0
  16. package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +109 -4
  17. package/plugins/devflow-core/skills/workflow-orchestrator/templates/uat-remediation.yaml +21 -1
  18. package/plugins/devflow-core/skills/workflow-project-context/SKILL.md +17 -0
  19. package/plugins/devflow-design/skills/workflow-api-contract/SKILL.md +38 -0
  20. package/plugins/devflow-implementation/agents/implementation-worker.md +3 -0
  21. package/plugins/devflow-implementation/skills/workflow-change-plan/SKILL.md +7 -0
  22. package/plugins/devflow-implementation/skills/workflow-change-plan/references/output-template.md +2 -0
  23. package/plugins/devflow-implementation/skills/workflow-execute-phase/SKILL.md +3 -3
  24. package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +63 -0
  25. package/plugins/devflow-quality/skills/workflow-acceptance-test-design/agents/openai.yaml +4 -0
  26. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/SKILL.md +7 -6
  27. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/intermediate-format.md +3 -0
  28. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/compile_dsl.py +49 -3
  29. package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/SKILL.md +48 -0
  30. package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/agents/openai.yaml +4 -0
  31. package/plugins/devflow-quality/skills/workflow-verify-work/SKILL.md +9 -3
  32. package/src/constants.ts +1 -1
  33. package/src/doctor.ts +6 -2
  34. package/src/project-context.ts +11 -1
  35. package/src/runtime.ts +7 -0
  36. package/src/usage-stats.ts +3 -3
@@ -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
- cases = [compile_case(c) for c in payload.get("cases", [])]
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 结论不能替代它。
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Run Frozen Acceptance Tests"
3
+ short_description: "执行冻结 TC 验收测试"
4
+ default_prompt: "基于冻结的 TC-* 绑定并执行验收测试;源码只能用于运行适配,不能改变测试意图。"
@@ -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. **Compatibility gate**
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
- 6. **Baseline decision gate**
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
- 7. **Lessons extraction gate**
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
@@ -1,5 +1,5 @@
1
1
  export const PRODUCT_NAME = "Devflow Pi";
2
- export const PRODUCT_VERSION = "0.1.0";
2
+ export const PRODUCT_VERSION = "0.1.21";
3
3
  export const PROJECT_SCHEMA_VERSION = 1;
4
4
  export const OMP_RUNTIME_VERSION = "17.2.7";
5
5
 
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
- const parsed = JSON.parse(await readFile(sdkManifest, "utf8")) as { version?: string };
52
- installedVersion = parsed.version || "unknown";
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());
@@ -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
  }
@@ -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
- const sessionTotals = (perSession[session] ??= emptyUsage());
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);