@guanlin4924/devflow-pi 0.1.17 → 0.1.19

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
@@ -34,6 +34,8 @@ devflow-pi
34
34
  /usage-stats
35
35
  ```
36
36
 
37
+ `/devflow project-init` 会先读取当前项目已有的 `.planning/PROJECT.md` 与 `.planning/navigation/INDEX.md`:仅回填仍存在于项目根目录内的已登记前后端路径,删除或越界的历史路径会被忽略。随后可选择复用已有边界、复用边界并重建索引,或手动覆盖。初始化模式使用选择器提供“只读规划(默认)”和“写入初始化”;选择后者并确认,只会创建 Devflow 的 `.planning/` 与 `.context/` 初始化产物,不会修改业务源码、依赖或项目配置。
38
+
37
39
  用量统计默认只读取当前项目的隔离会话:`/usage-stats`。需要明确查看历史 OMP 会话时,使用 `/usage-stats omp`,或加筛选条件:`/usage-stats omp --from 2026-08-01 --to 2026-08-10`。历史统计保留真实会话路径,不再把不同项目的长会话文件合并成 `main`;统计只使用 assistant message 的原始 usage,不虚构工具调用成本分摊。
38
40
 
39
41
  会话继续使用 `devflow-pi --resume`;也可用 `devflow-pi --resume <session-id>` 打开当前项目的指定 Session。OMP 17.2.7 退出时仍会显示上游硬编码的 `omp --resume` 提示,在专项发行版中应替换为这里的 `devflow-pi --resume`,不会去读取官方 OMP 的 Session 目录。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanlin4924/devflow-pi",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -16,6 +16,13 @@ export type SessionControlState = {
16
16
  label: string;
17
17
  };
18
18
 
19
+ type ProjectInitializationMode = "plan" | "apply";
20
+
21
+ type ProjectInitializationRequest = {
22
+ summary: string;
23
+ mode: ProjectInitializationMode;
24
+ };
25
+
19
26
  type ActiveExecutionContract = {
20
27
  contractId: string;
21
28
  feature: string;
@@ -91,6 +98,118 @@ export function planningState(intent: "project-init" | "requirement-draft"): Ses
91
98
  };
92
99
  }
93
100
 
101
+ export type ExistingProjectInitialization = {
102
+ projectName?: string;
103
+ frontendPaths: string[];
104
+ backendPaths: string[];
105
+ codeIndexStatus?: string;
106
+ sourcePath: string;
107
+ };
108
+
109
+ type DocumentedProjectLocations = {
110
+ frontendPaths: string[];
111
+ backendPaths: string[];
112
+ };
113
+
114
+ function markdownTableCells(line: string): string[] | null {
115
+ if (!line.trim().startsWith("|")) return null;
116
+ const cells = line.trim().split("|").slice(1, -1).map(cell => cell.trim());
117
+ return cells.length > 0 ? cells : null;
118
+ }
119
+
120
+ function documentedProjectPath(raw: string, cwd: string): string | null {
121
+ const cleaned = raw
122
+ .replaceAll("`", "")
123
+ .replaceAll("~", "")
124
+ .replaceAll("*", "")
125
+ .replace(/\s*[((].*$/, "")
126
+ .trim();
127
+ if (!cleaned) return null;
128
+ const absolute = path.resolve(cwd, cleaned);
129
+ const relative = path.relative(cwd, absolute).replaceAll("\\", "/");
130
+ if (!relative || relative === ".." || relative.startsWith("../") || path.isAbsolute(relative)) return null;
131
+ if (!existsSync(absolute)) return null;
132
+ return relative;
133
+ }
134
+
135
+ function documentedLocations(markdown: string, cwd: string): DocumentedProjectLocations {
136
+ const frontendPaths: string[] = [];
137
+ const backendPaths: string[] = [];
138
+ const lines = markdown.split(/\r?\n/);
139
+ for (let index = 0; index < lines.length - 2; index += 1) {
140
+ const header = markdownTableCells(lines[index]);
141
+ const divider = markdownTableCells(lines[index + 1]);
142
+ if (!header || !divider || !divider.every(cell => /^:?-{3,}:?$/.test(cell))) continue;
143
+ const pathColumn = header.findIndex(cell => /^(?:path|路径)$/i.test(cell));
144
+ const typeColumn = header.findIndex(cell => /^(?:type|类型)$/i.test(cell));
145
+ if (pathColumn < 0 || typeColumn < 0) continue;
146
+ for (let rowIndex = index + 2; rowIndex < lines.length; rowIndex += 1) {
147
+ const row = markdownTableCells(lines[rowIndex]);
148
+ if (!row || row.length !== header.length) break;
149
+ const projectPath = documentedProjectPath(row[pathColumn], cwd);
150
+ if (!projectPath) continue;
151
+ const type = row[typeColumn];
152
+ if (/(?:前端|frontend|\bh5\b|小程序|uniapp)/i.test(type)) frontendPaths.push(projectPath);
153
+ if (/(?:后端|backend|服务|\bapi\b)/i.test(type)) backendPaths.push(projectPath);
154
+ }
155
+ }
156
+ return {
157
+ frontendPaths: [...new Set(frontendPaths)],
158
+ backendPaths: [...new Set(backendPaths)],
159
+ };
160
+ }
161
+
162
+ function documentedProjectName(markdown: string): string | undefined {
163
+ const match = markdown.match(/^\s*-\s*\*\*Name\*\*:\s*(.+?)\s*$/mi);
164
+ return match?.[1]?.trim() || undefined;
165
+ }
166
+
167
+ function documentedCodeIndexStatus(markdown: string): string | undefined {
168
+ const comment = markdown.match(/code_index_status:\s*([^;\-\n]+)/i);
169
+ if (comment?.[1]?.trim()) return comment[1].trim();
170
+ const field = markdown.match(/^\s*-\s*\*\*Code Index Status\*\*:\s*([^\n]+)/mi);
171
+ return field?.[1]?.trim() || undefined;
172
+ }
173
+
174
+ /**
175
+ * Reuses only existing, project-local initialization records. Markdown is
176
+ * evidence, not authority by itself: every documented path must still exist
177
+ * beneath the current project root before it can be offered back to the user.
178
+ */
179
+ export function discoverExistingProjectInitialization(cwd: string): ExistingProjectInitialization | null {
180
+ const sources = [".planning/PROJECT.md", ".planning/navigation/INDEX.md"];
181
+ const records: Array<ExistingProjectInitialization> = [];
182
+ for (const sourcePath of sources) {
183
+ const absolute = path.join(cwd, sourcePath);
184
+ if (!existsSync(absolute)) continue;
185
+ const markdown = readFileSync(absolute, "utf8");
186
+ const locations = documentedLocations(markdown, cwd);
187
+ if (locations.frontendPaths.length === 0 && locations.backendPaths.length === 0) continue;
188
+ records.push({
189
+ projectName: documentedProjectName(markdown),
190
+ frontendPaths: locations.frontendPaths,
191
+ backendPaths: locations.backendPaths,
192
+ codeIndexStatus: documentedCodeIndexStatus(markdown),
193
+ sourcePath,
194
+ });
195
+ }
196
+ const preferred = records[0];
197
+ if (!preferred) return null;
198
+ const fallback = records.find(record => record !== preferred);
199
+ const useFallbackFrontend = preferred.frontendPaths.length === 0 && Boolean(fallback?.frontendPaths.length);
200
+ const useFallbackBackend = preferred.backendPaths.length === 0 && Boolean(fallback?.backendPaths.length);
201
+ return {
202
+ projectName: preferred.projectName ?? fallback?.projectName,
203
+ frontendPaths: preferred.frontendPaths.length > 0 ? preferred.frontendPaths : (fallback?.frontendPaths ?? []),
204
+ backendPaths: preferred.backendPaths.length > 0 ? preferred.backendPaths : (fallback?.backendPaths ?? []),
205
+ codeIndexStatus: preferred.codeIndexStatus ?? fallback?.codeIndexStatus,
206
+ sourcePath: [
207
+ preferred.sourcePath,
208
+ ...((useFallbackFrontend || useFallbackBackend) && fallback ? [fallback.sourcePath] : []),
209
+ ].join(", "),
210
+ };
211
+ }
212
+
94
213
  export function executingState(label = "当前工作流步骤执行"): SessionControlState {
95
214
  return { intent: "workflow-execute", phase: "executing", label };
96
215
  }
@@ -726,31 +845,117 @@ export default function devflowCore(pi: ExtensionAPI) {
726
845
  }
727
846
 
728
847
  async function collectProjectInitialization(ctx: {
848
+ cwd: string;
729
849
  ui: {
850
+ select(title: string, options: Array<{ label: string; description?: string }>): Promise<string | undefined>;
730
851
  input(title: string, placeholder?: string): Promise<string | undefined>;
731
852
  confirm(title: string, message: string): Promise<boolean>;
732
853
  notify(message: string, type?: "info" | "warning" | "error"): void;
733
854
  };
734
- }): Promise<string | null> {
735
- const projectName = await ctx.ui.input("Devflow 项目初始化", "项目名称(留空则使用当前目录)");
736
- const frontendPath = await ctx.ui.input("前端边界", "前端目录;没有请填写 none");
737
- if (frontendPath === undefined) return null;
738
- const backendPath = await ctx.ui.input("后端边界", "后端目录;没有请填写 none");
739
- if (backendPath === undefined) return null;
740
- const buildIndex = await ctx.ui.input("代码索引", "建立索引?输入 yes 或 no");
741
- if (buildIndex === undefined) return null;
855
+ }): Promise<ProjectInitializationRequest | null> {
856
+ const existing = discoverExistingProjectInitialization(ctx.cwd);
857
+ const reuseExisting = "使用已记录的项目结构(推荐)";
858
+ const rebuildExisting = "使用已记录的项目结构,并重新扫描代码";
859
+ const manualEntry = "我需要手动指定项目目录";
860
+
861
+ let projectName: string | undefined;
862
+ let frontendPath: string | undefined;
863
+ let backendPath: string | undefined;
864
+ let buildIndex: string | undefined;
865
+ let boundarySource: string | undefined;
866
+
867
+ if (existing) {
868
+ const frontend = existing.frontendPaths.length > 0 ? existing.frontendPaths.join(", ") : "未记录(unknown)";
869
+ const backend = existing.backendPaths.length > 0 ? existing.backendPaths.join(", ") : "未记录(unknown)";
870
+ const selected = await ctx.ui.select("检测到已有项目记录:这次初始化怎么处理?", [
871
+ {
872
+ label: reuseExisting,
873
+ description: `直接复用下面已核验、仍存在的目录,不重新扫描代码。\n前端:${frontend}\n后端:${backend}\n记录来源:${existing.sourcePath}${existing.codeIndexStatus ? `;索引状态:${existing.codeIndexStatus}` : ""}`,
874
+ },
875
+ {
876
+ label: rebuildExisting,
877
+ description: `沿用已登记目录,但重新扫描代码并更新索引。\n前端:${frontend}\n后端:${backend}`,
878
+ },
879
+ {
880
+ label: manualEntry,
881
+ description: "不采用上面的记录。我会依次询问项目名、前端目录和后端目录;目录均相对当前项目根目录。",
882
+ },
883
+ ]);
884
+ if (!selected) return null;
885
+ if (selected === reuseExisting || selected === rebuildExisting) {
886
+ projectName = existing.projectName;
887
+ frontendPath = existing.frontendPaths.join(", ") || "unknown";
888
+ backendPath = existing.backendPaths.join(", ") || "unknown";
889
+ buildIndex = selected === reuseExisting
890
+ ? `复用已有索引${existing.codeIndexStatus ? `(${existing.codeIndexStatus})` : ""}`
891
+ : "yes";
892
+ boundarySource = `${existing.sourcePath}(已校验目录仍存在)`;
893
+ }
894
+ }
895
+
896
+ if (!frontendPath || !backendPath || !buildIndex) {
897
+ projectName = await ctx.ui.input(
898
+ "项目显示名称(可留空)",
899
+ "用于 Devflow 文档标题;留空时使用当前目录名。例如:商城运营平台",
900
+ );
901
+ if (projectName === undefined) return null;
902
+ frontendPath = await ctx.ui.input(
903
+ "前端代码目录(相对当前项目根目录)",
904
+ "可填写多个目录,用英文逗号分隔。例:apps/admin, apps/h5;不确定请填 unknown",
905
+ );
906
+ if (frontendPath === undefined) return null;
907
+ backendPath = await ctx.ui.input(
908
+ "后端代码目录(相对当前项目根目录)",
909
+ "例:services/api 或 server;项目没有后端请填 none;不确定请填 unknown",
910
+ );
911
+ if (backendPath === undefined) return null;
912
+ const fullIndex = "建立完整代码索引(推荐)";
913
+ const skipIndex = "暂不建立代码索引";
914
+ const selectedIndex = await ctx.ui.select("是否扫描代码并建立索引?", [
915
+ {
916
+ label: fullIndex,
917
+ description: "扫描已确认的代码目录,建立调用关系和符号索引;后续需求与架构评估可直接复用。",
918
+ },
919
+ {
920
+ label: skipIndex,
921
+ description: "只建立项目说明与规则入口;后续需要源码级分析时再补建索引。",
922
+ },
923
+ ]);
924
+ if (!selectedIndex) return null;
925
+ buildIndex = selectedIndex === fullIndex ? "yes" : "no";
926
+ }
927
+ const planMode = "只读规划(不写任何项目文件)";
928
+ const applyMode = "写入初始化";
929
+ const selectedMode = await ctx.ui.select("确认这次初始化是否写入项目文件?", [
930
+ {
931
+ label: planMode,
932
+ description: "只读取项目和已有初始化记录,输出规划与待确认项;不写入任何项目文件。",
933
+ },
934
+ {
935
+ label: applyMode,
936
+ description: "确认范围后创建或更新 Devflow 初始化产物;不修改业务源码、依赖或项目配置。",
937
+ },
938
+ ]);
939
+ if (!selectedMode) return null;
940
+ const mode: ProjectInitializationMode = selectedMode === applyMode ? "apply" : "plan";
941
+ const modeSummary = mode === "apply" ? "写入初始化" : "只读规划";
742
942
  const summary = [
743
943
  `项目名:${projectName?.trim() || "当前目录"}`,
744
944
  `前端:${frontendPath.trim() || "未提供"}`,
745
945
  `后端:${backendPath.trim() || "未提供"}`,
746
946
  `建立代码索引:${buildIndex.trim() || "未提供"}`,
947
+ ...(boundarySource ? [`边界来源:${boundarySource}`] : []),
948
+ `初始化方式:${modeSummary}`,
747
949
  ].join("\n");
748
- const confirmed = await ctx.ui.confirm("确认项目初始化范围", `${summary}\n\n确认后只进入只读规划,不会创建工作流或写入项目文件。`);
950
+ const consequence = mode === "apply"
951
+ ? "确认后会创建 Devflow 的 .planning/ 与 .context/ 初始化产物;不会修改业务源码、依赖或项目配置。"
952
+ : "确认后只进入只读规划,不会创建工作流或写入项目文件。";
953
+ const confirmed = await ctx.ui.confirm("确认项目初始化范围", `${summary}\n\n${consequence}`);
749
954
  if (!confirmed) {
750
955
  ctx.ui.notify("项目初始化已取消;未写入任何文件。", "info");
751
956
  return null;
752
957
  }
753
- return summary;
958
+ return { summary, mode };
754
959
  }
