@guanlin4924/devflow-pi 0.1.99 → 0.1.100

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.100
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.100",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -2672,7 +2672,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2672
2672
  const details = issues.length > 0
2673
2673
  ? issues.map(issue => `- ${describeStartupIssue(issue && typeof issue === "object" ? issue as Record<string, unknown> : {})}`).join("\n")
2674
2674
  : "- 开工预检输出无法解析";
2675
- ctx.ui.notify(`开工预检未通过:\n${details}`, "error");
2675
+ ctx.ui.notify(`开工预检未通过:\n${details}\n可以说「下一步」,由 Devflow 诊断并给出处理办法。`, "error");
2676
2676
  return { ok: false };
2677
2677
  }
2678
2678
  const facts = payload.facts && typeof payload.facts === "object" ? payload.facts as Record<string, unknown> : {};
@@ -2982,7 +2982,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2982
2982
  return true;
2983
2983
  }
2984
2984
 
2985
- async function diagnoseCurrentWorkflow(cwd: string): Promise<{
2985
+ async function diagnoseCurrentWorkflow(cwd: string, options: { skipWorkflowUpgrade?: boolean } = {}): Promise<{
2986
2986
  snapshot: Record<string, unknown> | null;
2987
2987
  inspectError?: string;
2988
2988
  rolloverState: Record<string, unknown> | null;
@@ -2996,6 +2996,40 @@ export default function devflowCore(pi: ExtensionAPI) {
2996
2996
  ? parseJsonResult(await runRuby(cwd, "workflow-orchestrator/scripts/session_rollover.rb", ["inspect", "--json"]))
2997
2997
  : null;
2998
2998
  const rollover = rolloverState?.rollover as Record<string, unknown> | undefined;
2999
+ let workflowUpgrade = { outdated: false, blocked: false, paths: [] as string[] };
3000
+ if (typeof snapshot?.current_feature === "string" && !options.skipWorkflowUpgrade) {
3001
+ try {
3002
+ const upgradeRun = await runRuby(
3003
+ cwd,
3004
+ "workflow-orchestrator/scripts/init_workflow.rb",
3005
+ ["--force", "--json"],
3006
+ );
3007
+ if (!upgradeRun.ok) throw new Error("workflow upgrade dry-run failed");
3008
+ const upgradeResult = parseJsonResult(upgradeRun);
3009
+ if (!upgradeResult) throw new Error("workflow upgrade dry-run returned invalid JSON");
3010
+ // init_workflow.rb returns { changes: [{ path, exists,
3011
+ // same_as_template, action, ... }] }; a dry-run drift has action
3012
+ // "would_skip_existing", so drift is derived from the boolean fields.
3013
+ const changes = Array.isArray(upgradeResult?.changes) ? upgradeResult.changes : [];
3014
+ const records = changes.filter((change): change is Record<string, unknown> =>
3015
+ Boolean(change && typeof change === "object" && !Array.isArray(change))
3016
+ );
3017
+ const paths = records.flatMap(change =>
3018
+ change.exists === true && change.same_as_template !== true && typeof change.path === "string"
3019
+ ? [change.path]
3020
+ : []
3021
+ );
3022
+ workflowUpgrade = {
3023
+ outdated: paths.length > 0,
3024
+ blocked: records.some(change =>
3025
+ change.action === "blocked_state_mismatch" || change.action === "would_overwrite_blocked"
3026
+ ),
3027
+ paths,
3028
+ };
3029
+ } catch {
3030
+ workflowUpgrade = { outdated: false, blocked: false, paths: [] };
3031
+ }
3032
+ }
2999
3033
  const diagnosis = diagnoseWorkflow({
3000
3034
  inspect: { ok: Boolean(snapshot), error: inspectError },
3001
3035
  snapshot,
@@ -3003,6 +3037,7 @@ export default function devflowCore(pi: ExtensionAPI) {
3003
3037
  outputsReady: snapshot ? isReviewableWorkflowSnapshot(snapshot) : false,
3004
3038
  rolloverPrepared: Boolean(rolloverState && typeof rolloverState.revision === "number"
3005
3039
  && rollover?.status === "prepared" && typeof rollover.id === "string"),
3040
+ workflowUpgrade,
3006
3041
  });
3007
3042
  return { snapshot, inspectError, rolloverState, diagnosis };
3008
3043
  }
@@ -3063,8 +3098,10 @@ export default function devflowCore(pi: ExtensionAPI) {
3063
3098
  return;
3064
3099
  }
3065
3100
  }
3101
+ // Declining the template upgrade must not strand the step behind it.
3102
+ let skipWorkflowUpgrade = false;
3066
3103
  for (let attempt = 0; attempt < 4; attempt += 1) {
3067
- const current = await diagnoseCurrentWorkflow(ctx.cwd);
3104
+ const current = await diagnoseCurrentWorkflow(ctx.cwd, { skipWorkflowUpgrade });
3068
3105
  const { diagnosis, snapshot } = current;
3069
3106
  const summary = renderDiagnosis(diagnosis);
3070
3107
  if (diagnosis.kind === "execute" && snapshot) {
@@ -3086,6 +3123,12 @@ export default function devflowCore(pi: ExtensionAPI) {
3086
3123
  { label: skip, description: "保持现状,不做任何修改" },
3087
3124
  ]);
3088
3125
  const fix = fixes.find(item => FIX_LABELS[item].label === choice);
3126
+ const upgradeOffered = diagnosis.diagnoses.some(item => item.code === "WORKFLOW_TEMPLATE_OUTDATED");
3127
+ if (!fix && upgradeOffered) {
3128
+ skipWorkflowUpgrade = true;
3129
+ ctx.ui.notify("本次不升级工作流定义,继续按现有定义诊断。", "info");
3130
+ continue;
3131
+ }
3089
3132
  if (!fix) {
3090
3133
  ctx.ui.notify("已保持现状,未做任何修改。", "info");
3091
3134
  return;
@@ -3145,6 +3188,37 @@ export default function devflowCore(pi: ExtensionAPI) {
3145
3188
  ctx.ui.notify("登记表已修复,未推进工作流。正在重新诊断……", "info");
3146
3189
  continue;
3147
3190
  }
3191
+ if (fix === "upgrade-workflow") {
3192
+ const paths = diagnosis.diagnoses
3193
+ .find(item => item.code === "WORKFLOW_TEMPLATE_OUTDATED")?.evidence ?? [];
3194
+ const confirmed = await ctx.ui.confirm(
3195
+ "升级工作流定义",
3196
+ `将用当前版本内置模板替换以下工作流文件:\n${paths.map(item => `- ${item}`).join("\n")}\n\n这些文件中的手工修改会被替换;需求产物和 STATE 不会改变,也不会推进任何步骤。继续吗?`,
3197
+ );
3198
+ if (!confirmed) {
3199
+ skipWorkflowUpgrade = true;
3200
+ ctx.ui.notify("本次不升级工作流定义,继续按现有定义诊断。", "info");
3201
+ continue;
3202
+ }
3203
+ const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/init_workflow.rb", [
3204
+ "--write", "--force", "--json",
3205
+ ]);
3206
+ if (!controlPlaneSucceeded(result)) {
3207
+ ctx.ui.notify(`工作流升级失败:${describeControlFailure(result)}`, "error");
3208
+ return;
3209
+ }
3210
+ const synced = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/sync_artifacts.rb", [
3211
+ "--write", "--no-preserve", "--json",
3212
+ ]);
3213
+ if (!controlPlaneSucceeded(synced)) {
3214
+ ctx.ui.notify(`工作流升级后的产物同步失败:${describeControlFailure(synced)}`, "error");
3215
+ return;
3216
+ }
3217
+ activeContract = undefined;
3218
+ contractAgentCalls = 0;
3219
+ ctx.ui.notify("工作流定义已升级。正在重新诊断……", "info");
3220
+ continue;
3221
+ }
3148
3222
  if (fix === "help-bundle") {
3149
3223
  const bundlePath = await writeHelpBundle(ctx, current);
3150
3224
  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
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.100";
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 {