@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 +1 -1
- package/plugins/devflow-core/extensions/devflow-core.ts +10 -1
- package/plugins/devflow-core/skills/workflow-project-context/SKILL.md +17 -0
- 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/package.json
CHANGED
|
@@ -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
|
|
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
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);
|