@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 +1 -1
- package/package.json +1 -1
- package/plugins/devflow-core/extensions/devflow-core.ts +81 -5
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_workflow.rb +3 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/review_preflight.py +8 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/startup_preflight.py +6 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +10 -4
- package/plugins/devflow-design/skills/workflow-ui-spec/SKILL.md +1 -1
- package/plugins/devflow-requirements/skills/workflow-requirement-triage/SKILL.md +4 -4
- package/src/constants.ts +1 -1
- package/src/workflow-diagnosis.ts +17 -1
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -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
|
-
|
|
1079
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml
CHANGED
|
@@ -438,7 +438,9 @@ steps:
|
|
|
438
438
|
inputs:
|
|
439
439
|
- ".devflow/planning/requirements/{feature}/决策地图.md"
|
|
440
440
|
- ".devflow/planning/requirements/{feature}/wayfinder/"
|
|
441
|
-
-
|
|
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
|
-
-
|
|
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:
|
|
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. 无设计稿** →
|
|
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-
|
|
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 | `
|
|
122
|
-
| 冲突 | Existing behavior contradicts requested behavior | `
|
|
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
|
-
- `
|
|
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
|
@@ -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 {
|