@guanlin4924/devflow-pi 0.1.62 → 0.1.64

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 CHANGED
@@ -75,34 +75,7 @@ skills:
75
75
  # 其余四个插件目录同理
76
76
  ```
77
77
 
78
- ## 私有源码兼容模式
79
-
80
- `install.sh --apply` 只用于已经获得内部源码访问权限的开发环境。本文档不提供私有仓库地址;在内部工作副本中执行:
81
-
82
- ```bash
83
- bash .skills/install.sh --apply
84
- ```
85
-
86
- 它会执行 postflight;已有但不兼容的 `.omp/config.yml` 会使安装失败,不会以警告方式假装安装完整。
87
-
88
- 安装器只生成以下 OMP 原生资源:
89
-
90
- ```text
91
- .omp/config.yml
92
- .omp/AGENTS.md
93
- .omp/extensions/devflow-*.ts
94
- .omp/agents/*.md
95
- .omp/hook-config.json
96
- .agent-gates/index-first.json
97
- ```
98
-
99
- 已有且兼容的 `.omp/config.yml` 会保留。源码兼容模式下,已有配置缺少 Devflow skill 来源、model overrides 或工具策略时,先审查差异,再显式运行:
100
-
101
- ```bash
102
- bash .skills/install.sh --apply --force-config
103
- ```
104
-
105
- 启动 OMP 后:
78
+ 启动已发布的 Devflow Pi 后:
106
79
 
107
80
  ```text
108
81
  /devflow doctor
@@ -174,11 +147,3 @@ npm install -g @guanlin4924/devflow-pi@latest
174
147
  - 索引是加速器,不是真相源;过期时回退到 LSP、搜索、源码和测试。
175
148
  - 人工 checkpoint 不得在 headless 模式静默通过。
176
149
  - semantic correctness 不能靠 prompt 保证,必须由契约、确定性校验、独立审查和可复现测试共同约束。
177
-
178
- 源码兼容模式刷新:
179
-
180
- ```bash
181
- bash .skills/install.sh --refresh
182
- ```
183
-
184
- 刷新完成后重启 OMP 并再次运行 `/devflow doctor`。如果模板新增了强制配置而项目配置被保留,安装器会拒绝完成;审查后使用 `--force-config` 更新。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanlin4924/devflow-pi",
3
- "version": "0.1.62",
3
+ "version": "0.1.64",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -5,10 +5,17 @@ import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
7
7
  import { Box, Text } from "@oh-my-pi/pi-tui";
8
+ import { Database } from "bun:sqlite";
8
9
  import {
9
10
  TASK_SUBAGENT_LIFECYCLE_CHANNEL,
10
11
  type SubagentLifecyclePayload,
11
12
  } from "@oh-my-pi/pi-coding-agent/task";
13
+ import { InitializationStore, FACT_KINDS } from "../../../src/initialization-store.ts";
14
+ import { loadProjectContext } from "../../../src/project-context.ts";
15
+ import { runProjectInitialization } from "../../../src/project-initializer.ts";
16
+ import { projectControlDatabasePath } from "../../../src/paths.ts";
17
+ import { generateProjectDocuments } from "../../../src/project-documents.ts";
18
+ import { exportInitializationSnapshot, importLegacyInitialization, switchInitializationAuthority } from "../../../src/project-migration.ts";
12
19
 
13
20
 
14
21
  export type DevflowIntent = "idle" | "explain-state" | "project-init" | "requirement-draft" | "workflow-execute";
@@ -175,6 +182,7 @@ const READ_ONLY_TOOLS = new Set([
175
182
  "devflow_acceptance_tests",
176
183
  "devflow_usage_stats",
177
184
  "usage_stats",
185
+ "devflow_project_context",
178
186
  ]);
179
187
  const REQUIREMENT_DRAFT_TOOLS = new Set([...READ_ONLY_TOOLS, "ask"]);
180
188
  // A Todo update changes only the client-visible task list, never the project
@@ -299,7 +307,43 @@ function documentedCodeIndexStatus(markdown: string): string | undefined {
299
307
  * evidence, not authority by itself: every documented path must still exist
300
308
  * beneath the current project root before it can be offered back to the user.
301
309
  */