755
960
 
756
961
  async function runRuby(cwd: string, relativeScript: string, args: string[]) {
@@ -952,7 +1157,7 @@ export default function devflowCore(pi: ExtensionAPI) {
952
1157
  }
953
1158
 
954
1159
  let activeWorkflowTurn = false;
955
- let pendingProjectInitialization: string | undefined;
1160
+ let pendingProjectInitialization: ProjectInitializationRequest | undefined;
956
1161
 
957
1162
  async function triggerWorkflow(snapshot: Record<string, unknown>, source: "start" | "approval", requirementDraft?: string) {
958
1163
  activeWorkflowTurn = true;
@@ -1013,7 +1218,7 @@ export default function devflowCore(pi: ExtensionAPI) {
1013
1218
  }
1014
1219
  setControlState(ctx, executingState("执行已批准的项目初始化"));
1015
1220
  activeWorkflowTurn = true;
1016
- return { text: projectInitializationDirective(pendingProjectInitialization), handled: false };
1221
+ return { text: projectInitializationDirective(pendingProjectInitialization.summary), handled: false };
1017
1222
  }
1018
1223
  if (controlAction === "reject") {
1019
1224
  ctx.ui.notify("已保留初始化规划,未创建任何项目控制文件。请直接说明需要调整的初始化范围。", "info");
@@ -1070,9 +1275,14 @@ export default function devflowCore(pi: ExtensionAPI) {
1070
1275
  return { handled: true };
1071
1276
  }
1072
1277
  if (intent === "project-init") {
1073
- const summary = await collectProjectInitialization(ctx);
1074
- if (!summary) return { handled: true };
1075
- pendingProjectInitialization = summary;
1278
+ const initialization = await collectProjectInitialization(ctx);
1279
+ if (!initialization) return { handled: true };
1280
+ pendingProjectInitialization = initialization;
1281
+ if (initialization.mode === "apply") {
1282
+ setControlState(ctx, executingState("执行已确认的项目初始化"));
1283
+ activeWorkflowTurn = true;
1284
+ return { text: projectInitializationDirective(initialization.summary), handled: false };
1285
+ }
1076
1286
  const state = planningState("project-init");
1077
1287
  setControlState(ctx, state);
1078
1288
  return {
@@ -1081,7 +1291,7 @@ export default function devflowCore(pi: ExtensionAPI) {
1081
1291
  "用户已确认项目初始化的输入。你现在是 Planner,只允许只读分析和派发 project-scout / requirements-auditor / contract-auditor。",
1082
1292
  "不得创建 .planning 文件、不得调用状态迁移、不得执行实现。",
1083
1293
  "先输出:已知事实、待验证点、最少侦察任务和必须由用户决定的问题。没有证据时写 unknown。",
1084
- summary,
1294
+ initialization.summary,
1085
1295
  "</devflow-planner-directive>",
1086
1296
  ].join("\n"),
1087
1297
  };
@@ -2026,9 +2236,15 @@ export default function devflowCore(pi: ExtensionAPI) {
2026
2236
  return;
2027
2237
  }
2028
2238
  if (trimmed === "project-init") {
2029
- const summary = await collectProjectInitialization(ctx);
2030
- if (!summary) return;
2031
- pendingProjectInitialization = summary;
2239
+ const initialization = await collectProjectInitialization(ctx);
2240
+ if (!initialization) return;
2241
+ pendingProjectInitialization = initialization;
2242
+ if (initialization.mode === "apply") {
2243
+ setControlState(ctx, executingState("执行已确认的项目初始化"));
2244
+ activeWorkflowTurn = true;
2245
+ await pi.sendUserMessage(projectInitializationDirective(initialization.summary));
2246
+ return;
2247
+ }
2032
2248
  setControlState(ctx, planningState("project-init"));
2033
2249
  await pi.sendUserMessage(
2034
2250
  [
@@ -2036,7 +2252,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2036
2252
  "用户已确认项目初始化的输入。你现在是 Planner,只允许只读分析和派发 project-scout / requirements-auditor / contract-auditor。",
2037
2253
  "不得创建 .planning 文件、不得调用状态迁移、不得执行实现。",
2038
2254
  "先输出:已知事实、待验证点、最少侦察任务和必须由用户决定的问题。没有证据时写 unknown。",
2039
- summary,
2255
+ initialization.summary,
2040
2256
  "</devflow-planner-directive>",
2041
2257
  ].join("\n"),
2042
2258
  );