openfeel 1.2.3 → 1.3.0
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.en.md +40 -63
- package/README.md +39 -51
- package/README.zh-CN.md +40 -63
- package/dist/commands/archive.d.ts.map +1 -1
- package/dist/commands/archive.js +13 -2
- package/dist/commands/archive.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +38 -2
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/flow.d.ts.map +1 -1
- package/dist/commands/flow.js +45 -12
- package/dist/commands/flow.js.map +1 -1
- package/dist/commands/lint.d.ts.map +1 -1
- package/dist/commands/lint.js +78 -39
- package/dist/commands/lint.js.map +1 -1
- package/dist/commands/model.d.ts.map +1 -1
- package/dist/commands/model.js +86 -8
- package/dist/commands/model.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +21 -1
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/roadmap.d.ts.map +1 -1
- package/dist/commands/roadmap.js +21 -2
- package/dist/commands/roadmap.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +90 -5
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +7 -2
- package/dist/commands/update.js.map +1 -1
- package/dist/core/archive/merge.d.ts +16 -1
- package/dist/core/archive/merge.d.ts.map +1 -1
- package/dist/core/archive/merge.js +27 -4
- package/dist/core/archive/merge.js.map +1 -1
- package/dist/core/config.js +2 -2
- package/dist/core/dsh-profile.d.ts +93 -0
- package/dist/core/dsh-profile.d.ts.map +1 -0
- package/dist/core/dsh-profile.js +398 -0
- package/dist/core/dsh-profile.js.map +1 -0
- package/dist/core/errors.d.ts +8 -3
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +6 -3
- package/dist/core/errors.js.map +1 -1
- package/dist/core/i18n-data/en.d.ts.map +1 -1
- package/dist/core/i18n-data/en.js +32 -2
- package/dist/core/i18n-data/en.js.map +1 -1
- package/dist/core/i18n-data/zh-CN.d.ts.map +1 -1
- package/dist/core/i18n-data/zh-CN.js +32 -2
- package/dist/core/i18n-data/zh-CN.js.map +1 -1
- package/dist/core/plan/roadmap.d.ts +29 -0
- package/dist/core/plan/roadmap.d.ts.map +1 -1
- package/dist/core/plan/roadmap.js +95 -0
- package/dist/core/plan/roadmap.js.map +1 -1
- package/dist/core/setup.d.ts +12 -1
- package/dist/core/setup.d.ts.map +1 -1
- package/dist/core/setup.js +25 -10
- package/dist/core/setup.js.map +1 -1
- package/dist/core/template-loader.d.ts.map +1 -1
- package/dist/core/template-loader.js +38 -19
- package/dist/core/template-loader.js.map +1 -1
- package/dist/core/update.d.ts +2 -0
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +45 -17
- package/dist/core/update.js.map +1 -1
- package/dsh/ACTION-GUIDE.md +366 -0
- package/dsh/ADAPTER.md +65 -0
- package/dsh/README.md +264 -0
- package/dsh/cordis.patch.yml +26 -0
- package/dsh/docs/compat/commands.md +873 -0
- package/dsh/docs/compat/kb-skills.md +883 -0
- package/dsh/docs/compat/persistence.md +1013 -0
- package/dsh/docs/compat/pipeline.md +1565 -0
- package/dsh/docs/compat/req.md +1243 -0
- package/dsh/docs/compat/stage.md +1437 -0
- package/dsh/lib/adapters/cli.js +624 -0
- package/dsh/lib/adapters/dashboard.js +393 -0
- package/dsh/lib/adapters/workspace.js +150 -0
- package/dsh/lib/index.js +189 -0
- package/dsh/lib/surface/command.js +186 -0
- package/dsh/lib/surface/prompt.js +131 -0
- package/dsh/lib/tools/index.js +1245 -0
- package/dsh/lib/tools/shared.js +320 -0
- package/dsh/package.json +45 -0
- package/package.json +9 -2
|
@@ -0,0 +1,1245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenFeel × DSH 适配器 — 工具集
|
|
3
|
+
*
|
|
4
|
+
* 每个工具 = 一个 `openfeel` CLI 命令组 + 一个 `action` 判别器。
|
|
5
|
+
* **所有写操作都经由 CLI**,因此 OpenFeel 的全部门禁(相位转移表、
|
|
6
|
+
* 需求 done 门、REV 阻塞、归档判据、revision CAS)都保持单一权威。
|
|
7
|
+
*
|
|
8
|
+
* 适配器自己只做三件事:
|
|
9
|
+
* 1. 把动作翻译为精确 argv;
|
|
10
|
+
* 2. 把 CLI 的人类文本 / JSON 归一化为工具结果;
|
|
11
|
+
* 3. 在拒绝时给出**可操作的下一步**(改哪个参数、先跑哪条命令)。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { FeelError } from '../adapters/cli.js';
|
|
15
|
+
import { legalNextPhases, planNextStep, readSnapshot } from '../adapters/dashboard.js';
|
|
16
|
+
import { resolveWorkspace } from '../adapters/workspace.js';
|
|
17
|
+
import {
|
|
18
|
+
boolean,
|
|
19
|
+
compileParameters,
|
|
20
|
+
envelopeSchema,
|
|
21
|
+
integer,
|
|
22
|
+
presentCall,
|
|
23
|
+
pushFlag,
|
|
24
|
+
pushOption,
|
|
25
|
+
pushPositional,
|
|
26
|
+
renderEnvelope,
|
|
27
|
+
requireArg,
|
|
28
|
+
string,
|
|
29
|
+
stringArray,
|
|
30
|
+
stringConst,
|
|
31
|
+
stringEnum,
|
|
32
|
+
toWireEnvelope,
|
|
33
|
+
} from './shared.js';
|
|
34
|
+
|
|
35
|
+
/** 15 个合法相位。 */
|
|
36
|
+
const PHASES = [
|
|
37
|
+
'plan_pending',
|
|
38
|
+
'plan_review',
|
|
39
|
+
'plan_passed',
|
|
40
|
+
'scheme_pending',
|
|
41
|
+
'scheme_review',
|
|
42
|
+
'scheme_passed',
|
|
43
|
+
'exec_running',
|
|
44
|
+
'review_pending',
|
|
45
|
+
'review_failed',
|
|
46
|
+
'review_passed',
|
|
47
|
+
'test_pending',
|
|
48
|
+
'test_failed',
|
|
49
|
+
'test_passed',
|
|
50
|
+
'archiving',
|
|
51
|
+
'done',
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/** 工作流模式(general 仅需求级合法)。 */
|
|
55
|
+
const MODES = ['full', 'fast', 'general'];
|
|
56
|
+
/** 审查档位。 */
|
|
57
|
+
const TIERS = ['full', 'lean', 'skip'];
|
|
58
|
+
/** 需求状态。 */
|
|
59
|
+
const REQ_STATUSES = ['draft', 'scheduled', 'in_progress', 'done', 'blocked', 'cancelled', 'rejected'];
|
|
60
|
+
/** 需求优先级。 */
|
|
61
|
+
const REQ_PRIORITIES = ['P0', 'P1', 'P2'];
|
|
62
|
+
/** 知识库分类(闭集)。 */
|
|
63
|
+
const KB_CATEGORIES = ['architecture', 'patterns', 'troubleshooting', 'setup'];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 工具工厂的公共依赖。
|
|
67
|
+
* @typedef {object} FeelToolContext
|
|
68
|
+
* @property {import('../adapters/cli.js').FeelCli} cli CLI 执行器。
|
|
69
|
+
* @property {(exec: object, args: object) => { projectRoot: string, initialized: boolean }} workspace 工作区解析。
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 一次工具执行。
|
|
74
|
+
* @typedef {object} ToolExecution
|
|
75
|
+
* @property {Record<string, any>} args 模型参数。
|
|
76
|
+
* @property {object} exec DSH 工具执行上下文(含 agent / signal)。
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 包装一次 CLI 调用为工具结果。
|
|
81
|
+
*
|
|
82
|
+
* 实现侧用 `{ summary, data, next }`;wire 侧由 `toWireEnvelope` 把 `data`
|
|
83
|
+
* 与 `next.arguments` 序列化为字符串(原因见 `shared.js` 的 `envelopeSchema`)。
|
|
84
|
+
*
|
|
85
|
+
* @param {string} action
|
|
86
|
+
* @param {() => Promise<{ data: unknown, summary: string, next?: { tool: string, arguments: unknown, why: string } | null }>} body
|
|
87
|
+
* @returns {Promise<any>}
|
|
88
|
+
*/
|
|
89
|
+
async function envelope(action, body) {
|
|
90
|
+
const result = await body();
|
|
91
|
+
return toWireEnvelope(action, {
|
|
92
|
+
summary: result.summary,
|
|
93
|
+
data: result.data,
|
|
94
|
+
next: result.next,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 构造全部工具定义。
|
|
100
|
+
*
|
|
101
|
+
* @param {FeelToolContext} context
|
|
102
|
+
* @returns {Array<Record<string, any>>}
|
|
103
|
+
*/
|
|
104
|
+
export function createTools(context) {
|
|
105
|
+
const { cli } = context;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 在目标项目上执行(把 cwd 锚定到解析出的项目根)。
|
|
109
|
+
* @param {object} exec
|
|
110
|
+
* @param {Record<string, any>} args
|
|
111
|
+
* @returns {{ cwd: string, sessionCwd: string }}
|
|
112
|
+
*/
|
|
113
|
+
const target = (exec, args) => {
|
|
114
|
+
const workspace = resolveWorkspace(exec, {
|
|
115
|
+
projectPath: typeof args.project === 'string' ? args.project : undefined,
|
|
116
|
+
fallback: cli.cwd,
|
|
117
|
+
});
|
|
118
|
+
return { cwd: workspace.projectRoot, sessionCwd: workspace.sessionCwd, initialized: workspace.initialized };
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 要求项目已初始化(配置可放宽)。
|
|
123
|
+
* @param {{ initialized: boolean, cwd: string }} resolved
|
|
124
|
+
*/
|
|
125
|
+
const requireInitialized = (resolved) => {
|
|
126
|
+
if (resolved.initialized) return;
|
|
127
|
+
throw new FeelError(
|
|
128
|
+
`目录 ${resolved.cwd} 不是 OpenFeel 项目(缺少 .openfeel/)。请先在该目录执行 \`openfeel init\`,或用参数 project 指定已初始化的项目路径。`,
|
|
129
|
+
{ kind: 'refusal', code: 'FEEL_NOT_INITIALIZED', detail: { projectRoot: resolved.cwd } },
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/* ══════════════════════════ 1. feel_status ══════════════════════════ */
|
|
134
|
+
|
|
135
|
+
const feelStatus = {
|
|
136
|
+
name: 'feel_status',
|
|
137
|
+
description:
|
|
138
|
+
'读取 OpenFeel 项目当前状态快照:宏观 pipeline 相位、**每个阶段的相位**(调度目标是 phase!=done 的活跃阶段)、当前工作流模式与来源、该阶段在此模式下的**合法下一相位**、负责 Agent、审查档位、守卫与阻塞审查。调度字段优先消费 `flow next --json` 权威输出(失败时回退 `flow status`/`flow mode`)。做任何推进/委派前先调用它。',
|
|
139
|
+
parameters: {
|
|
140
|
+
action: stringConst('status', '固定为 status(读取快照)。'),
|
|
141
|
+
project: string('项目根目录(相对会话工作区或绝对路径)。缺省时从会话工作区向上回溯 .openfeel/。'),
|
|
142
|
+
with_requirements: boolean('是否附带需求概览(多一次 CLI 调用)。'),
|
|
143
|
+
},
|
|
144
|
+
output: { schema: envelopeSchema(), render: renderEnvelope },
|
|
145
|
+
async execute(args, exec) {
|
|
146
|
+
const resolved = target(exec, args);
|
|
147
|
+
requireInitialized(resolved);
|
|
148
|
+
return envelope('status', async () => {
|
|
149
|
+
const snapshot = await readSnapshot(cli, {
|
|
150
|
+
cwd: resolved.cwd,
|
|
151
|
+
signal: exec.signal,
|
|
152
|
+
withRequirements: args.with_requirements === true,
|
|
153
|
+
});
|
|
154
|
+
snapshot.project.root = resolved.cwd;
|
|
155
|
+
const focus = snapshot.focus;
|
|
156
|
+
return {
|
|
157
|
+
data: snapshot,
|
|
158
|
+
summary: `pipeline=${snapshot.pipeline.phase || '(空)'} 活跃阶段=${focus.stage || '(无)'} phase=${focus.phase || '(空)'} mode=${snapshot.mode.current}(${snapshot.mode.source}) 负责=${focus.responsibleAgent} 可推进到=[${(focus.legalNextPhases ?? []).join(', ') || '无'}]`,
|
|
159
|
+
next: focus.phase === 'done' || focus.phase === ''
|
|
160
|
+
? null
|
|
161
|
+
: { tool: 'feel_next', arguments: {}, why: '获取「下一步该谁做」的明确建议。' },
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
presentCall: () => presentCall('读取 Feel 状态', 'get'),
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/* ══════════════════════════ 2. feel_next ══════════════════════════ */
|
|
169
|
+
|
|
170
|
+
const feelNext = {
|
|
171
|
+
name: 'feel_next',
|
|
172
|
+
description:
|
|
173
|
+
'返回「下一步该做什么、该谁做」。直接消费 `openfeel flow next --json` 的权威决策(负责 Agent / 相位标签 / 合法下一相位 / 建议命令 / 守卫 / 阻塞审查),并给出推荐工具调用与 OpenFeel 交接标记;适配器不持有相位→Agent 映射。Feel 角色在每次调度前应调用它,而不是自行推断。',
|
|
174
|
+
parameters: {
|
|
175
|
+
action: stringConst('next', '固定为 next(决定下一步该谁做)。'),
|
|
176
|
+
project: string('项目根目录。缺省从会话工作区回溯。'),
|
|
177
|
+
},
|
|
178
|
+
output: { schema: envelopeSchema(), render: renderEnvelope },
|
|
179
|
+
async execute(args, exec) {
|
|
180
|
+
const resolved = target(exec, args);
|
|
181
|
+
requireInitialized(resolved);
|
|
182
|
+
return envelope('next', async () => {
|
|
183
|
+
const snapshot = await readSnapshot(cli, { cwd: resolved.cwd, signal: exec.signal });
|
|
184
|
+
snapshot.project.root = resolved.cwd;
|
|
185
|
+
const plan = planNextStep(snapshot);
|
|
186
|
+
return {
|
|
187
|
+
data: plan,
|
|
188
|
+
summary: plan.summary,
|
|
189
|
+
next: plan.toolCall === null || plan.toolCall === undefined
|
|
190
|
+
? null
|
|
191
|
+
: { tool: plan.toolCall.tool, arguments: plan.toolCall.arguments, why: `推进到 ${plan.toolCall.arguments.to}` },
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
presentCall: () => presentCall('决定下一步', 'read'),
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/* ══════════════════════════ 3. feel_flow ══════════════════════════ */
|
|
199
|
+
|
|
200
|
+
const feelFlow = {
|
|
201
|
+
name: 'feel_flow',
|
|
202
|
+
description:
|
|
203
|
+
'流水线操作(openfeel flow)。action=advance 推进相位(受转移表 + done 门 + blocking REV 门约束;--force 只能跳过相位合法性,**不能**绕过 blocking REV);action=current/overview/log/metrics/retry 为只读;action=phases 列出合法相位与流转;action=mode 查看模式与来源;action=review_* 管理审查条目(REV);action=repair 修复 flow.json;action=migrate 升级旧版 flow.json。',
|
|
204
|
+
parameters: {
|
|
205
|
+
action: stringEnum(
|
|
206
|
+
['advance', 'current', 'overview', 'phases', 'mode', 'log', 'metrics', 'retry', 'repair', 'migrate', 'review_add', 'review_resolve', 'review_update', 'review_remove'],
|
|
207
|
+
'操作。',
|
|
208
|
+
{ required: true },
|
|
209
|
+
),
|
|
210
|
+
stage: string('阶段 ID(advance/retry 必需;phases/mode 可选用于按阶段派生模式)。'),
|
|
211
|
+
to: stringEnum(PHASES, 'advance 的目标相位。'),
|
|
212
|
+
op: string('操作 ID(如 v1.2.1-stage-93.op-001)。'),
|
|
213
|
+
force: boolean('advance 时跳过相位合法性与跳跃检查(不能绕过 blocking REV 门)。'),
|
|
214
|
+
dry_run: boolean('仅校验/预览,不写盘。'),
|
|
215
|
+
review_tier: stringEnum(TIERS, '审查档位:full=零裁剪 / lean=裁 scheme_review / skip=再裁 plan_review。缺省不裁剪(=full)。'),
|
|
216
|
+
quiet: boolean('静默非错误输出。'),
|
|
217
|
+
mode: stringEnum(['full', 'fast', 'general'], 'phases 时显式指定工作流模式。'),
|
|
218
|
+
last: integer('log 显示的最近条数。', { minimum: 1 }),
|
|
219
|
+
rev_id: string('REV 条目 ID(review_resolve/review_update/review_remove 必需,如 REV-001)。'),
|
|
220
|
+
title: string('review_add/review_update 的标题。'),
|
|
221
|
+
priority: stringEnum(['high', 'medium', 'low'], 'review_update 的优先级。'),
|
|
222
|
+
blocking: boolean('review_add/review_update 是否阻塞流水线(缺省视为阻塞)。'),
|
|
223
|
+
auto_fix: string('review_add 的自动修复说明;设置后跳过 scheme_pending 直接推进 exec_running。'),
|
|
224
|
+
result: stringEnum(['pass', 'fail'], '未使用,保留占位。'),
|
|
225
|
+
prune_orphans: boolean('repair 时清理键孤儿。'),
|
|
226
|
+
backup: boolean('repair 前备份为 .bak。'),
|
|
227
|
+
project: string('项目根目录。'),
|
|
228
|
+
},
|
|
229
|
+
output: { schema: envelopeSchema(), render: renderEnvelope },
|
|
230
|
+
async execute(args, exec) {
|
|
231
|
+
const { cwd } = target(exec, args);
|
|
232
|
+
const action = args.action;
|
|
233
|
+
const opts = { cwd, signal: exec.signal };
|
|
234
|
+
|
|
235
|
+
switch (action) {
|
|
236
|
+
case 'advance': {
|
|
237
|
+
const stage = requireArg(args, 'stage', action);
|
|
238
|
+
const to = requireArg(args, 'to', action);
|
|
239
|
+
const argv = ['flow', 'advance', '--stage', stage, '--to', to];
|
|
240
|
+
pushOption(argv, '--op', args.op);
|
|
241
|
+
pushFlag(argv, '--force', args.force);
|
|
242
|
+
pushFlag(argv, '--dry-run', args.dry_run);
|
|
243
|
+
pushOption(argv, '--review-tier', args.review_tier);
|
|
244
|
+
pushFlag(argv, '--quiet', args.quiet);
|
|
245
|
+
return envelope(action, async () => {
|
|
246
|
+
const out = args.dry_run === true
|
|
247
|
+
? await cli.run(argv, opts)
|
|
248
|
+
: await cli.run(argv, opts);
|
|
249
|
+
if (out.code !== 0) throw cli.errorFromResult(out, argv);
|
|
250
|
+
const snapshot = await readSnapshot(cli, opts).catch(() => undefined);
|
|
251
|
+
return {
|
|
252
|
+
data: { stage, to, dryRun: args.dry_run === true, output: out.stdout },
|
|
253
|
+
summary: args.dry_run === true
|
|
254
|
+
? `预演通过:${stage} → ${to}(未写盘)。`
|
|
255
|
+
: `${stage} 已推进到 ${to}。当前活跃阶段=${snapshot?.focus?.stage ?? '?'} phase=${snapshot?.focus?.phase ?? '?'}`,
|
|
256
|
+
next: snapshot === undefined ? null : snapshotNext(snapshot),
|
|
257
|
+
};
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
case 'current':
|
|
261
|
+
case 'overview':
|
|
262
|
+
case 'log':
|
|
263
|
+
case 'metrics':
|
|
264
|
+
case 'retry':
|
|
265
|
+
case 'phases':
|
|
266
|
+
case 'mode': {
|
|
267
|
+
const argv = ['flow', action];
|
|
268
|
+
pushOption(argv, '--stage', args.stage);
|
|
269
|
+
pushOption(argv, '--mode', args.mode);
|
|
270
|
+
pushOption(argv, '--last', args.last);
|
|
271
|
+
pushFlag(argv, '--json', true);
|
|
272
|
+
return envelope(action, async () => {
|
|
273
|
+
const data = await cli.json(argv, opts);
|
|
274
|
+
return { data, summary: `已读取 flow ${action}。` };
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
case 'repair': {
|
|
278
|
+
const argv = ['flow', 'repair'];
|
|
279
|
+
pushFlag(argv, '--dry-run', args.dry_run);
|
|
280
|
+
pushFlag(argv, '--backup', args.backup);
|
|
281
|
+
pushFlag(argv, '--prune-orphans', args.prune_orphans);
|
|
282
|
+
return envelope(action, async () => {
|
|
283
|
+
const out = await cli.write(argv, opts);
|
|
284
|
+
return { data: { output: out.text }, summary: args.dry_run === true ? '修复预演完成(未写盘)。' : '修复完成。' };
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
case 'migrate': {
|
|
288
|
+
const argv = ['flow', 'migrate'];
|
|
289
|
+
pushFlag(argv, '--dry-run', args.dry_run);
|
|
290
|
+
return envelope(action, async () => {
|
|
291
|
+
const out = await cli.write(argv, opts);
|
|
292
|
+
return { data: { output: out.text }, summary: '旧版 flow.json 迁移完成。' };
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
case 'review_add': {
|
|
296
|
+
const argv = ['flow', 'review', 'add'];
|
|
297
|
+
pushOption(argv, '--op', args.op);
|
|
298
|
+
pushOption(argv, '--title', requireArg(args, 'title', action));
|
|
299
|
+
pushOption(argv, '--auto-fix', args.auto_fix);
|
|
300
|
+
if (args.blocking !== undefined) argv.push('--blocking', args.blocking === true ? 'true' : 'false');
|
|
301
|
+
return envelope(action, async () => {
|
|
302
|
+
const out = await cli.write(argv, opts);
|
|
303
|
+
return { data: { output: out.text }, summary: '审查条目已添加。' };
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
case 'review_resolve':
|
|
307
|
+
case 'review_remove': {
|
|
308
|
+
const revId = requireArg(args, 'rev_id', action);
|
|
309
|
+
const argv = ['flow', 'review', action === 'review_resolve' ? 'resolve' : 'remove', revId];
|
|
310
|
+
return envelope(action, async () => {
|
|
311
|
+
const out = await cli.write(argv, opts);
|
|
312
|
+
return { data: { revId, output: out.text }, summary: `REV ${revId} 已${action === 'review_resolve' ? '解决' : '删除'}。` };
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
case 'review_update': {
|
|
316
|
+
const revId = requireArg(args, 'rev_id', action);
|
|
317
|
+
const argv = ['flow', 'review', 'update', revId];
|
|
318
|
+
pushOption(argv, '--priority', args.priority);
|
|
319
|
+
pushOption(argv, '--title', args.title);
|
|
320
|
+
if (args.blocking !== undefined) argv.push('--blocking', args.blocking === true ? 'true' : 'false');
|
|
321
|
+
if (argv.length === 4) throw new FeelError('review_update 至少需要 priority / title / blocking 之一', { kind: 'usage', code: 'FEEL_BAD_USAGE' });
|
|
322
|
+
return envelope(action, async () => {
|
|
323
|
+
const out = await cli.write(argv, opts);
|
|
324
|
+
return { data: { revId, output: out.text }, summary: `REV ${revId} 已更新。` };
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
default:
|
|
328
|
+
throw new FeelError(`未知 action: ${action}`, { kind: 'usage', code: 'FEEL_UNKNOWN_ACTION' });
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
presentCall: (args) => presentCall(`Feel 流水线 ${args.action}`, args.action, args.stage ?? args.rev_id),
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/* ══════════════════════════ 4. feel_stage ══════════════════════════ */
|
|
335
|
+
|
|
336
|
+
const feelStage = {
|
|
337
|
+
name: 'feel_stage',
|
|
338
|
+
description:
|
|
339
|
+
'工作阶段与 status.md 操作。action=list 列出工作阶段;action=add 添加工作阶段(建目录 + overview/status + 注册 flow.json,这是**唯一完整入口**);action=status 查看阶段状态;action=set 原子更新 status.md 字段(状态/执行模式/自动推进/责任 Agent/工作流模式);action=task 勾选或追加任务;action=deps 设置依赖;action=reset 复位相位;action=remove 移除阶段。禁止直接编辑 status.md 或 flow.json。',
|
|
340
|
+
parameters: {
|
|
341
|
+
action: stringEnum(['list', 'add', 'status', 'set', 'task', 'deps', 'reset', 'remove'], '操作。', { required: true }),
|
|
342
|
+
stage: string('阶段 ID(如 v1.2.1-stage-93 或 stage-93)。除 list/add 外必需。'),
|
|
343
|
+
deps: stringArray('依赖阶段 ID 列表(add/deps)。'),
|
|
344
|
+
tasks: stringArray('初始任务列表(add)。'),
|
|
345
|
+
exec_mode: stringEnum(['manual', 'auto'], '执行模式(add/set)。'),
|
|
346
|
+
auto_advance: stringEnum(['enabled', 'disabled'], '自动推进(add/set)。'),
|
|
347
|
+
workflow_mode: stringEnum(MODES, '工作流模式(add/set)。**general 仅需求级合法**,阶段级会被拒绝。'),
|
|
348
|
+
status: stringEnum(['planned', 'review_failed', 'review_passed', 'testing', 'archiving', 'done'], '阶段粗粒度状态(set)。'),
|
|
349
|
+
review_agent: string('当前责任 Agent(set,如 openfeel-executor)。'),
|
|
350
|
+
task_no: integer('任务编号(task 的勾选操作)。', { minimum: 1 }),
|
|
351
|
+
task_add: string('追加一条任务描述(task)。'),
|
|
352
|
+
done: boolean('task:标记为已完成。'),
|
|
353
|
+
undone: boolean('task:标记为未完成。'),
|
|
354
|
+
to: stringEnum(PHASES, 'reset 的目标相位。'),
|
|
355
|
+
dry_run: boolean('仅预览(reset/remove)。'),
|
|
356
|
+
force: boolean('remove 时越过安全校验。'),
|
|
357
|
+
purge: boolean('remove 时同时删除阶段目录(非 TTY 须配 force)。'),
|
|
358
|
+
project: string('项目根目录。'),
|
|
359
|
+
},
|
|
360
|
+
output: { schema: envelopeSchema(), render: renderEnvelope },
|
|
361
|
+
async execute(args, exec) {
|
|
362
|
+
const { cwd } = target(exec, args);
|
|
363
|
+
const action = args.action;
|
|
364
|
+
const opts = { cwd, signal: exec.signal };
|
|
365
|
+
|
|
366
|
+
switch (action) {
|
|
367
|
+
case 'list': {
|
|
368
|
+
return envelope(action, async () => {
|
|
369
|
+
const data = await cli.json(['plan', 'stage', 'list', '--json'], opts);
|
|
370
|
+
const stages = Array.isArray(data?.stages) ? data.stages : [];
|
|
371
|
+
return { data, summary: `已列出工作阶段(${stages.length} 个)。` };
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
case 'add': {
|
|
375
|
+
const stage = requireArg(args, 'stage', action);
|
|
376
|
+
const argv = ['plan', 'stage', 'add', stage];
|
|
377
|
+
if (Array.isArray(args.deps) && args.deps.length > 0) argv.push('--deps', ...args.deps);
|
|
378
|
+
if (Array.isArray(args.tasks) && args.tasks.length > 0) argv.push('--tasks', ...args.tasks);
|
|
379
|
+
pushOption(argv, '--exec-mode', args.exec_mode);
|
|
380
|
+
pushOption(argv, '--auto-advance', args.auto_advance);
|
|
381
|
+
pushOption(argv, '--workflow-mode', args.workflow_mode);
|
|
382
|
+
return envelope(action, async () => {
|
|
383
|
+
const out = await cli.write(argv, opts);
|
|
384
|
+
return { data: { stage, output: out.text }, summary: `阶段 ${stage} 已添加并注册。` };
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
case 'status': {
|
|
388
|
+
const stage = args.stage === undefined ? [] : [requireArg(args, 'stage', action)];
|
|
389
|
+
return envelope(action, async () => {
|
|
390
|
+
const data = await cli.json(['stage', 'status', ...stage, '--json'], opts);
|
|
391
|
+
return { data, summary: '已读取阶段状态。' };
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
case 'set': {
|
|
395
|
+
const stage = requireArg(args, 'stage', action);
|
|
396
|
+
const argv = ['stage', 'set', stage];
|
|
397
|
+
pushOption(argv, '--status', args.status);
|
|
398
|
+
pushOption(argv, '--exec-mode', args.exec_mode);
|
|
399
|
+
pushOption(argv, '--auto-advance', args.auto_advance);
|
|
400
|
+
pushOption(argv, '--review-agent', args.review_agent);
|
|
401
|
+
pushOption(argv, '--workflow-mode', args.workflow_mode);
|
|
402
|
+
if (argv.length === 3) throw new FeelError('stage set 至少需要一个字段(status/exec_mode/auto_advance/review_agent/workflow_mode)', { kind: 'usage', code: 'FEEL_BAD_USAGE' });
|
|
403
|
+
return envelope(action, async () => {
|
|
404
|
+
const out = await cli.write(argv, opts);
|
|
405
|
+
return { data: { stage, output: out.text }, summary: `阶段 ${stage} 字段已更新。` };
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
case 'task': {
|
|
409
|
+
const stage = requireArg(args, 'stage', action);
|
|
410
|
+
const argv = ['stage', 'task', stage];
|
|
411
|
+
if (args.task_no !== undefined) argv.push(String(args.task_no));
|
|
412
|
+
pushOption(argv, '--add', args.task_add);
|
|
413
|
+
pushFlag(argv, '--done', args.done);
|
|
414
|
+
pushFlag(argv, '--undone', args.undone);
|
|
415
|
+
return envelope(action, async () => {
|
|
416
|
+
const out = await cli.write(argv, opts);
|
|
417
|
+
return { data: { stage, output: out.text }, summary: '阶段任务已更新。' };
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
case 'deps': {
|
|
421
|
+
const stage = requireArg(args, 'stage', action);
|
|
422
|
+
const argv = ['flow', 'stage', 'set', stage];
|
|
423
|
+
if (Array.isArray(args.deps) && args.deps.length > 0) argv.push('--deps', ...args.deps);
|
|
424
|
+
return envelope(action, async () => {
|
|
425
|
+
const out = await cli.write(argv, opts);
|
|
426
|
+
return { data: { stage, deps: args.deps ?? [], output: out.text }, summary: `阶段 ${stage} 依赖已${Array.isArray(args.deps) && args.deps.length > 0 ? '设置' : '清空'}。` };
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
case 'reset': {
|
|
430
|
+
const stage = requireArg(args, 'stage', action);
|
|
431
|
+
const argv = ['flow', 'stage', 'reset', stage];
|
|
432
|
+
pushOption(argv, '--to', requireArg(args, 'to', action));
|
|
433
|
+
pushFlag(argv, '--dry-run', args.dry_run);
|
|
434
|
+
return envelope(action, async () => {
|
|
435
|
+
const out = await cli.write(argv, opts);
|
|
436
|
+
return { data: { stage, to: args.to, dryRun: args.dry_run === true, output: out.text }, summary: `阶段 ${stage} 相位已复位到 ${args.to}。` };
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
case 'remove': {
|
|
440
|
+
const stage = requireArg(args, 'stage', action);
|
|
441
|
+
const argv = ['flow', 'stage', 'remove', stage];
|
|
442
|
+
pushFlag(argv, '--force', args.force);
|
|
443
|
+
pushFlag(argv, '--dry-run', args.dry_run);
|
|
444
|
+
pushFlag(argv, '--purge', args.purge);
|
|
445
|
+
return envelope(action, async () => {
|
|
446
|
+
const out = await cli.write(argv, opts);
|
|
447
|
+
return { data: { stage, output: out.text }, summary: `阶段 ${stage} 已${args.dry_run === true ? '预演移除' : '移除'}。` };
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
default:
|
|
451
|
+
throw new FeelError(`未知 action: ${action}`, { kind: 'usage', code: 'FEEL_UNKNOWN_ACTION' });
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
presentCall: (args) => presentCall(`Feel 阶段 ${args.action}`, args.action, args.stage),
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
/* ══════════════════════════ 5. feel_op ══════════════════════════ */
|
|
458
|
+
|
|
459
|
+
const feelOp = {
|
|
460
|
+
name: 'feel_op',
|
|
461
|
+
description:
|
|
462
|
+
'操作方案(op)与执行结果。action=list 列出方案(含状态与模板填充度);action=create 创建方案(draft);action=publish 把 draft 发布为 pending;action=rename 改标题;action=remove 注销方案(不删模板文件);action=register 补注册文件孤儿;action=attempt 记录执行结果 pass/fail(驱动重试计数);action=retry 查询重试状态。',
|
|
463
|
+
parameters: {
|
|
464
|
+
action: stringEnum(['list', 'create', 'publish', 'rename', 'remove', 'register', 'attempt', 'retry'], '操作。', { required: true }),
|
|
465
|
+
stage: string('阶段 ID。除 retry 外必需。'),
|
|
466
|
+
op: string('操作 ID(如 op-001,或 attempt/retry 用的完整 ID v1.2.1-stage-93.op-001)。'),
|
|
467
|
+
title: string('方案标题(create/rename)。'),
|
|
468
|
+
result: stringEnum(['pass', 'fail'], 'attempt 的执行结果。'),
|
|
469
|
+
project: string('项目根目录。'),
|
|
470
|
+
},
|
|
471
|
+
output: { schema: envelopeSchema(), render: renderEnvelope },
|
|
472
|
+
async execute(args, exec) {
|
|
473
|
+
const { cwd } = target(exec, args);
|
|
474
|
+
const action = args.action;
|
|
475
|
+
const opts = { cwd, signal: exec.signal };
|
|
476
|
+
|
|
477
|
+
switch (action) {
|
|
478
|
+
case 'list': {
|
|
479
|
+
const argv = ['flow', 'ops', 'list'];
|
|
480
|
+
pushOption(argv, '--stage', args.stage);
|
|
481
|
+
argv.push('--json');
|
|
482
|
+
return envelope(action, async () => {
|
|
483
|
+
const data = await cli.json(argv, opts);
|
|
484
|
+
return { data, summary: '已列出操作方案。' };
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
case 'create': {
|
|
488
|
+
const stage = requireArg(args, 'stage', action);
|
|
489
|
+
const argv = ['plan', 'scheme', 'create', stage];
|
|
490
|
+
pushPositional(argv, args.title);
|
|
491
|
+
return envelope(action, async () => {
|
|
492
|
+
const out = await cli.write(argv, opts);
|
|
493
|
+
return { data: { stage, output: out.text }, summary: `已为 ${stage} 创建操作方案。` };
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
case 'publish': {
|
|
497
|
+
const stage = requireArg(args, 'stage', action);
|
|
498
|
+
const op = requireArg(args, 'op', action);
|
|
499
|
+
return envelope(action, async () => {
|
|
500
|
+
const out = await cli.write(['plan', 'scheme', 'publish', stage, op], opts);
|
|
501
|
+
return { data: { stage, op, output: out.text }, summary: `方案 ${op} 已发布为可执行。` };
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
case 'rename': {
|
|
505
|
+
const stage = requireArg(args, 'stage', action);
|
|
506
|
+
const op = requireArg(args, 'op', action);
|
|
507
|
+
const argv = ['plan', 'scheme', 'rename', stage, op];
|
|
508
|
+
pushOption(argv, '--title', args.title);
|
|
509
|
+
return envelope(action, async () => {
|
|
510
|
+
const out = await cli.write(argv, opts);
|
|
511
|
+
return { data: { stage, op, output: out.text }, summary: `方案 ${op} 标题已更新。` };
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
case 'remove': {
|
|
515
|
+
const stage = requireArg(args, 'stage', action);
|
|
516
|
+
const op = requireArg(args, 'op', action);
|
|
517
|
+
return envelope(action, async () => {
|
|
518
|
+
const out = await cli.write(['plan', 'scheme', 'remove', stage, op], opts);
|
|
519
|
+
return { data: { stage, op, output: out.text }, summary: `方案 ${op} 已注销。` };
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
case 'register': {
|
|
523
|
+
const stage = requireArg(args, 'stage', action);
|
|
524
|
+
const argv = ['plan', 'scheme', 'register', stage];
|
|
525
|
+
pushPositional(argv, args.op);
|
|
526
|
+
return envelope(action, async () => {
|
|
527
|
+
const out = await cli.write(argv, opts);
|
|
528
|
+
return { data: { stage, output: out.text }, summary: '文件孤儿方案已补注册。' };
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
case 'attempt': {
|
|
532
|
+
const op = requireArg(args, 'op', action);
|
|
533
|
+
const result = requireArg(args, 'result', action);
|
|
534
|
+
return envelope(action, async () => {
|
|
535
|
+
const out = await cli.write(['flow', 'attempt', '--op', op, '--result', result], opts);
|
|
536
|
+
return { data: { op, result, output: out.text }, summary: `已记录 ${op} 执行结果=${result}。` };
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
case 'retry': {
|
|
540
|
+
const op = requireArg(args, 'op', action);
|
|
541
|
+
return envelope(action, async () => {
|
|
542
|
+
const data = await cli.json(['flow', 'retry', '--op', op, '--json'], opts);
|
|
543
|
+
const attempts = typeof data?.attempts === 'number' ? data.attempts : 0;
|
|
544
|
+
const max = typeof data?.maxAttempts === 'number' ? data.maxAttempts : 0;
|
|
545
|
+
const exhausted = data?.exhausted === true ? '(重试次数已用尽)' : '';
|
|
546
|
+
return { data, summary: `${op} 重试状态:${data?.state ?? '?'},${attempts}/${max}${exhausted}。` };
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
default:
|
|
550
|
+
throw new FeelError(`未知 action: ${action}`, { kind: 'usage', code: 'FEEL_UNKNOWN_ACTION' });
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
presentCall: (args) => presentCall(`Feel 方案 ${args.action}`, args.action, args.op),
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
/* ══════════════════════════ 6. feel_req ══════════════════════════ */
|
|
557
|
+
|
|
558
|
+
const feelReq = {
|
|
559
|
+
name: 'feel_req',
|
|
560
|
+
description:
|
|
561
|
+
'需求分发系统(openfeel req,7 状态)。action=add 创建需求(ID 自动分配);list 过滤列表;show 详情;board 看板(7 组固定顺序);next 推荐下一个可接取需求;claim 认领(占用校验、无 TTL);release 释放;set 更新白名单字段;link 关联/解绑阶段;advance 显式推进状态(**done 门:非 general 需求须全部关联阶段 done**);split 拆分(子需求直接入 scheduled,父需求仅留 parent_id 溯源)。',
|
|
562
|
+
parameters: {
|
|
563
|
+
action: stringEnum(['add', 'list', 'show', 'board', 'next', 'claim', 'release', 'set', 'link', 'advance', 'split'], '操作。', { required: true }),
|
|
564
|
+
id: string('需求 ID(如 REQ-001)。show/claim/release/set/advance/split 必需。'),
|
|
565
|
+
title: string('需求标题(add 必需;set 为改标题)。'),
|
|
566
|
+
target_version: string('预估版本(如 v1.3.0;纯展示/过滤维度,非门禁)。'),
|
|
567
|
+
priority: stringEnum(REQ_PRIORITIES, '优先级。'),
|
|
568
|
+
workflow_mode: stringEnum(MODES, '需求级工作流模式(general = 无流水线,仅需求级合法)。'),
|
|
569
|
+
status: stringEnum(REQ_STATUSES, '状态(list 过滤 / set 目标状态)。'),
|
|
570
|
+
owner: string('认领人(claim/next 视角;缺省取 .info.json user → git config user.name)。'),
|
|
571
|
+
limit: integer('next 返回候选上限。', { minimum: 1 }),
|
|
572
|
+
stage: string('阶段 ID(link 必需)。'),
|
|
573
|
+
requirement: string('需求 ID(link 关联用)。'),
|
|
574
|
+
unlink: boolean('link 时解除该阶段的需求关联。'),
|
|
575
|
+
to: stringEnum(REQ_STATUSES, 'advance 的目标状态。'),
|
|
576
|
+
into: stringArray('split 的子需求标题列表(至少一项)。'),
|
|
577
|
+
project: string('项目根目录。'),
|
|
578
|
+
},
|
|
579
|
+
output: { schema: envelopeSchema(), render: renderEnvelope },
|
|
580
|
+
async execute(args, exec) {
|
|
581
|
+
const { cwd } = target(exec, args);
|
|
582
|
+
const action = args.action;
|
|
583
|
+
const opts = { cwd, signal: exec.signal };
|
|
584
|
+
|
|
585
|
+
switch (action) {
|
|
586
|
+
case 'add': {
|
|
587
|
+
const argv = ['req', 'add', requireArg(args, 'title', action)];
|
|
588
|
+
pushOption(argv, '--target-version', args.target_version);
|
|
589
|
+
pushOption(argv, '--priority', args.priority);
|
|
590
|
+
pushOption(argv, '--workflow-mode', args.workflow_mode);
|
|
591
|
+
return envelope(action, async () => {
|
|
592
|
+
const out = await cli.write(argv, opts);
|
|
593
|
+
return { data: { output: out.text }, summary: '需求已创建。' };
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
case 'list': {
|
|
597
|
+
const argv = ['req', 'list', '--json'];
|
|
598
|
+
pushOption(argv, '--status', args.status);
|
|
599
|
+
pushOption(argv, '--target-version', args.target_version);
|
|
600
|
+
pushOption(argv, '--owner', args.owner);
|
|
601
|
+
return envelope(action, async () => {
|
|
602
|
+
const data = await cli.json(argv, opts);
|
|
603
|
+
const count = Array.isArray(data?.requirements) ? data.requirements.length : 0;
|
|
604
|
+
return { data, summary: `共 ${count} 条需求。` };
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
case 'board': {
|
|
608
|
+
const argv = ['req', 'board', '--json'];
|
|
609
|
+
pushOption(argv, '--target-version', args.target_version);
|
|
610
|
+
return envelope(action, async () => {
|
|
611
|
+
const data = await cli.json(argv, opts);
|
|
612
|
+
return { data, summary: '已读取需求看板(7 组固定顺序)。' };
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
case 'next': {
|
|
616
|
+
const argv = ['req', 'next', '--json'];
|
|
617
|
+
pushOption(argv, '--owner', args.owner);
|
|
618
|
+
pushOption(argv, '--limit', args.limit);
|
|
619
|
+
return envelope(action, async () => {
|
|
620
|
+
const data = await cli.json(argv, opts);
|
|
621
|
+
const picked = data?.requirement;
|
|
622
|
+
return {
|
|
623
|
+
data,
|
|
624
|
+
summary: picked === undefined || picked === null ? '当前没有可接取的 scheduled 需求。' : `推荐接取 ${picked.id ?? ''} ${picked.title ?? ''}`.trim(),
|
|
625
|
+
next: picked === undefined || picked === null || picked.id === undefined
|
|
626
|
+
? null
|
|
627
|
+
: { tool: 'feel_req', arguments: { action: 'claim', id: picked.id }, why: '认领该需求以占用,防并发覆盖。' },
|
|
628
|
+
};
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
case 'show': {
|
|
632
|
+
const id = requireArg(args, 'id', action);
|
|
633
|
+
return envelope(action, async () => {
|
|
634
|
+
const data = await cli.json(['req', 'show', id, '--json'], opts);
|
|
635
|
+
return { data, summary: `已读取需求 ${id}。` };
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
case 'claim': {
|
|
639
|
+
const id = requireArg(args, 'id', action);
|
|
640
|
+
const argv = ['req', 'claim', id];
|
|
641
|
+
pushOption(argv, '--owner', args.owner);
|
|
642
|
+
return envelope(action, async () => {
|
|
643
|
+
const out = await cli.write(argv, opts);
|
|
644
|
+
return { data: { id, output: out.text }, summary: `需求 ${id} 已认领。` };
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
case 'release': {
|
|
648
|
+
const id = requireArg(args, 'id', action);
|
|
649
|
+
return envelope(action, async () => {
|
|
650
|
+
const out = await cli.write(['req', 'release', id], opts);
|
|
651
|
+
return { data: { id, output: out.text }, summary: `需求 ${id} 已释放。` };
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
case 'set': {
|
|
655
|
+
const id = requireArg(args, 'id', action);
|
|
656
|
+
const argv = ['req', 'set', id];
|
|
657
|
+
pushOption(argv, '--status', args.status);
|
|
658
|
+
pushOption(argv, '--title', args.title);
|
|
659
|
+
pushOption(argv, '--target-version', args.target_version);
|
|
660
|
+
pushOption(argv, '--priority', args.priority);
|
|
661
|
+
pushOption(argv, '--workflow-mode', args.workflow_mode);
|
|
662
|
+
if (argv.length === 3) throw new FeelError('req set 至少需要一个字段(status/title/target_version/priority/workflow_mode)', { kind: 'usage', code: 'FEEL_BAD_USAGE' });
|
|
663
|
+
return envelope(action, async () => {
|
|
664
|
+
const out = await cli.write(argv, opts);
|
|
665
|
+
return { data: { id, output: out.text }, summary: `需求 ${id} 字段已更新。` };
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
case 'link': {
|
|
669
|
+
const stage = requireArg(args, 'stage', action);
|
|
670
|
+
const argv = ['req', 'link', '--stage', stage];
|
|
671
|
+
if (args.unlink === true) {
|
|
672
|
+
argv.push('--unlink');
|
|
673
|
+
} else {
|
|
674
|
+
argv.push('--requirement', requireArg(args, 'requirement', action));
|
|
675
|
+
}
|
|
676
|
+
return envelope(action, async () => {
|
|
677
|
+
const out = await cli.write(argv, opts);
|
|
678
|
+
return {
|
|
679
|
+
data: { stage, requirement: args.requirement ?? null, unlinked: args.unlink === true, output: out.text },
|
|
680
|
+
summary: args.unlink === true ? `阶段 ${stage} 需求关联已解除。` : `阶段 ${stage} 已关联需求 ${args.requirement}。`,
|
|
681
|
+
};
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
case 'advance': {
|
|
685
|
+
const id = requireArg(args, 'id', action);
|
|
686
|
+
const argv = ['req', 'advance', id];
|
|
687
|
+
pushOption(argv, '--to', requireArg(args, 'to', action));
|
|
688
|
+
return envelope(action, async () => {
|
|
689
|
+
const out = await cli.write(argv, opts);
|
|
690
|
+
return { data: { id, to: args.to, output: out.text }, summary: `需求 ${id} 已推进到 ${args.to}。` };
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
case 'split': {
|
|
694
|
+
const id = requireArg(args, 'id', action);
|
|
695
|
+
const into = Array.isArray(args.into) ? args.into.filter((item) => typeof item === 'string' && item.trim() !== '') : [];
|
|
696
|
+
if (into.length === 0) {
|
|
697
|
+
throw new FeelError('req split 需要至少一个 --into 子需求标题', { kind: 'usage', code: 'FEEL_BAD_USAGE' });
|
|
698
|
+
}
|
|
699
|
+
const argv = ['req', 'split', id, '--json'];
|
|
700
|
+
for (const title of into) argv.push('--into', title);
|
|
701
|
+
return envelope(action, async () => {
|
|
702
|
+
const data = await cli.json(argv, opts);
|
|
703
|
+
const children = Array.isArray(data?.children) ? data.children : [];
|
|
704
|
+
return {
|
|
705
|
+
data,
|
|
706
|
+
summary: `需求 ${id} 已拆分为 ${children.length} 个子需求(均已入 scheduled)。`,
|
|
707
|
+
next: children.length === 0 || children[0].id === undefined
|
|
708
|
+
? null
|
|
709
|
+
: { tool: 'feel_req', arguments: { action: 'next' }, why: '选取下一个可接取的子需求。' },
|
|
710
|
+
};
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
default:
|
|
714
|
+
throw new FeelError(`未知 action: ${action}`, { kind: 'usage', code: 'FEEL_UNKNOWN_ACTION' });
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
presentCall: (args) => presentCall(`Feel 需求 ${args.action}`, args.action, args.id ?? args.title),
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
/* ══════════════════════════ 7. feel_checkpoint ══════════════════════════ */
|
|
721
|
+
|
|
722
|
+
const feelCheckpoint = {
|
|
723
|
+
name: 'feel_checkpoint',
|
|
724
|
+
description:
|
|
725
|
+
'Checkpoint 快照管理:相位推进时自动保存 flow.json 快照。action=list 列出快照(可按阶段过滤);action=restore 从快照恢复(覆盖当前 flow.json,需 force 确认;可用 stage 做选择性回退、dry_run 预览差异)。',
|
|
726
|
+
parameters: {
|
|
727
|
+
action: stringEnum(['list', 'restore'], '操作。', { required: true }),
|
|
728
|
+
stage: string('阶段 ID(list 过滤 / restore 选择性回退)。'),
|
|
729
|
+
checkpoint_file: string('快照文件名(restore 必需,如 v1.2.1-stage-93-20260807T162300-exec_running.json)。'),
|
|
730
|
+
force: boolean('restore 确认(覆盖当前 flow.json)。'),
|
|
731
|
+
dry_run: boolean('restore 仅预览差异。'),
|
|
732
|
+
project: string('项目根目录。'),
|
|
733
|
+
},
|
|
734
|
+
output: { schema: envelopeSchema(), render: renderEnvelope },
|
|
735
|
+
async execute(args, exec) {
|
|
736
|
+
const { cwd } = target(exec, args);
|
|
737
|
+
const action = args.action;
|
|
738
|
+
const opts = { cwd, signal: exec.signal };
|
|
739
|
+
|
|
740
|
+
if (action === 'list') {
|
|
741
|
+
const argv = ['flow', 'checkpoint', 'list'];
|
|
742
|
+
pushPositional(argv, args.stage);
|
|
743
|
+
argv.push('--json');
|
|
744
|
+
return envelope(action, async () => {
|
|
745
|
+
const data = await cli.json(argv, opts);
|
|
746
|
+
return { data, summary: '已列出 Checkpoint 快照。' };
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const file = requireArg(args, 'checkpoint_file', action);
|
|
751
|
+
const argv = ['flow', 'checkpoint', 'restore', file];
|
|
752
|
+
pushFlag(argv, '--force', args.force);
|
|
753
|
+
pushOption(argv, '--stage', args.stage);
|
|
754
|
+
pushFlag(argv, '--dry-run', args.dry_run);
|
|
755
|
+
if (args.dry_run !== true && args.force !== true) {
|
|
756
|
+
throw new FeelError('restore 会覆盖当前 flow.json;请显式传 force=true 确认,或先用 dry_run=true 预览差异。', {
|
|
757
|
+
kind: 'usage',
|
|
758
|
+
code: 'FEEL_RESTORE_NEEDS_FORCE',
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
return envelope(action, async () => {
|
|
762
|
+
const out = await cli.write(argv, opts);
|
|
763
|
+
return {
|
|
764
|
+
data: { checkpointFile: file, stage: args.stage ?? null, dryRun: args.dry_run === true, output: out.text },
|
|
765
|
+
summary: args.dry_run === true ? '已预览恢复差异(未写盘)。' : `已从 ${file} 恢复 flow.json。`,
|
|
766
|
+
};
|
|
767
|
+
});
|
|
768
|
+
},
|
|
769
|
+
presentCall: (args) => presentCall(`Feel Checkpoint ${args.action}`, args.action, args.checkpoint_file),
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
/* ══════════════════════════ 8. feel_archive ══════════════════════════ */
|
|
773
|
+
|
|
774
|
+
const feelArchive = {
|
|
775
|
+
name: 'feel_archive',
|
|
776
|
+
description:
|
|
777
|
+
'版本管理。action=list 列出已归档版本;action=show 查看某归档版本;action=create 执行 flow.json 版本分片归档(判据门:该版本全部阶段 done **且** 无 blocking open REV;**无 force 覆盖**;归档后该版本阶段从 flow.json 移除);action=stage_summary 是**另一个**功能(顶层 openfeel archive <stage>,生成阶段摘要到 .openfeel/log/,不动 flow.json)。v1.2.4 起 stage_summary **拒绝覆盖**已存在的归档文件(`ARCHIVE_EXISTS`,零写盘);确需覆盖须传 force=true。',
|
|
778
|
+
parameters: {
|
|
779
|
+
action: stringEnum(['list', 'show', 'create', 'stage_summary'], '操作。', { required: true }),
|
|
780
|
+
version: string('版本号(如 v1.2.0;前缀 v 可省略)。show/create 必需。'),
|
|
781
|
+
stage: string('阶段 ID(stage_summary 必需)。'),
|
|
782
|
+
dry_run: boolean('create 仅预览判据与将归档内容,零写盘。'),
|
|
783
|
+
force: boolean('stage_summary 覆盖已存在的归档文件(默认拒绝;覆盖会冲掉既有摘要)。'),
|
|
784
|
+
project: string('项目根目录。'),
|
|
785
|
+
},
|
|
786
|
+
output: { schema: envelopeSchema(), render: renderEnvelope },
|
|
787
|
+
async execute(args, exec) {
|
|
788
|
+
const { cwd } = target(exec, args);
|
|
789
|
+
const action = args.action;
|
|
790
|
+
const opts = { cwd, signal: exec.signal };
|
|
791
|
+
|
|
792
|
+
switch (action) {
|
|
793
|
+
case 'list':
|
|
794
|
+
return envelope(action, async () => {
|
|
795
|
+
const data = await cli.json(['flow', 'archive', 'list', '--json'], opts);
|
|
796
|
+
const versions = Array.isArray(data?.versions) ? data.versions : [];
|
|
797
|
+
return { data, summary: `已归档 ${versions.length} 个版本。` };
|
|
798
|
+
});
|
|
799
|
+
case 'show': {
|
|
800
|
+
const version = requireArg(args, 'version', action);
|
|
801
|
+
return envelope(action, async () => {
|
|
802
|
+
const data = await cli.json(['flow', 'archive', 'show', version, '--json'], opts);
|
|
803
|
+
return { data, summary: `已读取归档版本 ${version}。` };
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
case 'create': {
|
|
807
|
+
const version = requireArg(args, 'version', action);
|
|
808
|
+
const argv = ['flow', 'archive', version, '--json'];
|
|
809
|
+
pushFlag(argv, '--dry-run', args.dry_run);
|
|
810
|
+
return envelope(action, async () => {
|
|
811
|
+
const data = await cli.json(argv, opts);
|
|
812
|
+
return {
|
|
813
|
+
data,
|
|
814
|
+
summary: args.dry_run === true ? `归档判据预演(${version}):未写盘。` : `版本 ${version} 已分片归档。`,
|
|
815
|
+
};
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
case 'stage_summary': {
|
|
819
|
+
const stage = requireArg(args, 'stage', action);
|
|
820
|
+
return envelope(action, async () => {
|
|
821
|
+
// 出口 #33(json-contract §5):archive <stage> --json。
|
|
822
|
+
// v1.2.4 起 CLI 会**拒绝覆盖**已存在的归档文件(ARCHIVE_EXISTS / refusal /
|
|
823
|
+
// 零写盘);force=true 才显式覆盖。这里把拒绝翻译成可操作的提示,
|
|
824
|
+
// 而不是让模型盲目重试。
|
|
825
|
+
const argv = ['archive', stage, '--json'];
|
|
826
|
+
if (args.force === true) argv.push('--force');
|
|
827
|
+
try {
|
|
828
|
+
const data = await cli.json(argv, opts);
|
|
829
|
+
const ops = typeof data?.opsCount === 'number' ? data.opsCount : 0;
|
|
830
|
+
const reviews = typeof data?.reviewsCount === 'number' ? data.reviewsCount : 0;
|
|
831
|
+
return {
|
|
832
|
+
data,
|
|
833
|
+
summary: args.force === true
|
|
834
|
+
? `阶段 ${stage} 归档已**覆盖重写**:${ops} 个 op、${reviews} 条 REV。`
|
|
835
|
+
: `阶段 ${stage} 摘要已归档:${ops} 个 op、${reviews} 条 REV。`,
|
|
836
|
+
};
|
|
837
|
+
} catch (error) {
|
|
838
|
+
if (error instanceof FeelError && error.code === 'ARCHIVE_EXISTS') {
|
|
839
|
+
throw new FeelError(
|
|
840
|
+
`阶段 ${stage} 的归档文件已存在,OpenFeel 已拒绝覆盖且未写盘。\n`
|
|
841
|
+
+ '如确认要用自动生成的内容**覆盖**既有摘要(会丢失人工整理内容),请以 force=true 重试;'
|
|
842
|
+
+ '否则无需任何操作——既有归档保持不变。',
|
|
843
|
+
{ kind: 'refusal', code: 'ARCHIVE_EXISTS', detail: { ...error.detail, stage } },
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
throw error;
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
default:
|
|
851
|
+
throw new FeelError(`未知 action: ${action}`, { kind: 'usage', code: 'FEEL_UNKNOWN_ACTION' });
|
|
852
|
+
}
|
|
853
|
+
},
|
|
854
|
+
presentCall: (args) => presentCall(`Feel 版本 ${args.action}`, args.action, args.version),
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
/* ══════════════════════════ 9. feel_kb ══════════════════════════ */
|
|
858
|
+
|
|
859
|
+
const feelKb = {
|
|
860
|
+
name: 'feel_kb',
|
|
861
|
+
description:
|
|
862
|
+
'知识库(.openfeel/kb/,4 个固定分类)。action=list 按分类列出;action=add 添加条目;action=search 关键词检索(大小写不敏感子串,带高亮);action=index 索引概览;action=dedup 相似度检查(只读建议,阈值默认 0.8)。沉淀前先去重。',
|
|
863
|
+
parameters: {
|
|
864
|
+
action: stringEnum(['list', 'add', 'search', 'index', 'dedup'], '操作。', { required: true }),
|
|
865
|
+
category: stringEnum(KB_CATEGORIES, '分类(list 过滤 / add 必需 / dedup 限定)。'),
|
|
866
|
+
title: string('条目标题(add 必需)。'),
|
|
867
|
+
content: string('条目内容(add;省略则从 stdin 读,本适配器要求显式提供)。'),
|
|
868
|
+
query: string('搜索关键词(search 必需)。'),
|
|
869
|
+
limit: integer('search 结果上限。', { minimum: 1 }),
|
|
870
|
+
offset: integer('search 偏移量。', { minimum: 0 }),
|
|
871
|
+
threshold: string('dedup 相似度阈值 0~1(默认 0.8)。'),
|
|
872
|
+
project: string('项目根目录。'),
|
|
873
|
+
},
|
|
874
|
+
output: { schema: envelopeSchema(), render: renderEnvelope },
|
|
875
|
+
async execute(args, exec) {
|
|
876
|
+
const { cwd } = target(exec, args);
|
|
877
|
+
const action = args.action;
|
|
878
|
+
const opts = { cwd, signal: exec.signal };
|
|
879
|
+
|
|
880
|
+
switch (action) {
|
|
881
|
+
case 'list': {
|
|
882
|
+
const argv = ['knowledge', 'list'];
|
|
883
|
+
pushOption(argv, '--type', args.category);
|
|
884
|
+
argv.push('--json');
|
|
885
|
+
return envelope(action, async () => {
|
|
886
|
+
const data = await cli.json(argv, opts);
|
|
887
|
+
return { data, summary: '已列出知识条目。' };
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
case 'add': {
|
|
891
|
+
const category = requireArg(args, 'category', action);
|
|
892
|
+
const title = requireArg(args, 'title', action);
|
|
893
|
+
const argv = ['knowledge', 'add', category, title];
|
|
894
|
+
pushOption(argv, '--content', requireArg(args, 'content', action));
|
|
895
|
+
return envelope(action, async () => {
|
|
896
|
+
const out = await cli.write(argv, opts);
|
|
897
|
+
return { data: { category, title, output: out.text }, summary: `知识条目「${title}」已归档到 ${category}。` };
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
case 'search': {
|
|
901
|
+
const query = requireArg(args, 'query', action);
|
|
902
|
+
const argv = ['knowledge', 'search', query];
|
|
903
|
+
pushOption(argv, '--limit', args.limit);
|
|
904
|
+
pushOption(argv, '--offset', args.offset);
|
|
905
|
+
argv.push('--json');
|
|
906
|
+
return envelope(action, async () => {
|
|
907
|
+
const data = await cli.json(argv, opts);
|
|
908
|
+
return { data, summary: `已检索「${query}」。` };
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
case 'index':
|
|
912
|
+
return envelope(action, async () => {
|
|
913
|
+
const data = await cli.json(['knowledge', 'index', '--json'], opts);
|
|
914
|
+
return { data, summary: '已读取知识库索引概览。' };
|
|
915
|
+
});
|
|
916
|
+
case 'dedup': {
|
|
917
|
+
const content = requireArg(args, 'content', action);
|
|
918
|
+
const argv = ['knowledge', 'dedup', content];
|
|
919
|
+
pushOption(argv, '--category', args.category);
|
|
920
|
+
pushOption(argv, '--threshold', args.threshold);
|
|
921
|
+
argv.push('--json');
|
|
922
|
+
return envelope(action, async () => {
|
|
923
|
+
const data = await cli.json(argv, opts);
|
|
924
|
+
return { data, summary: '已检查相似知识条目(只读)。' };
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
default:
|
|
928
|
+
throw new FeelError(`未知 action: ${action}`, { kind: 'usage', code: 'FEEL_UNKNOWN_ACTION' });
|
|
929
|
+
}
|
|
930
|
+
},
|
|
931
|
+
presentCall: (args) => presentCall(`Feel 知识库 ${args.action}`, args.action, args.query ?? args.title),
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
/* ══════════════════════════ 10. feel_verify ══════════════════════════ */
|
|
935
|
+
|
|
936
|
+
const feelVerify = {
|
|
937
|
+
name: 'feel_verify',
|
|
938
|
+
description:
|
|
939
|
+
'健康检查与对账。action=health 全面检查 flow.json / 跨文件一致性 / 僵尸状态 / config.yaml(fix=true 时以 flow.json 为权威回写 status.md 的状态字段);action=lint 项目健康检查(i18n 键一致性 / kb 引用有效性);action=recover 跨会话上下文恢复(流水线状态、阻塞原因、待处理任务),换会话或中断后先跑它;action=project 结构化项目概览;action=config_get / config_set 读写项目配置(workflow_mode / auto_advance / merge_mode / execution_mode)。',
|
|
940
|
+
parameters: {
|
|
941
|
+
action: stringEnum(['health', 'lint', 'recover', 'project', 'config_get', 'config_set', 'config_effective'], '操作。', { required: true }),
|
|
942
|
+
quick: boolean('health 仅检查关键项。'),
|
|
943
|
+
fix: boolean('health 以 flow.json 为权威回写 status.md 状态字段。'),
|
|
944
|
+
dry_run: boolean('health fix 仅预览。'),
|
|
945
|
+
target: stringEnum(['i18n', 'kb'], 'lint 的目标。'),
|
|
946
|
+
key: string('配置键(config_get/config_set;支持 defaults.X 或 X)。'),
|
|
947
|
+
value: string('配置值(config_set)。'),
|
|
948
|
+
sync_stages: boolean('config_set 时同步写入所有已注册阶段的 status.md。'),
|
|
949
|
+
project: string('项目根目录。'),
|
|
950
|
+
},
|
|
951
|
+
output: { schema: envelopeSchema(), render: renderEnvelope },
|
|
952
|
+
async execute(args, exec) {
|
|
953
|
+
const { cwd } = target(exec, args);
|
|
954
|
+
const action = args.action;
|
|
955
|
+
const opts = { cwd, signal: exec.signal };
|
|
956
|
+
|
|
957
|
+
switch (action) {
|
|
958
|
+
case 'health': {
|
|
959
|
+
const argv = ['flow', 'health', '--json'];
|
|
960
|
+
pushFlag(argv, '--quick', args.quick);
|
|
961
|
+
pushFlag(argv, '--fix', args.fix);
|
|
962
|
+
pushFlag(argv, '--dry-run', args.dry_run);
|
|
963
|
+
return envelope(action, async () => {
|
|
964
|
+
const data = await cli.json(argv, opts);
|
|
965
|
+
return { data, summary: '已执行流水线健康检查。' };
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
case 'lint': {
|
|
969
|
+
const target = args.target ?? 'kb';
|
|
970
|
+
return envelope(action, async () => {
|
|
971
|
+
const data = await cli.json(['lint', target, '--json'], opts);
|
|
972
|
+
const ok = data?.ok !== false;
|
|
973
|
+
const issues = Array.isArray(data?.issues) ? data.issues.length : 0;
|
|
974
|
+
return { data, summary: `lint ${target} ${ok ? '通过' : '发现问题'}(${issues} 项)。` };
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
case 'recover':
|
|
978
|
+
return envelope(action, async () => {
|
|
979
|
+
const data = await cli.json(['flow', 'recover', '--json'], opts);
|
|
980
|
+
const pending = Array.isArray(data?.pendingTasks) ? data.pendingTasks.length : 0;
|
|
981
|
+
const blocked = typeof data?.blockedBy === 'string' && data.blockedBy !== '' ? `,阻塞:${data.blockedBy}` : '';
|
|
982
|
+
return { data, summary: `已恢复跨会话上下文:阶段状态=${data?.stageStatus ?? '(未知)'},待处理任务 ${pending} 项${blocked}。` };
|
|
983
|
+
});
|
|
984
|
+
case 'project':
|
|
985
|
+
return envelope(action, async () => {
|
|
986
|
+
const data = await cli.json(['project', 'overview', '--json'], opts);
|
|
987
|
+
return { data, summary: '已输出项目概览。' };
|
|
988
|
+
});
|
|
989
|
+
case 'config_get': {
|
|
990
|
+
// v1.2.4 起 `config get` 无 key 不再报 key-required,而是 `--all` 返回全量。
|
|
991
|
+
// 有 key 时保持原行为。
|
|
992
|
+
const argv = ['config', 'get'];
|
|
993
|
+
if (args.key === undefined || args.key === '') {
|
|
994
|
+
argv.push('--all', '--json');
|
|
995
|
+
} else {
|
|
996
|
+
argv.push(args.key, '--json');
|
|
997
|
+
}
|
|
998
|
+
return envelope(action, async () => {
|
|
999
|
+
const data = await cli.json(argv, opts);
|
|
1000
|
+
const summary = args.key === undefined || args.key === ''
|
|
1001
|
+
? `已读取全量配置(scope=${data?.scope ?? '?'})。`
|
|
1002
|
+
: `已读取配置 ${args.key} = ${JSON.stringify(data?.value)}(scope=${data?.scope ?? '?'})。`;
|
|
1003
|
+
return { data, summary };
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
case 'config_effective': {
|
|
1007
|
+
const argv = ['config', 'effective'];
|
|
1008
|
+
pushPositional(argv, args.key);
|
|
1009
|
+
argv.push('--json');
|
|
1010
|
+
return envelope(action, async () => {
|
|
1011
|
+
const data = await cli.json(argv, opts);
|
|
1012
|
+
return { data, summary: '已读取配置有效值与来源。' };
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
case 'config_set': {
|
|
1016
|
+
const key = requireArg(args, 'key', action);
|
|
1017
|
+
const value = requireArg(args, 'value', action);
|
|
1018
|
+
const argv = ['config', 'set', key, value];
|
|
1019
|
+
pushFlag(argv, '--sync-stages', args.sync_stages);
|
|
1020
|
+
return envelope(action, async () => {
|
|
1021
|
+
const out = await cli.write(argv, opts);
|
|
1022
|
+
return { data: { key, value, output: out.text }, summary: `配置 ${key} 已设为 ${value}。` };
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
default:
|
|
1026
|
+
throw new FeelError(`未知 action: ${action}`, { kind: 'usage', code: 'FEEL_UNKNOWN_ACTION' });
|
|
1027
|
+
}
|
|
1028
|
+
},
|
|
1029
|
+
presentCall: (args) => presentCall(`Feel 校验 ${args.action}`, args.action, args.key ?? args.target),
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
/* ══════════════════════════ 11. feel_review ══════════════════════════ */
|
|
1033
|
+
|
|
1034
|
+
const feelReview = {
|
|
1035
|
+
name: 'feel_review',
|
|
1036
|
+
description:
|
|
1037
|
+
'审查条目(REV)视图。action=list 列出(可按 op 过滤);action=accept 验收(标记 closed)。新增/修改/删除请用 feel_flow 的 review_add / review_update / review_remove。blocking 未被显式设为 false 的 open REV 会阻塞进入 done 与版本归档。',
|
|
1038
|
+
parameters: {
|
|
1039
|
+
action: stringEnum(['list', 'accept'], '操作。', { required: true }),
|
|
1040
|
+
op: string('按操作 ID 过滤(list)。'),
|
|
1041
|
+
rev_id: string('REV 条目 ID(accept 必需)。'),
|
|
1042
|
+
project: string('项目根目录。'),
|
|
1043
|
+
},
|
|
1044
|
+
output: { schema: envelopeSchema(), render: renderEnvelope },
|
|
1045
|
+
async execute(args, exec) {
|
|
1046
|
+
const { cwd } = target(exec, args);
|
|
1047
|
+
const action = args.action;
|
|
1048
|
+
const opts = { cwd, signal: exec.signal };
|
|
1049
|
+
|
|
1050
|
+
if (action === 'list') {
|
|
1051
|
+
const argv = ['view', 'list'];
|
|
1052
|
+
pushOption(argv, '--op', args.op);
|
|
1053
|
+
argv.push('--json');
|
|
1054
|
+
return envelope(action, async () => {
|
|
1055
|
+
const data = await cli.json(argv, opts);
|
|
1056
|
+
const total = Array.isArray(data?.reviews) ? data.reviews.length : Array.isArray(data?.items) ? data.items.length : 0;
|
|
1057
|
+
return { data, summary: `已列出审查条目(${total} 条)。` };
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const revId = requireArg(args, 'rev_id', action);
|
|
1062
|
+
return envelope(action, async () => {
|
|
1063
|
+
const out = await cli.write(['view', 'accept', revId], opts);
|
|
1064
|
+
return { data: { revId, output: out.text }, summary: `REV ${revId} 已验收(closed)。` };
|
|
1065
|
+
});
|
|
1066
|
+
},
|
|
1067
|
+
presentCall: (args) => presentCall(`Feel 审查 ${args.action}`, args.action, args.rev_id),
|
|
1068
|
+
};
|
|
1069
|
+
|
|
1070
|
+
/* ══════════════════════════ 12. feel_plan ══════════════════════════ */
|
|
1071
|
+
|
|
1072
|
+
const feelPlan = {
|
|
1073
|
+
name: 'feel_plan',
|
|
1074
|
+
description:
|
|
1075
|
+
'计划与路线图。action=stage_add 是 feel_stage.add 的别名(完整入口);action=stage_list 列出工作阶段;action=scheme_list 列出操作方案;action=roadmap_create 创建版本分期大纲;action=roadmap_show 查看大纲;action=instructions 为指定 artifact 生成结构化指令(XML 或 JSON)。',
|
|
1076
|
+
parameters: {
|
|
1077
|
+
action: stringEnum(['stage_list', 'scheme_list', 'roadmap_create', 'roadmap_show', 'instructions'], '操作。', { required: true }),
|
|
1078
|
+
stage: string('阶段 ID(scheme_list 过滤)。'),
|
|
1079
|
+
version: string('版本号(roadmap_create 必需,如 1.0;roadmap_show 可选)。'),
|
|
1080
|
+
artifact: string('artifact ID(instructions 必需,如 proposal / implementation)。'),
|
|
1081
|
+
change: string('变更名称(instructions 的 --change)。'),
|
|
1082
|
+
json: boolean('instructions 以 JSON 输出。'),
|
|
1083
|
+
project: string('项目根目录。'),
|
|
1084
|
+
},
|
|
1085
|
+
output: { schema: envelopeSchema(), render: renderEnvelope },
|
|
1086
|
+
async execute(args, exec) {
|
|
1087
|
+
const { cwd } = target(exec, args);
|
|
1088
|
+
const action = args.action;
|
|
1089
|
+
const opts = { cwd, signal: exec.signal };
|
|
1090
|
+
|
|
1091
|
+
switch (action) {
|
|
1092
|
+
case 'stage_list':
|
|
1093
|
+
return envelope(action, async () => {
|
|
1094
|
+
const data = await cli.json(['plan', 'stage', 'list', '--json'], opts);
|
|
1095
|
+
const stages = Array.isArray(data?.stages) ? data.stages : [];
|
|
1096
|
+
return { data, summary: `已列出工作阶段(${stages.length} 个)。` };
|
|
1097
|
+
});
|
|
1098
|
+
case 'scheme_list': {
|
|
1099
|
+
const argv = ['plan', 'scheme', 'list'];
|
|
1100
|
+
pushPositional(argv, args.stage);
|
|
1101
|
+
argv.push('--json');
|
|
1102
|
+
return envelope(action, async () => {
|
|
1103
|
+
const data = await cli.json(argv, opts);
|
|
1104
|
+
return { data, summary: '已列出操作方案。' };
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
case 'roadmap_create': {
|
|
1108
|
+
const version = requireArg(args, 'version', action);
|
|
1109
|
+
return envelope(action, async () => {
|
|
1110
|
+
const out = await cli.write(['roadmap', 'create', version], opts);
|
|
1111
|
+
return { data: { version, output: out.text }, summary: `已创建 ${version} 分期大纲。` };
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
case 'roadmap_show': {
|
|
1115
|
+
const argv = ['roadmap', 'show'];
|
|
1116
|
+
pushPositional(argv, args.version);
|
|
1117
|
+
argv.push('--json');
|
|
1118
|
+
return envelope(action, async () => {
|
|
1119
|
+
const data = await cli.json(argv, opts);
|
|
1120
|
+
const milestones = Array.isArray(data?.milestones) ? data.milestones.length : 0;
|
|
1121
|
+
// 不传版本时 CLI 返回空态 { version: null, milestones: [] }(不自动发现版本)。
|
|
1122
|
+
const summary = data?.version === null || data?.version === undefined
|
|
1123
|
+
? '未指定版本:roadmap show --json 不自动发现版本,请显式传 version。'
|
|
1124
|
+
: `已读取 ${data.version} 分期大纲(${milestones} 个里程碑)。`;
|
|
1125
|
+
return { data, summary };
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
case 'instructions': {
|
|
1129
|
+
const artifact = requireArg(args, 'artifact', action);
|
|
1130
|
+
const argv = ['instructions', artifact];
|
|
1131
|
+
pushOption(argv, '--change', args.change);
|
|
1132
|
+
pushFlag(argv, '--json', args.json);
|
|
1133
|
+
return envelope(action, async () => {
|
|
1134
|
+
// `instructions` 默认产出 XML 文本;显式 json=true 时按 JSON 解析,
|
|
1135
|
+
// 避免把结构化结果当字符串塞给模型。
|
|
1136
|
+
if (args.json === true) {
|
|
1137
|
+
const data = await cli.json(argv, opts);
|
|
1138
|
+
return { data, summary: `已生成 ${artifact} 的结构化指令(JSON)。` };
|
|
1139
|
+
}
|
|
1140
|
+
const out = await cli.text(argv, opts);
|
|
1141
|
+
return { data: { artifact, format: 'xml', output: out.text }, summary: `已生成 ${artifact} 的结构化指令(XML)。` };
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
default:
|
|
1145
|
+
throw new FeelError(`未知 action: ${action}`, { kind: 'usage', code: 'FEEL_UNKNOWN_ACTION' });
|
|
1146
|
+
}
|
|
1147
|
+
},
|
|
1148
|
+
presentCall: (args) => presentCall(`Feel 计划 ${args.action}`, args.action, args.artifact ?? args.version),
|
|
1149
|
+
};
|
|
1150
|
+
|
|
1151
|
+
/* ══════════════════════════ 13. feel_model ══════════════════════════ */
|
|
1152
|
+
|
|
1153
|
+
const feelModel = {
|
|
1154
|
+
name: 'feel_model',
|
|
1155
|
+
description:
|
|
1156
|
+
'Agent 模型配置(三层级:default / global / project)。action=list 列出所有 agent 的模型;action=get 读取某 agent 的模型;action=set 设置某 agent 的模型。注意:这是 OpenFeel 的**模型意识**配置,不改变 DSH 实际使用的模型;用户主权键 model 在部署时永不被覆盖。',
|
|
1157
|
+
parameters: {
|
|
1158
|
+
action: stringEnum(['list', 'get', 'set'], '操作。', { required: true }),
|
|
1159
|
+
agent: string('Agent ID(get/set 必需,如 openfeel-executor)。'),
|
|
1160
|
+
model: string('模型 ID(set 必需,如 deepseek/deepseek-flash)。'),
|
|
1161
|
+
scope: stringEnum(['default', 'global', 'project'], '层级,缺省 project。'),
|
|
1162
|
+
project: string('项目根目录。'),
|
|
1163
|
+
},
|
|
1164
|
+
output: { schema: envelopeSchema(), render: renderEnvelope },
|
|
1165
|
+
async execute(args, exec) {
|
|
1166
|
+
const { cwd } = target(exec, args);
|
|
1167
|
+
const action = args.action;
|
|
1168
|
+
const opts = { cwd, signal: exec.signal };
|
|
1169
|
+
|
|
1170
|
+
switch (action) {
|
|
1171
|
+
case 'list': {
|
|
1172
|
+
const argv = ['model', 'list'];
|
|
1173
|
+
pushOption(argv, '--scope', args.scope);
|
|
1174
|
+
argv.push('--json');
|
|
1175
|
+
return envelope(action, async () => {
|
|
1176
|
+
const data = await cli.json(argv, opts);
|
|
1177
|
+
const agents = Array.isArray(data?.agents) ? data.agents.length : 0;
|
|
1178
|
+
// scope 为 null 表示「未指定层级 → 展示生效值」,不是缺失。
|
|
1179
|
+
const scopeLabel = data?.scope === null || data?.scope === undefined
|
|
1180
|
+
? '生效值(未指定层级)'
|
|
1181
|
+
: String(data.scope);
|
|
1182
|
+
return { data, summary: `已列出 agent 模型配置(${agents} 个,scope=${scopeLabel})。` };
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
case 'get': {
|
|
1186
|
+
const agent = requireArg(args, 'agent', action);
|
|
1187
|
+
const argv = ['model', 'get', agent];
|
|
1188
|
+
pushOption(argv, '--scope', args.scope);
|
|
1189
|
+
argv.push('--json');
|
|
1190
|
+
return envelope(action, async () => {
|
|
1191
|
+
const data = await cli.json(argv, opts);
|
|
1192
|
+
const summary = data?.model === null || data?.model === undefined
|
|
1193
|
+
? `${agent} 未配置模型(未命中任何层级)。`
|
|
1194
|
+
: `${agent} 的模型:${data.model}(provider=${data.provider ?? '?'},来源=${data.source ?? '?'})。`;
|
|
1195
|
+
return { data, summary };
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
case 'set': {
|
|
1199
|
+
const agent = requireArg(args, 'agent', action);
|
|
1200
|
+
const model = requireArg(args, 'model', action);
|
|
1201
|
+
const argv = ['model', 'set', agent, model];
|
|
1202
|
+
pushOption(argv, '--scope', args.scope);
|
|
1203
|
+
return envelope(action, async () => {
|
|
1204
|
+
const out = await cli.write(argv, opts);
|
|
1205
|
+
return { data: { agent, model, output: out.text }, summary: `${agent} 的模型已设为 ${model}。` };
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
default:
|
|
1209
|
+
throw new FeelError(`未知 action: ${action}`, { kind: 'usage', code: 'FEEL_UNKNOWN_ACTION' });
|
|
1210
|
+
}
|
|
1211
|
+
},
|
|
1212
|
+
presentCall: (args) => presentCall(`Feel 模型 ${args.action}`, args.action, args.agent),
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1215
|
+
return [
|
|
1216
|
+
feelStatus,
|
|
1217
|
+
feelNext,
|
|
1218
|
+
feelFlow,
|
|
1219
|
+
feelStage,
|
|
1220
|
+
feelOp,
|
|
1221
|
+
feelReq,
|
|
1222
|
+
feelCheckpoint,
|
|
1223
|
+
feelArchive,
|
|
1224
|
+
feelKb,
|
|
1225
|
+
feelVerify,
|
|
1226
|
+
feelReview,
|
|
1227
|
+
feelPlan,
|
|
1228
|
+
feelModel,
|
|
1229
|
+
// 注册表既不编译也不校验 parameters(本插件不经 defineTool),
|
|
1230
|
+
// 因此在交给注册表前统一转成子集接受的裸 JSON Schema。
|
|
1231
|
+
].map((tool) => ({ ...tool, parameters: compileParameters(tool.parameters) }));
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* 从快照推导建议的下一步(内部复用)。
|
|
1236
|
+
* @param {Record<string, any>} snapshot
|
|
1237
|
+
* @returns {{ tool: string, arguments: any, why: string } | null}
|
|
1238
|
+
*/
|
|
1239
|
+
function snapshotNext(snapshot) {
|
|
1240
|
+
const plan = planNextStep(snapshot);
|
|
1241
|
+
if (plan.toolCall === null || plan.toolCall === undefined) return null;
|
|
1242
|
+
return { tool: plan.toolCall.tool, arguments: plan.toolCall.arguments, why: `推进到 ${plan.toolCall.arguments.to}` };
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
export { PHASES, MODES, TIERS, REQ_STATUSES, KB_CATEGORIES, legalNextPhases };
|