302
- export function discoverExistingProjectInitialization(cwd: string): ExistingProjectInitialization | null {
310
+ export function discoverExistingProjectInitialization(cwd: string, options: { allowLegacyImport?: boolean } = {}): ExistingProjectInitialization | null {
311
+ const projectManifest = path.join(cwd, ".devflow", "project.json");
312
+ if (existsSync(projectManifest)) {
313
+ try {
314
+ const manifest = JSON.parse(readFileSync(projectManifest, "utf8")) as { projectId?: string };
315
+ if (manifest.projectId) {
316
+ const databasePath = projectControlDatabasePath(manifest.projectId);
317
+ if (existsSync(databasePath)) {
318
+ const database = new Database(databasePath, { readonly: true });
319
+ try {
320
+ const current = database.query(`SELECT c.config_json AS config, s.completeness
321
+ FROM workspaces w
322
+ JOIN authority_state a ON a.project_id=w.project_id AND a.authority='database'
323
+ JOIN current_context p ON p.workspace_id=w.id
324
+ JOIN scan_configs c ON c.id=p.config_id
325
+ JOIN context_snapshots s ON s.id=p.snapshot_id
326
+ WHERE w.project_id=? AND w.location=? ORDER BY p.revision DESC LIMIT 1`
327
+ ).get(manifest.projectId, realpathSync(cwd)) as { config: string; completeness: string } | null;
328
+ if (current) {
329
+ const config = JSON.parse(current.config) as { repositories?: Array<{ relativePath?: string; role?: string }> };
330
+ const repositories = config.repositories ?? [];
331
+ const existing = (repository: { relativePath?: string }) => repository.relativePath && documentedProjectPath(repository.relativePath, cwd);
332
+ return {
333
+ frontendPaths: repositories.filter(item => item.role === "frontend").map(existing).filter((item): item is string => Boolean(item)),
334
+ backendPaths: repositories.filter(item => item.role === "backend").map(existing).filter((item): item is string => Boolean(item)),
335
+ codeIndexStatus: current.completeness,
336
+ sourcePath: databasePath,
337
+ };
338
+ }
339
+ } finally { database.close(); }
340
+ }
341
+ }
342
+ } catch {
343
+ // Legacy Markdown below remains import evidence when the database is not usable.
344
+ }
345
+ }
346
+ if (!options.allowLegacyImport) return null;
303
347
  const sources = [".planning/PROJECT.md", ".planning/navigation/INDEX.md"];
304
348
  const records: Array<ExistingProjectInitialization> = [];
305
349
  for (const sourcePath of sources) {
@@ -1195,6 +1239,106 @@ export function stepScopedMutationBlockReason(
1195
1239
 
1196
1240
  export default function devflowCore(pi: ExtensionAPI) {
1197
1241
  const { z } = pi.zod;
1242
+ pi.registerTool({
1243
+ name: "devflow_project_context",
1244
+ label: "Devflow Project Context",
1245
+ description: "Build or query the database-backed, versioned project initialization context. Scan is local and static; it never executes discovered commands.",
1246
+ parameters: z.object({
1247
+ action: z.enum(["scan", "query", "config", "workspaces", "rebind-workspace", "diff", "run-status", "cancel", "recover", "generate-docs", "import-legacy", "export", "authority", "switch-authority", "backup", "restore"]),
1248
+ runId: z.string().optional(),
1249
+ snapshotId: z.string().optional(),
1250
+ fromSnapshotId: z.string().optional(),
1251
+ toSnapshotId: z.string().optional(),
1252
+ kind: z.enum(FACT_KINDS).optional(),
1253
+ repository: z.string().optional(),
1254
+ after: z.string().optional(),
1255
+ limit: z.number().int().min(1).max(500).default(100),
1256
+ requestId: z.string().optional(),
1257
+ mode: z.enum(["full", "fast"]).optional(),
1258
+ repositories: z.array(z.object({
1259
+ name: z.string().min(1),
1260
+ path: z.string().min(1),
1261
+ role: z.enum(["frontend", "backend", "shared", "other"]),
1262
+ })).optional(),
1263
+ requiredKinds: z.array(z.enum(FACT_KINDS)).optional(),
1264
+ documentMode: z.enum(["template", "none"]).optional(),
1265
+ sources: z.array(z.string()).optional(),
1266
+ targetPath: z.string().optional(),
1267
+ authority: z.enum(["legacy", "shadow", "database"]).optional(),
1268
+ expectedRevision: z.number().int().min(0).optional(),
1269
+ compatibilityExport: z.string().optional(),
1270
+ workspaceId: z.string().optional(),
1271
+ }),
1272
+ approval: args => (["query", "config", "workspaces", "diff", "run-status", "authority"].includes(args.action) ? "read" : "exec"),
1273
+ async execute(_id, params, _signal, _onUpdate, ctx) {
1274
+ try {
1275
+ const project = await loadProjectContext(ctx.cwd);
1276
+ let value: unknown;
1277
+ if (params.action === "scan") {
1278
+ const config = params.repositories?.length ? {
1279
+ mode: params.mode ?? "full" as const,
1280
+ repositories: params.repositories.map(item => ({ name: item.name, relativePath: item.path, role: item.role })),
1281
+ requiredKinds: params.requiredKinds,
1282
+ adapterVersions: { static: "1" },
1283
+ } : undefined;
1284
+ value = await runProjectInitialization(project, { requestId: params.requestId, config, documentMode: params.documentMode ?? "template" });
1285
+ } else if (params.action === "generate-docs") {
1286
+ if (!params.snapshotId) throw new Error("snapshotId is required for generate-docs");
1287
+ value = await generateProjectDocuments(project, params.snapshotId, { mode: "template" });
1288
+ } else if (params.action === "import-legacy") {
1289
+ value = await importLegacyInitialization(project, params.sources ?? [], params.requestId);
1290
+ } else if (params.action === "export") {
1291
+ if (!params.snapshotId || !params.targetPath) throw new Error("snapshotId and targetPath are required for export");
1292
+ value = await exportInitializationSnapshot(project, params.snapshotId, params.targetPath);
1293
+ } else if (params.action === "switch-authority") {
1294
+ if (!params.authority || params.expectedRevision === undefined) throw new Error("authority and expectedRevision are required for switch-authority");
1295
+ value = await switchInitializationAuthority(project, params.authority, params.expectedRevision, { compatibilityExport: params.compatibilityExport });
1296
+ } else if (params.action === "restore") {
1297
+ if (!params.targetPath) throw new Error("targetPath is required for restore");
1298
+ await InitializationStore.restore(project, path.resolve(project.root, params.targetPath)); value = { restored: true, database: project.controlDatabasePath };
1299
+ } else {
1300
+ const store = await InitializationStore.open(project);
1301
+ try {
1302
+ if (params.action === "run-status") {
1303
+ if (!params.runId) throw new Error("runId is required for run-status");
1304
+ value = store.getRun(params.runId);
1305
+ } else if (params.action === "config") {
1306
+ value = store.getInitializationConfiguration(params.workspaceId ?? project.workspaceId);
1307
+ } else if (params.action === "workspaces") {
1308
+ value = store.listWorkspaces();
1309
+ } else if (params.action === "rebind-workspace") {
1310
+ if (!params.workspaceId) throw new Error("workspaceId is required for rebind-workspace");
1311
+ value = await store.registerWorkspace({ rebindWorkspaceId: params.workspaceId });
1312
+ } else if (params.action === "cancel") {
1313
+ if (!params.runId) throw new Error("runId is required for cancel");
1314
+ value = { runId: params.runId, cancelled: store.cancelRun(params.runId) };
1315
+ } else if (params.action === "recover") {
1316
+ if (!params.runId) throw new Error("runId is required for recover");
1317
+ value = { runId: params.runId, ...store.recoverRun(params.runId) };
1318
+ } else if (params.action === "authority") {
1319
+ value = store.getAuthority();
1320
+ } else if (params.action === "backup") {
1321
+ if (!params.targetPath) throw new Error("targetPath is required for backup");
1322
+ const target = path.resolve(project.root, params.targetPath);
1323
+ if (target !== project.root && !target.startsWith(`${project.root}${path.sep}`)) throw new Error("Backup path escapes project root");
1324
+ await store.backup(target); value = { backup: target };
1325
+ } else if (params.action === "diff") {
1326
+ if (!params.fromSnapshotId || !params.toSnapshotId) throw new Error("fromSnapshotId and toSnapshotId are required for diff");
1327
+ value = store.compareSnapshots(params.fromSnapshotId, params.toSnapshotId);
1328
+ } else if (params.snapshotId) {
1329
+ value = store.queryContext({ snapshotId: params.snapshotId, kind: params.kind, repository: params.repository, after: params.after, limit: params.limit });
1330
+ } else {
1331
+ value = store.queryCurrent(project.workspaceId, { kind: params.kind, repository: params.repository, after: params.after, limit: params.limit });
1332
+ }
1333
+ } finally { store.close(); }
1334
+ }
1335
+ return { content: [{ type: "text" as const, text: JSON.stringify(value, null, 2) }], details: value };
1336
+ } catch (error) {
1337
+ const message = error instanceof Error ? error.message : String(error);
1338
+ return { content: [{ type: "text" as const, text: message }], details: { error: message }, isError: true };
1339
+ }
1340
+ },
1341
+ });
1198
1342
  // CLI --resume means "continue this chat session" at the OMP layer. The
1199
1343
  // launcher also supplies this one-shot, process-owned signal so Devflow can
1200
1344
  // safely resume an authoritative incomplete workflow step. It is consumed
@@ -1635,7 +1779,7 @@ export default function devflowCore(pi: ExtensionAPI) {
1635
1779
  if (isEmbeddedRuntime()) {
1636
1780
  add("config", "pass", "embedded Devflow Pi owns the managed runtime profile");
1637
1781
  } else if (!existsSync(configPath)) {
1638
- add("config", "fail", "missing .omp/config.yml; run bash .skills/install.sh --apply");
1782
+ add("config", "fail", "missing .omp/config.yml; configure the OMP project profile before starting");
1639
1783
  } else {
1640
1784
  const config = readFileSync(configPath, "utf8");
1641
1785
  const requiredConfigPatterns: Array<[string, RegExp]> = [
@@ -3716,10 +3860,11 @@ export default function devflowCore(pi: ExtensionAPI) {
3716
3860
  pi.registerTool({
3717
3861
  name: "devflow_skill_script",
3718
3862
  label: "Devflow Skill Script",
3719
- description: "Run an allowlisted deterministic script bundled with a Devflow skill. project-context-validate is a final initialization gate: call it only after all manifest outputs and current-run markers are written.",
3863
+ description: "Run an allowlisted deterministic script bundled with a Devflow skill. Database validation is the authority for new initialization runs; the manifest validator is legacy compatibility only.",
3720
3864
  parameters: z.object({
3721
3865
  script: z.enum([
3722
3866
  "project-context-validate",
3867
+ "project-context-db-validate",
3723
3868
  "orchestrator-manage-lessons",
3724
3869
  "orchestrator-update-triage",
3725
3870
  "wireframe-validate",
@@ -3731,6 +3876,7 @@ export default function devflowCore(pi: ExtensionAPI) {
3731
3876
  const kitRoot = resolveKitRoot(ctx.cwd);
3732
3877
  const scripts: Record<string, [string, string]> = {
3733
3878
  "project-context-validate": ["python3", "workflow-project-context/scripts/validate_initialization.py"],
3879
+ "project-context-db-validate": ["python3", "workflow-project-context/scripts/validate_initialization_database.py"],
3734
3880
  "orchestrator-manage-lessons": ["ruby", "workflow-orchestrator/scripts/manage_lessons.rb"],
3735
3881
  "orchestrator-update-triage": ["ruby", "workflow-orchestrator/scripts/update_triage_route.rb"],
3736
3882
  "wireframe-validate": ["python3", "workflow-orchestrator/scripts/wireframe_validate.py"],
@@ -4,6 +4,39 @@ from __future__ import annotations
4
4
 
5
5
  from pathlib import Path
6
6
  from typing import Any
7
+ import json
8
+ import os
9
+ import sqlite3
10
+
11
+
12
+ def database_toolchain(root: Path) -> list[str] | None:
13
+ manifest_path = root / ".devflow" / "project.json"
14
+ if not manifest_path.is_file():
15
+ return None
16
+ try:
17
+ project_id = json.loads(manifest_path.read_text(encoding="utf-8"))["projectId"]
18
+ data_home = Path(os.environ.get("DEVFLOW_HOME", Path.home() / ".devflow-pi"))
19
+ database = data_home / "projects" / project_id / "control.db"
20
+ connection = sqlite3.connect(f"file:{database}?mode=ro", uri=True)
21
+ connection.row_factory = sqlite3.Row
22
+ try:
23
+ authority = connection.execute("SELECT authority FROM authority_state WHERE project_id=?", (project_id,)).fetchone()
24
+ if not authority or authority["authority"] != "database":
25
+ return None
26
+ rows = connection.execute(
27
+ """SELECT f.value_json FROM workspaces w
28
+ JOIN current_context c ON c.workspace_id=w.id
29
+ JOIN context_snapshots s ON s.id=c.snapshot_id
30
+ JOIN facts f ON f.run_id=s.run_id AND f.kind='toolchain'
31
+ WHERE w.project_id=? AND w.location=?
32
+ ORDER BY c.revision DESC""",
33
+ (project_id, str(root.resolve())),
34
+ ).fetchall()
35
+ return [str(json.loads(row["value_json"]).get("command", "")) for row in rows]
36
+ finally:
37
+ connection.close()
38
+ except (OSError, KeyError, ValueError, json.JSONDecodeError, sqlite3.Error):
39
+ return None
7
40
 
8
41
 
9
42
  def runner_marker(argv: list[str]) -> str | None:
@@ -43,11 +76,15 @@ def validate_test_command(root: Path, argv: Any) -> str | None:
43
76
  marker = runner_marker(argv)
44
77
  if marker is None:
45
78
  return "test command runner is not in the Core allowlist"
46
- toolchain = root / ".planning" / "toolchain.md"
47
- if not toolchain.is_file():
48
- return "initialized .planning/toolchain.md is required before Core can execute a test command"
49
- content = toolchain.read_text(encoding="utf-8").lower()
79
+ commands = database_toolchain(root)
80
+ if commands is None:
81
+ toolchain = root / ".planning" / "toolchain.md"
82
+ if not toolchain.is_file():
83
+ return "initialized toolchain context (database or .planning/toolchain.md compatibility projection) is required before Core can execute a test command"
84
+ content = toolchain.read_text(encoding="utf-8").lower()
85
+ else:
86
+ content = "\n".join(commands).lower()
50
87
  aliases = {"pytest": ("pytest",), "unittest": ("unittest",), "bun test": ("bun test",), "npm": ("npm",), "pnpm": ("pnpm",), "yarn": ("yarn",), "go test": ("go test",), "newman": ("newman",), "playwright": ("playwright",), "vitest": ("vitest",)}
51
88
  if not any(alias in content for alias in aliases[marker]):
52
- return f"test runner '{marker}' was not discovered in .planning/toolchain.md"
89
+ return f"test runner '{marker}' was not discovered in the initialization toolchain snapshot"
53
90
  return None