@guanlin4924/devflow-pi 0.1.20 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanlin4924/devflow-pi",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -512,12 +512,21 @@ function assertRuntimeProjectBoundary(cwd: string): void {
512
512
  if (!canonical.startsWith(`${resolvedCwd}${path.sep}`)) {
513
513
  throw new Error(`Devflow control path escapes project boundary: ${canonical}`);
514
514
  }
515
+ if (lstatSync(target).isDirectory()) assertNoSymlinkDescendantsSync(target);
515
516
  } catch (error) {
516
517
  if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
517
518
  }
518
519
  }
519
520
  }
520
521
 
522
+ function assertNoSymlinkDescendantsSync(target: string): void {
523
+ for (const entry of readdirSync(target, { withFileTypes: true })) {
524
+ const child = path.join(target, entry.name);
525
+ if (entry.isSymbolicLink()) throw new Error(`Refusing symlinked Devflow control path: ${child}`);
526
+ if (entry.isDirectory()) assertNoSymlinkDescendantsSync(child);
527
+ }
528
+ }
529
+
521
530
  function safeControlPath(cwd: string, raw: string | undefined, kind: "state" | "workflow"): string | undefined {
522
531
  if (raw === undefined) return undefined;
523
532
  if (path.isAbsolute(raw)) throw new Error(`${kind} path must be project-relative`);
@@ -2271,7 +2280,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2271
2280
  pi.registerTool({
2272
2281
  name: "devflow_skill_script",
2273
2282
  label: "Devflow Skill Script",
2274
- description: "Run an allowlisted deterministic script bundled with a Devflow skill without relying on a .skills filesystem path.",
2283
+ 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.",
2275
2284
  parameters: z.object({
2276
2285
  script: z.enum([
2277
2286
  "project-context-validate",
@@ -72,6 +72,18 @@ This skill combines three patterns:
72
72
 
73
73
  Load bundled resources only when needed:
74
74
 
75
+ **Internal resource URL rule (mandatory):** `skill://` URLs identify one exact
76
+ resource. Never pass a `skill://` URL to Glob or Grep and never append `*`,
77
+ `**`, or other wildcard syntax to it. First choose a concrete template name
78
+ from the inventory below, then use Read with the complete URL. A generated
79
+ project file such as `.context/spec/frontend/index.md` is an output and is not
80
+ the same thing as a source template under `assets/`.
81
+
82
+ Available depth templates:
83
+
84
+ - frontend: `api-client`, `case-editor`, `components`, `directory-structure`, `forms`, `routing`, `state`, `testing`
85
+ - backend: `directory-structure`, `engine`, `errors`, `locator`, `model`, `repository`, `router`, `scheduler`, `schema`, `service`, `testing`
86
+
75
87
  - Read `assets/project-initialization-template.md` before writing `PROJECT.md`.
76
88
  - Read `assets/codebase-navigation-template.md` before writing each codebase navigation file.
77
89
  - Read `assets/navigation-index-template.md` before writing `.planning/navigation/INDEX.md`.
@@ -806,6 +818,11 @@ Check:
806
818
  - `STATE.md` includes completion statuses and next action for each codebase.
807
819
  - Generated files include an initialization quality summary.
808
820
 
821
+ Run **only after every manifest-declared output has been written and all four
822
+ current-run status markers have been added**. This is a final gate, not a
823
+ planning or progress check. Do not call it while the preceding steps are still
824
+ describing or planning file updates.
825
+
809
826
  Run:
810
827
 
811
828
  ```text
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);