@guanlin4924/devflow-pi 0.1.99 → 0.1.101

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
@@ -11,7 +11,7 @@ Devflow Pi 是一个面向项目研发交付的独立 OMP 产品入口,内置
11
11
  安装与启动:
12
12
 
13
13
  ```bash
14
- npm install -g @guanlin4924/devflow-pi@0.1.99
14
+ npm install -g @guanlin4924/devflow-pi@0.1.101
15
15
  cd /path/to/your-project
16
16
  devflow-pi init
17
17
  devflow-pi doctor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanlin4924/devflow-pi",
3
- "version": "0.1.99",
3
+ "version": "0.1.101",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -1075,8 +1075,10 @@ function isCoreOwnedGovernancePath(filePath: string): boolean {
1075
1075
  || /^\.devflow\/planning\/phases\/[^/\\\x00-\x1F\x7F]+\/REVIEW\.md$/u.test(filePath);
1076
1076
  }
1077
1077
 
1078
- function planningFeatureFromPath(filePath: string): string | null {
1079
- const match = filePath.match(/^\.devflow\/planning\/(?:requirements|features)\/([^/\\\x00-\x1F\x7F]+)(?:\/|$)/u);
1078
+ // Only `requirements/<feature>/...` is feature-scoped; files directly under
1079
+ // requirements/ (baseline.md, baseline.meta.json) are global artifacts.
1080
+ export function planningFeatureFromPath(filePath: string): string | null {
1081
+ const match = filePath.match(/^\.devflow\/planning\/(?:requirements|features)\/([^/\\\x00-\x1F\x7F]+)\//u);
1080
1082
  return match?.[1] ?? null;
1081
1083
  }
1082
1084
 
@@ -2672,7 +2674,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2672
2674
  const details = issues.length > 0
2673
2675
  ? issues.map(issue => `- ${describeStartupIssue(issue && typeof issue === "object" ? issue as Record<string, unknown> : {})}`).join("\n")
2674
2676
  : "- 开工预检输出无法解析";
2675
- ctx.ui.notify(`开工预检未通过:\n${details}`, "error");
2677
+ ctx.ui.notify(`开工预检未通过:\n${details}\n可以说「下一步」,由 Devflow 诊断并给出处理办法。`, "error");
2676
2678
  return { ok: false };
2677
2679
  }
2678
2680
  const facts = payload.facts && typeof payload.facts === "object" ? payload.facts as Record<string, unknown> : {};
@@ -2982,7 +2984,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2982
2984
  return true;
2983
2985
  }
2984
2986
 
2985
- async function diagnoseCurrentWorkflow(cwd: string): Promise<{
2987
+ async function diagnoseCurrentWorkflow(cwd: string, options: { skipWorkflowUpgrade?: boolean } = {}): Promise<{
2986
2988
  snapshot: Record<string, unknown> | null;
2987
2989
  inspectError?: string;
2988
2990
  rolloverState: Record<string, unknown> | null;
@@ -2996,6 +2998,40 @@ export default function devflowCore(pi: ExtensionAPI) {
2996
2998
  ? parseJsonResult(await runRuby(cwd, "workflow-orchestrator/scripts/session_rollover.rb", ["inspect", "--json"]))
2997
2999
  : null;
2998
3000
  const rollover = rolloverState?.rollover as Record<string, unknown> | undefined;
3001
+ let workflowUpgrade = { outdated: false, blocked: false, paths: [] as string[] };
3002
+ if (typeof snapshot?.current_feature === "string" && !options.skipWorkflowUpgrade) {
3003
+ try {
3004
+ const upgradeRun = await runRuby(
3005
+ cwd,
3006
+ "workflow-orchestrator/scripts/init_workflow.rb",
3007
+ ["--force", "--json"],
3008
+ );
3009
+ if (!upgradeRun.ok) throw new Error("workflow upgrade dry-run failed");
3010
+ const upgradeResult = parseJsonResult(upgradeRun);
3011
+ if (!upgradeResult) throw new Error("workflow upgrade dry-run returned invalid JSON");
3012
+ // init_workflow.rb returns { changes: [{ path, exists,
3013
+ // same_as_template, action, ... }] }; a dry-run drift has action
3014
+ // "would_skip_existing", so drift is derived from the boolean fields.
3015
+ const changes = Array.isArray(upgradeResult?.changes) ? upgradeResult.changes : [];
3016
+ const records = changes.filter((change): change is Record<string, unknown> =>
3017
+ Boolean(change && typeof change === "object" && !Array.isArray(change))
3018
+ );
3019
+ const paths = records.flatMap(change =>
3020
+ change.exists === true && change.same_as_template !== true && typeof change.path === "string"
3021
+ ? [change.path]
3022
+ : []
3023
+ );
3024
+ workflowUpgrade = {
3025
+ outdated: paths.length > 0,
3026
+ blocked: records.some(change =>
3027
+ change.action === "blocked_state_mismatch" || change.action === "would_overwrite_blocked"
3028
+ ),
3029
+ paths,
3030
+ };
3031
+ } catch {
3032
+ workflowUpgrade = { outdated: false, blocked: false, paths: [] };
3033
+ }
3034
+ }
2999
3035
  const diagnosis = diagnoseWorkflow({
3000
3036
  inspect: { ok: Boolean(snapshot), error: inspectError },
3001
3037
  snapshot,
@@ -3003,6 +3039,7 @@ export default function devflowCore(pi: ExtensionAPI) {
3003
3039
  outputsReady: snapshot ? isReviewableWorkflowSnapshot(snapshot) : false,
3004
3040
  rolloverPrepared: Boolean(rolloverState && typeof rolloverState.revision === "number"
3005
3041
  && rollover?.status === "prepared" && typeof rollover.id === "string"),
3042
+ workflowUpgrade,
3006
3043
  });
3007
3044
  return { snapshot, inspectError, rolloverState, diagnosis };
3008
3045
  }
@@ -3063,8 +3100,10 @@ export default function devflowCore(pi: ExtensionAPI) {
3063
3100
  return;
3064
3101
  }
3065
3102
  }
3103
+ // Declining the template upgrade must not strand the step behind it.
3104
+ let skipWorkflowUpgrade = false;
3066
3105
  for (let attempt = 0; attempt < 4; attempt += 1) {
3067
- const current = await diagnoseCurrentWorkflow(ctx.cwd);
3106
+ const current = await diagnoseCurrentWorkflow(ctx.cwd, { skipWorkflowUpgrade });
3068
3107
  const { diagnosis, snapshot } = current;
3069
3108
  const summary = renderDiagnosis(diagnosis);
3070
3109
  if (diagnosis.kind === "execute" && snapshot) {
@@ -3086,6 +3125,12 @@ export default function devflowCore(pi: ExtensionAPI) {
3086
3125
  { label: skip, description: "保持现状,不做任何修改" },
3087
3126
  ]);
3088
3127
  const fix = fixes.find(item => FIX_LABELS[item].label === choice);
3128
+ const upgradeOffered = diagnosis.diagnoses.some(item => item.code === "WORKFLOW_TEMPLATE_OUTDATED");
3129
+ if (!fix && upgradeOffered) {
3130
+ skipWorkflowUpgrade = true;
3131
+ ctx.ui.notify("本次不升级工作流定义,继续按现有定义诊断。", "info");
3132
+ continue;
3133
+ }
3089
3134
  if (!fix) {
3090
3135
  ctx.ui.notify("已保持现状,未做任何修改。", "info");
3091
3136
  return;
@@ -3145,6 +3190,37 @@ export default function devflowCore(pi: ExtensionAPI) {
3145
3190
  ctx.ui.notify("登记表已修复,未推进工作流。正在重新诊断……", "info");
3146
3191
  continue;
3147
3192
  }
3193
+ if (fix === "upgrade-workflow") {
3194
+ const paths = diagnosis.diagnoses
3195
+ .find(item => item.code === "WORKFLOW_TEMPLATE_OUTDATED")?.evidence ?? [];
3196
+ const confirmed = await ctx.ui.confirm(
3197
+ "升级工作流定义",
3198
+ `将用当前版本内置模板替换以下工作流文件:\n${paths.map(item => `- ${item}`).join("\n")}\n\n这些文件中的手工修改会被替换;需求产物和 STATE 不会改变,也不会推进任何步骤。继续吗?`,
3199
+ );
3200
+ if (!confirmed) {
3201
+ skipWorkflowUpgrade = true;
3202
+ ctx.ui.notify("本次不升级工作流定义,继续按现有定义诊断。", "info");
3203
+ continue;
3204
+ }
3205
+ const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/init_workflow.rb", [
3206
+ "--write", "--force", "--json",
3207
+ ]);
3208
+ if (!controlPlaneSucceeded(result)) {
3209
+ ctx.ui.notify(`工作流升级失败:${describeControlFailure(result)}`, "error");
3210
+ return;
3211
+ }
3212
+ const synced = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/sync_artifacts.rb", [
3213
+ "--write", "--no-preserve", "--json",
3214
+ ]);
3215
+ if (!controlPlaneSucceeded(synced)) {
3216
+ ctx.ui.notify(`工作流升级后的产物同步失败:${describeControlFailure(synced)}`, "error");
3217
+ return;
3218
+ }
3219
+ activeContract = undefined;
3220
+ contractAgentCalls = 0;
3221
+ ctx.ui.notify("工作流定义已升级。正在重新诊断……", "info");
3222
+ continue;
3223
+ }
3148
3224
  if (fix === "help-bundle") {
3149
3225
  const bundlePath = await writeHelpBundle(ctx, current);
3150
3226
  ctx.ui.notify(`已生成求助包:${bundlePath}\n只包含诊断信息(状态摘要、校验错误、最近的工具报错),不含源码。可以把这个文件发给维护者。`, "info");
@@ -63,7 +63,9 @@ targets = {
63
63
  "workflows-README.md" => ".devflow/planning/workflows/README.md"
64
64
  }
65
65
 
66
- blocked_target_paths = if options[:force] && options[:write] && !options[:allow_state_mismatch]
66
+ # Evaluated for dry runs too, so callers can tell before writing whether a
67
+ # forced upgrade would be refused for the active STATE step set.
68
+ blocked_target_paths = if options[:force] && !options[:allow_state_mismatch]
67
69
  targets.select do |template_name, target_path|
68
70
  source = File.join(template_dir, template_name)
69
71
  target = File.join(root, target_path)
@@ -158,7 +158,14 @@ def preflight(root: Path, feature: str, step: str, contract_path: str,
158
158
  issues.append({"code": "INVALID_OBLIGATION_PATH", "index": index, "detail": str(error)})
159
159
  continue
160
160
  producer = obligation.get("producer")
161
- if producer == "existing":
161
+ if producer == "existing" and relative.endswith("/"):
162
+ # Collection inputs (trailing "/") are directories, not files. As in
163
+ # gate_runtime.rb, a missing collection is allowed at entry.
164
+ if path.is_dir():
165
+ evidence.append({"kind": "input_collection", "path": relative})
166
+ elif path.exists() or stage != "entry":
167
+ issues.append({"code": "MISSING_INPUT_FACT", "index": index, "path": relative})
168
+ elif producer == "existing":
162
169
  if not path.is_file():
163
170
  issues.append({"code": "MISSING_INPUT_FACT", "index": index, "path": relative})
164
171
  elif obligation.get("sha256") is not None and obligation.get("sha256") != sha256(path):
@@ -120,10 +120,15 @@ def startup_preflight(root: Path, feature: str, step: str,
120
120
  else:
121
121
  continue
122
122
  rendered = render_path(relative, feature, phase)
123
+ collection = relative.endswith("/")
123
124
  try:
124
- exists = review_preflight.within(root, rendered).is_file()
125
+ target = review_preflight.within(root, rendered)
126
+ exists = target.is_dir() if collection else target.is_file()
125
127
  except ValueError:
126
128
  exists = False
129
+ # Same rule as gate_runtime.rb: a collection input may be absent at entry.
130
+ if collection and not exists:
131
+ required = False
127
132
  input_facts.append({"path": rendered, "required": required, "exists": exists})
128
133
  # Input readiness is independent of whether the caller supplied
129
134
  # a frozen contract. Keep the derived contract view exempt, as
@@ -438,7 +438,9 @@ steps:
438
438
  inputs:
439
439
  - ".devflow/planning/requirements/{feature}/决策地图.md"
440
440
  - ".devflow/planning/requirements/{feature}/wayfinder/"
441
- - ".devflow/planning/CONTEXT.md"
441
+ # Glossary created by domain-modeling on the first feature of a project.
442
+ - path: ".devflow/planning/CONTEXT.md"
443
+ required_at_entry: false
442
444
  outputs:
443
445
  - path: ".devflow/planning/requirements/{feature}/领域模型记录.md"
444
446
  type: domain-modeling
@@ -464,7 +466,9 @@ steps:
464
466
  inputs:
465
467
  - ".devflow/planning/requirements/{feature}/决策地图.md"
466
468
  - ".devflow/planning/requirements/{feature}/领域模型记录.md"
467
- - ".devflow/planning/CONTEXT.md"
469
+ # Glossary created by domain-modeling on the first feature of a project.
470
+ - path: ".devflow/planning/CONTEXT.md"
471
+ required_at_entry: false
468
472
  outputs:
469
473
  - path: ".devflow/planning/requirements/{feature}/可实现性审查.md"
470
474
  type: requirement-feasibility-review
@@ -1033,7 +1037,7 @@ steps:
1033
1037
  outputs:
1034
1038
  - path: ".devflow/planning/test-runs/{feature}/human-acceptance-results.json"
1035
1039
  type: human-acceptance-results
1036
- required: true
1040
+ required: false
1037
1041
  - path: ".devflow/planning/test-runs/{feature}/developer-handoff.md"
1038
1042
  type: developer-acceptance-handoff
1039
1043
  required: true
@@ -1050,7 +1054,9 @@ steps:
1050
1054
  - ".devflow/planning/code-index/index.sqlite"
1051
1055
  - ".devflow/planning/code-index/relations.jsonl"
1052
1056
  - ".devflow/planning/phases/{phase}/REVIEW.md"
1053
- - ".devflow/planning/test-runs/{feature}/human-acceptance-results.json"
1057
+ - path: ".devflow/planning/test-runs/{feature}/human-acceptance-results.json"
1058
+ required: false
1059
+ required_at_entry: false
1054
1060
  - ".devflow/planning/test-runs/{feature}/developer-handoff.md"
1055
1061
  outputs:
1056
1062
  - path: ".devflow/planning/features/{feature}/acceptance-test-bindings.json"
@@ -73,7 +73,7 @@ UI-SPEC 不写代码细节(那是前端技术方案的事),但必须把 UI
73
73
  - **A. 用户直接提供的截图/图片** → 直接读取分析
74
74
  - **B. Figma/蓝湖链接** → 要求用户导出截图后提供,不直接访问
75
75
  - **C. 需求基线中的 UI 描述** → 从 baseline.md 提取文字描述
76
- - **D. 无设计稿** → 标记为待确认,UI-SPEC.md 不生成,下游功能点梳理和前端方案直接使用基线描述
76
+ - **D. 无设计稿** → 仍按本文既定结构生成 UI-SPEC.md,使用需求基线中的 UI 描述填充,并在文档顶部声明“未提供设计来源,需求基线中的 UI 描述是本规范的来源”;所有缺少已确认来源的视觉值都写入“待确认事项”,下游步骤继续读取该文件
77
77
 
78
78
  3. 如果设计稿路径为图片(.png/.jpg/.webp),使用 Read 工具读取图片内容进行视觉分析。
79
79
 
@@ -26,7 +26,7 @@ Always create or update:
26
26
  "duplicate_count": 0,
27
27
  "prior_rejection_count": 0,
28
28
  "blocking_question": "",
29
- "next_skill": "workflow-requirement-baseline | workflow-demand-clarify | workflow-wayfinder | none",
29
+ "next_skill": "workflow-demand-clarify | workflow-wayfinder | none",
30
30
  "searched_sources": [],
31
31
  "related_code_paths": [],
32
32
  "related_history_paths": []
@@ -118,8 +118,8 @@ Result categories:
118
118
  |----------|---------|-------|
119
119
  | 已实现且一致 | Current behavior already satisfies the request | `already_done` |
120
120
  | 已实现但用户可能不知道 | Capability exists but may be hidden, permission-limited, or named differently | `ready_for_human` |
121
- | 部分重叠 | Existing behavior covers part of request | `ready_for_baseline` with reuse note |
122
- | 冲突 | Existing behavior contradicts requested behavior | `ready_for_baseline` with conflict note |
121
+ | 部分重叠 | Existing behavior covers part of request | `ready_for_wayfinder` with reuse note |
122
+ | 冲突 | Existing behavior contradicts requested behavior | `ready_for_wayfinder` with conflict note |
123
123
  | 未发现 | No reliable overlap after searched sources | Continue triage |
124
124
 
125
125
  ## Phase 3 — Prior Rejection / Out-Of-Scope Check
@@ -149,7 +149,7 @@ If found, route:
149
149
 
150
150
  - `wontfix` when the current request exactly repeats a still-valid rejection.
151
151
  - `ready_for_human` when the user may intentionally be reversing a prior decision.
152
- - `ready_for_baseline` when the prior decision is related but not blocking; record it as context.
152
+ - `ready_for_wayfinder` when the prior decision is related but not blocking; record the prior decision as context.
153
153
 
154
154
  ## Phase 4 — Scale And Fog Routing
155
155
 
package/src/constants.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PRODUCT_NAME = "Devflow Pi";
2
- export const PRODUCT_VERSION = "0.1.99";
2
+ export const PRODUCT_VERSION = "0.1.101";
3
3
  export const PROJECT_SCHEMA_VERSION = 1;
4
4
  export const OMP_RUNTIME_VERSION = "17.2.7";
5
5
 
@@ -12,6 +12,7 @@ export type FixAction =
12
12
  | "review-and-advance"
13
13
  | "rerun-step"
14
14
  | "revise-outputs"
15
+ | "upgrade-workflow"
15
16
  | "repair-registry"
16
17
  | "resume-rollover"
17
18
  | "help-bundle"
@@ -22,6 +23,7 @@ export type DiagnosisCode =
22
23
  | "NO_ACTIVE_FEATURE"
23
24
  | "WORKFLOW_COMPLETE"
24
25
  | "ROLLOVER_PENDING"
26
+ | "WORKFLOW_TEMPLATE_OUTDATED"
25
27
  | "REGISTRY_DRIFT"
26
28
  | "CHECKPOINT_WAITING"
27
29
  | "STEP_OUTPUTS_READY"
@@ -56,6 +58,7 @@ export type DiagnosisInput = {
56
58
  /** Every required output of the current step exists and is not marked invalid; `planned` allowed. */
57
59
  outputsReady: boolean;
58
60
  rolloverPrepared: boolean;
61
+ workflowUpgrade?: { outdated: boolean; blocked: boolean; paths: string[] };
59
62
  };
60
63
 
61
64
  export const FIX_LABELS: Record<FixAction, { label: string; description: string }> = {
@@ -63,9 +66,10 @@ export const FIX_LABELS: Record<FixAction, { label: string; description: string
63
66
  "review-and-advance": { label: "审查后推进", description: "弹出推进确认;确认后由 Core 校验产物并推进到下一步" },
64
67
  "rerun-step": { label: "重新执行这一步", description: "按当前步骤重新启动 AI(会先激活执行合同)" },
65
68
  "revise-outputs": { label: "按意见修改产物", description: "回到审查状态;你直接说明要改什么,AI 修改后再推进" },
69
+ "upgrade-workflow": { label: "升级工作流定义", description: "用当前版本内置的模板替换项目里的工作流文件(.devflow/planning/workflows/);不改需求产物,不推进步骤" },
66
70
  "repair-registry": { label: "修复登记表", description: "由 Core 按当前状态重建产物登记表;不推进工作流" },
67
71
  "resume-rollover": { label: "继续会话交接", description: "完成未走完的会话切换,然后在新会话中继续" },
68
- "help-bundle": { label: "生成求助包", description: "把诊断信息写到 .devflow/diagnostics/,不含源码内容" },
72
+ "help-bundle": { label: "生成求助包", description: "把诊断信息写到 .devflow/runs/diagnostics/,不含源码内容" },
69
73
  "cancel-feature": { label: "弃用当前需求", description: "结束当前需求并保留现有产物;之后可以创建新需求" },
70
74
  };
71
75
 
@@ -160,6 +164,18 @@ export function diagnoseWorkflow(input: DiagnosisInput): WorkflowDiagnosis {
160
164
  diagnoses: [{ code: "WORKFLOW_COMPLETE", title: `需求「${feature}」已经结束`, evidence: [], fixes: ["create-feature"] }],
161
165
  };
162
166
  }
167
+ if (input.workflowUpgrade?.outdated && !input.workflowUpgrade.blocked) {
168
+ return {
169
+ ...base,
170
+ kind: "repair",
171
+ diagnoses: [{
172
+ code: "WORKFLOW_TEMPLATE_OUTDATED",
173
+ title: "项目里的工作流定义比当前版本旧",
174
+ evidence: input.workflowUpgrade.paths,
175
+ fixes: ["upgrade-workflow"],
176
+ }],
177
+ };
178
+ }
163
179
  const plan = record(snapshot.recovery_plan);
164
180
  if (plan?.classification === "safe_repairable") {
165
181
  return {